@onjmin/dtm 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -159,18 +159,534 @@ async function buildNameToKeyMapping() {
159
159
  return nameToKey;
160
160
  }
161
161
 
162
+ // node_modules/.pnpm/@onjmin+chord-parser@1.0.2/node_modules/@onjmin/chord-parser/dist/index.mjs
163
+ var SHARP_NAMES = [
164
+ "C",
165
+ "C#",
166
+ "D",
167
+ "D#",
168
+ "E",
169
+ "F",
170
+ "F#",
171
+ "G",
172
+ "G#",
173
+ "A",
174
+ "A#",
175
+ "B"
176
+ ];
177
+ var FLAT_NAMES = [
178
+ "C",
179
+ "Db",
180
+ "D",
181
+ "Eb",
182
+ "E",
183
+ "F",
184
+ "Gb",
185
+ "G",
186
+ "Ab",
187
+ "A",
188
+ "Bb",
189
+ "B"
190
+ ];
191
+ var toPitchClass = (n) => (n % 12 + 12) % 12;
192
+ var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
193
+ var SyntaxErrorWithPos = class extends Error {
194
+ constructor(input, msg) {
195
+ super(
196
+ `SyntaxError: ${msg}
197
+ input.idx: ${input.idx}
198
+ input.str: ${input.str}`
199
+ );
200
+ this.name = "ChordSyntaxError";
201
+ }
202
+ };
203
+ var err = (input, msg) => {
204
+ throw new SyntaxErrorWithPos(input, msg);
205
+ };
206
+ var Input = class _Input {
207
+ static nums = new Set("0123456789");
208
+ str;
209
+ nest;
210
+ idx;
211
+ constructor(str, nest = 0) {
212
+ this.str = str;
213
+ this.nest = nest;
214
+ this.idx = 0;
215
+ }
216
+ get isEOF() {
217
+ return this.str.length <= this.idx;
218
+ }
219
+ get char() {
220
+ return this.str[this.idx];
221
+ }
222
+ /** 先頭の連続する数字を消費して数値で返す(破壊的)。数字が無ければ null。 */
223
+ get num() {
224
+ let str = "";
225
+ while (!this.isEOF) {
226
+ const char = this.char;
227
+ if (!_Input.nums.has(char)) break;
228
+ str += char;
229
+ this.idx++;
230
+ }
231
+ return str.length ? Number(str) : null;
232
+ }
233
+ slice(i) {
234
+ return this.str.slice(this.idx, this.idx + i);
235
+ }
236
+ };
237
+ var Output = class {
238
+ pitch = null;
239
+ chord = null;
240
+ isChord = false;
241
+ pending = null;
242
+ nest = -1;
243
+ get value() {
244
+ const { pitch, chord } = this;
245
+ return new Set(
246
+ [...chord].map((v) => v + pitch)
247
+ );
248
+ }
249
+ set value(chord) {
250
+ const pitch = this.pitch;
251
+ this.chord = new Set([...chord].map((v) => v - pitch));
252
+ }
253
+ };
254
+ var Matcher = class {
255
+ map = /* @__PURE__ */ new Map();
256
+ /** キー長を降順で保持(最長一致のため)。 */
257
+ lengths = [];
258
+ _set(key, value) {
259
+ this.map.set(key, value);
260
+ if (!this.lengths.includes(key.length)) {
261
+ this.lengths.push(key.length);
262
+ this.lengths.sort((a, b) => b - a);
263
+ }
264
+ }
265
+ set(key, value) {
266
+ if (Array.isArray(key)) for (const k of key) this._set(k, value);
267
+ else this._set(key, value);
268
+ }
269
+ parse(input) {
270
+ for (const i of this.lengths) {
271
+ const s = input.slice(i);
272
+ if (this.map.has(s)) {
273
+ input.idx += s.length;
274
+ return this.map.get(s);
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+ };
280
+ var BRACKET_START = 0;
281
+ var BRACKET_END = 1;
282
+ var COMMA = 2;
283
+ var DIVIDE = 3;
284
+ var formulaMatcher = new Matcher();
285
+ formulaMatcher.set("(", BRACKET_START);
286
+ formulaMatcher.set(")", BRACKET_END);
287
+ formulaMatcher.set(",", COMMA);
288
+ formulaMatcher.set(["/", "on"], DIVIDE);
289
+ var parseFormula = (input, output = new Output(), nest = 0) => {
290
+ let start = input.idx;
291
+ const _eval = (idx) => {
292
+ const str = input.str.slice(start, idx);
293
+ if (str.length) parseTerm(new Input(str, nest), output);
294
+ };
295
+ while (true) {
296
+ const { idx } = input;
297
+ if (input.isEOF) {
298
+ if (nest) err(input, `Unclosed ${nest} brackets`);
299
+ _eval(idx);
300
+ return output;
301
+ }
302
+ const res = formulaMatcher.parse(input);
303
+ if (res === null) {
304
+ input.idx++;
305
+ continue;
306
+ }
307
+ const { pending } = output;
308
+ _eval(idx);
309
+ switch (res) {
310
+ case BRACKET_START:
311
+ parseFormula(input, output, nest + 1);
312
+ break;
313
+ case BRACKET_END:
314
+ if (nest - 1 < 0) err(input, "Unable to close brackets");
315
+ return output;
316
+ case COMMA:
317
+ output.pending = pending;
318
+ break;
319
+ case DIVIDE: {
320
+ const o = parseFormula(input, new Output(), nest);
321
+ const v = [...output.value];
322
+ if (o.isChord) {
323
+ output.value = [...o.value].concat(v);
324
+ } else {
325
+ const a = v.sort((x, y) => x - y);
326
+ const pitch = (o.pitch + 3) % 12 - 3;
327
+ if (a[0] < pitch) {
328
+ while (a[0] < pitch) a.push(a.shift() + 12);
329
+ } else {
330
+ while (true) {
331
+ const w = a[a.length - 1] - 12;
332
+ if (w < pitch) break;
333
+ a.pop();
334
+ a.unshift(w);
335
+ }
336
+ }
337
+ a.push(pitch);
338
+ output.value = a;
339
+ }
340
+ break;
341
+ }
342
+ }
343
+ start = input.idx;
344
+ }
345
+ };
346
+ var parseTerm = (input, output) => {
347
+ if (input.isEOF) return output;
348
+ if (output.pitch === null) return parsePitch(input, output);
349
+ if (output.pending === null) return parseFunc(input, output);
350
+ return parsePending(input, output);
351
+ };
352
+ var halfMatcher = new Matcher();
353
+ var halfMatcherStrict = new Matcher();
354
+ for (const m of [halfMatcher, halfMatcherStrict]) {
355
+ m.set(["#", "\u266F"], 1);
356
+ m.set(["b", "\u266D"], -1);
357
+ }
358
+ halfMatcher.set("+", 1);
359
+ halfMatcher.set("-", -1);
360
+ var parseHalf = (input, isPitch = false) => (isPitch ? halfMatcherStrict : halfMatcher).parse(input);
361
+ var idx2pitch = [0, 2, 4, 5, 7, 9, 11];
362
+ for (const i of [...idx2pitch.keys()]) idx2pitch.push(idx2pitch[i] + 12);
363
+ var deg2pitch = (deg) => idx2pitch[deg - 1];
364
+ var pitchMatcher = new Matcher();
365
+ for (const [i, v] of [..."CDEFGAB"].entries())
366
+ pitchMatcher.set(v, idx2pitch[i]);
367
+ var parsePitch = (input, output) => {
368
+ const pitch = pitchMatcher.parse(input);
369
+ if (pitch === null) err(input, "Not found pitch");
370
+ output.pitch = pitch;
371
+ const half = parseHalf(input, true);
372
+ if (half !== null) output.pitch += half;
373
+ return parseBase(input, output);
374
+ };
375
+ var MAJOR = [0, 4, 7];
376
+ var DIM = [0, 3, 6];
377
+ var baseMatcher = new Matcher();
378
+ baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
379
+ baseMatcher.set(["dim", "\u3007"], DIM);
380
+ baseMatcher.set("+", [0, 4, 8]);
381
+ baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
382
+ var parseBase = (input, output) => {
383
+ const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
384
+ const res = isMajMarker ? null : baseMatcher.parse(input);
385
+ if (res !== null) output.isChord = true;
386
+ output.chord = new Set(res || MAJOR);
387
+ if (res === DIM) {
388
+ const { num } = input;
389
+ const chord = output.chord;
390
+ if (num !== null) chord.add(deg2pitch(num) - 2);
391
+ }
392
+ output.nest = input.nest;
393
+ return parseTerm(input, output);
394
+ };
395
+ var add = (chord, n, half) => {
396
+ chord.add(deg2pitch(n) + half);
397
+ };
398
+ var aug = (chord) => {
399
+ chord.delete(deg2pitch(5));
400
+ chord.add(deg2pitch(5) + 1);
401
+ };
402
+ var _7th = (chord, n, _half2, isFlat = false) => {
403
+ if (n === 5) chord.delete(deg2pitch(3));
404
+ else if (n === 6) chord.add(deg2pitch(6));
405
+ else if (n === 69) chord.add(deg2pitch(6)).add(deg2pitch(9));
406
+ else {
407
+ if (n >= 7) chord.add(deg2pitch(7) + (isFlat ? -1 : 0));
408
+ if (n >= 9) chord.add(deg2pitch(9));
409
+ if (n >= 11) chord.add(deg2pitch(11));
410
+ if (n >= 13) chord.add(deg2pitch(13));
411
+ }
412
+ };
413
+ var _half = (chord, n, half) => {
414
+ chord.delete(deg2pitch(n));
415
+ chord.add(deg2pitch(n) + half);
416
+ };
417
+ var funcMatcher = new Matcher();
418
+ funcMatcher.set("add", add);
419
+ funcMatcher.set(["omit", "no"], (chord, n, half) => {
420
+ chord.delete(deg2pitch(n) + half);
421
+ });
422
+ funcMatcher.set("sus", (chord, n, half) => {
423
+ chord.delete(deg2pitch(3));
424
+ chord.add(deg2pitch(n) + half);
425
+ });
426
+ funcMatcher.set(
427
+ ["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
428
+ _7th
429
+ );
430
+ funcMatcher.set("aug", aug);
431
+ var parseFunc = (input, output) => {
432
+ if (!output.isChord) output.isChord = true;
433
+ const func = funcMatcher.parse(input);
434
+ const chord = output.chord;
435
+ if (func === null) {
436
+ const isAug = input.char === "+";
437
+ const half = parseHalf(input);
438
+ const { num } = input;
439
+ if (num === null) {
440
+ if (isAug) aug(chord);
441
+ else err(input, "Not found number");
442
+ }
443
+ if (half === null) {
444
+ if (input.nest === output.nest) _7th(chord, num, 0, true);
445
+ else add(chord, num, 0);
446
+ } else {
447
+ _half(chord, num, half);
448
+ }
449
+ } else if (func === aug) {
450
+ aug(chord);
451
+ } else {
452
+ output.pending = func;
453
+ }
454
+ return parseTerm(input, output);
455
+ };
456
+ var parsePending = (input, output) => {
457
+ const half = parseHalf(input);
458
+ const { num } = input;
459
+ const { pending, chord } = output;
460
+ if (num === null) err(input, "Not found number");
461
+ pending(
462
+ chord,
463
+ num,
464
+ half === null ? 0 : half
465
+ );
466
+ output.pending = null;
467
+ return parseTerm(input, output);
468
+ };
469
+ var parseChord = (symbol) => {
470
+ const output = parseFormula(new Input(symbol));
471
+ const notes = [...output.value].sort((a, b) => a - b);
472
+ const intervals = [...output.chord].sort((a, b) => a - b);
473
+ const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
474
+ (a, b) => a - b
475
+ );
476
+ return {
477
+ symbol,
478
+ root: toPitchClass(output.pitch),
479
+ notes,
480
+ pitchClasses,
481
+ intervals
482
+ };
483
+ };
484
+ var QUALITY_SOURCE = [
485
+ "",
486
+ // major
487
+ "m",
488
+ // minor
489
+ "7",
490
+ // dominant 7th
491
+ "M7",
492
+ // major 7th
493
+ "m7",
494
+ // minor 7th
495
+ "dim",
496
+ // diminished triad
497
+ "m7b5",
498
+ // half-diminished
499
+ "aug",
500
+ // augmented triad
501
+ "6",
502
+ // major 6th
503
+ "m6",
504
+ // minor 6th
505
+ "sus4",
506
+ "sus2",
507
+ "mM7",
508
+ // minor major 7th
509
+ "dim7",
510
+ // diminished 7th
511
+ "7sus4",
512
+ "7#5",
513
+ // augmented 7th
514
+ "add9",
515
+ "madd9",
516
+ "9",
517
+ "M9",
518
+ "m9",
519
+ "69",
520
+ "m69",
521
+ "5"
522
+ // power chord
523
+ ];
524
+ var QUALITIES = QUALITY_SOURCE.map(
525
+ (quality, priority) => ({
526
+ quality,
527
+ pitchClasses: parseChord(`C${quality}`).pitchClasses,
528
+ priority
529
+ })
530
+ );
531
+ var QUALITY_BY_PCSET = (() => {
532
+ const map = /* @__PURE__ */ new Map();
533
+ for (const def of QUALITIES) {
534
+ const key = def.pitchClasses.join(",");
535
+ if (!map.has(key)) map.set(key, def);
536
+ }
537
+ return map;
538
+ })();
539
+ var detectChord = (notes, options = {}) => {
540
+ if (!notes.length) return [];
541
+ const { flat = false } = options;
542
+ const pcs = [...new Set(notes.map(toPitchClass))].sort((a, b) => a - b);
543
+ const bass = toPitchClass(
544
+ options.bass ?? notes.reduce((m, v) => Math.min(m, v), notes[0])
545
+ );
546
+ const scored = [];
547
+ for (const root of pcs) {
548
+ const relSet = new Set(pcs.map((pc) => toPitchClass(pc - root)));
549
+ for (const def of QUALITY_BY_PCSET.values()) {
550
+ let matchCount = 0;
551
+ let hasRoot = false;
552
+ for (const pc of def.pitchClasses) {
553
+ if (relSet.has(pc)) {
554
+ matchCount++;
555
+ if (pc === 0) hasRoot = true;
556
+ }
557
+ }
558
+ if (!hasRoot) continue;
559
+ if (matchCount < Math.min(2, def.pitchClasses.length)) continue;
560
+ const missingCount = def.pitchClasses.length - matchCount;
561
+ let extraCount = 0;
562
+ const defSet = new Set(def.pitchClasses);
563
+ for (const pc of relSet) {
564
+ if (!defSet.has(pc)) {
565
+ extraCount++;
566
+ }
567
+ }
568
+ const score = matchCount * 2 - missingCount * 1 - extraCount * 0.5;
569
+ const rootSymbol = noteName(root, flat) + def.quality;
570
+ const inversion = root !== bass;
571
+ scored.push({
572
+ score,
573
+ priority: def.priority,
574
+ candidate: {
575
+ symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
576
+ rootSymbol,
577
+ root,
578
+ quality: def.quality,
579
+ bass,
580
+ inversion
581
+ }
582
+ });
583
+ }
584
+ }
585
+ scored.sort((a, b) => {
586
+ if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
587
+ if (a.candidate.inversion !== b.candidate.inversion)
588
+ return a.candidate.inversion ? 1 : -1;
589
+ if (a.priority !== b.priority) return a.priority - b.priority;
590
+ return a.candidate.root - b.candidate.root;
591
+ });
592
+ return scored.map((s) => s.candidate);
593
+ };
594
+ var toneRoleWeight = (rel) => {
595
+ if (rel === 0) return 1.3;
596
+ if (rel === 3 || rel === 4) return 1.2;
597
+ if (rel === 10 || rel === 11) return 0.95;
598
+ if (rel === 6 || rel === 7 || rel === 8) return 0.7;
599
+ return 0.85;
600
+ };
601
+ var CHORD_TEMPLATES = (() => {
602
+ const templates = [];
603
+ for (let root = 0; root < 12; root++) {
604
+ for (const def of QUALITIES) {
605
+ const pcs = /* @__PURE__ */ new Set();
606
+ const weights = new Array(12).fill(0);
607
+ const rel = /* @__PURE__ */ new Set();
608
+ for (const relPc of def.pitchClasses) {
609
+ rel.add(relPc);
610
+ const pc = toPitchClass(relPc + root);
611
+ pcs.add(pc);
612
+ weights[pc] = toneRoleWeight(relPc);
613
+ }
614
+ templates.push({
615
+ root,
616
+ quality: def.quality,
617
+ priority: def.priority,
618
+ pcs,
619
+ weights,
620
+ rel
621
+ });
622
+ }
623
+ }
624
+ return templates;
625
+ })();
626
+ var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
627
+ var parseChords = (str, bpm = 120) => {
628
+ const output = [];
629
+ const secBar = 60 / bpm * 4;
630
+ const frontChars = new Set("ABCDEFG_=%N");
631
+ let idx = 0;
632
+ let last = null;
633
+ for (const line of toHan(str).split("\n").map((v) => v.trim())) {
634
+ if (!line.length || /^#/.test(line)) continue;
635
+ for (const str2 of line.split(/[|ll→]/)) {
636
+ if (!str2.length) continue;
637
+ const when = idx++ * secBar;
638
+ const a = [];
639
+ for (let i = 0; i < str2.length; i++) {
640
+ const char = str2[i];
641
+ const prev = str2[i - 1];
642
+ const prev2 = str2.slice(i - 2, i);
643
+ if (!frontChars.has(char)) continue;
644
+ if (prev === "/" || prev2 === "on") continue;
645
+ if (prev2 === "N." && char === "C") continue;
646
+ a.push(i);
647
+ }
648
+ if (!a.length) continue;
649
+ const divide = 2 ** Math.ceil(Math.log2(a.length));
650
+ const unitTime = secBar / divide;
651
+ for (const [i, v] of a.entries()) {
652
+ const s = str2.slice(v, i === a.length - 1 ? str2.length : a[i + 1]).replace(/\s+/g, "");
653
+ const c = s[0];
654
+ if (c === "_" || c === "N") {
655
+ last = null;
656
+ continue;
657
+ }
658
+ if (c === "=") {
659
+ if (last) last.duration += unitTime;
660
+ continue;
661
+ }
662
+ const _when = when + i * unitTime;
663
+ if (c === "%") {
664
+ if (last === null) continue;
665
+ const base = last;
666
+ last = { ...base, when: _when, duration: unitTime };
667
+ } else {
668
+ const key = s.slice(0, s[1] === "#" ? 2 : 1);
669
+ const chord = s.slice(key.length).replace(/[\s・]/g, "");
670
+ last = {
671
+ key,
672
+ chord,
673
+ when: _when,
674
+ duration: unitTime
675
+ };
676
+ }
677
+ output.push(last);
678
+ }
679
+ if (last !== null && divide > a.length)
680
+ last.duration += unitTime * (divide - a.length);
681
+ }
682
+ }
683
+ return output;
684
+ };
685
+
162
686
  // src/chords.ts
163
687
  var C3 = 48;
164
688
  var buildChordPlacements = (options) => {
165
- const {
166
- chordStr,
167
- patternType,
168
- rootShift,
169
- bpm,
170
- stepsPerBar,
171
- parseChord,
172
- parseChords
173
- } = options;
689
+ const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
174
690
  const placements = [];
175
691
  if (!chordStr.trim()) return placements;
176
692
  const offset = rootShift;
@@ -200,7 +716,7 @@ var buildChordPlacements = (options) => {
200
716
  for (const chord of group) {
201
717
  let notes;
202
718
  try {
203
- notes = [...parseChord(`${chord.key}${chord.chord}`).value];
719
+ notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
204
720
  } catch {
205
721
  continue;
206
722
  }
@@ -287,7 +803,7 @@ var buildChordPlacements = (options) => {
287
803
  chordNames.forEach((chordName, barIndex) => {
288
804
  let notes;
289
805
  try {
290
- notes = [...parseChord(chordName).value];
806
+ notes = [...parseChord(chordName).notes];
291
807
  } catch {
292
808
  return;
293
809
  }
@@ -361,6 +877,7 @@ var buildUI = (target, options) => {
361
877
  <button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
362
878
  <button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
363
879
  <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
880
+ <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
364
881
  <span class="dtm-grow"></span>
365
882
  <span class="dtm-label">BPM</span>
366
883
  <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
@@ -453,10 +970,10 @@ var buildUI = (target, options) => {
453
970
  <div class="dtm-row dtm-hidden" data-dtm="midi-track-selection"></div>
454
971
  <div class="dtm-row">
455
972
  <span class="dtm-label">MML</span>
456
- <textarea class="dtm-textarea" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
973
+ <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
974
+ <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">\u8AAD\u8FBC</button>
457
975
  </div>
458
976
  <div class="dtm-row">
459
- <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">MML\u8AAD\u8FBC</button>
460
977
  <span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
461
978
  <select class="dtm-select" data-dtm="shift-select">
462
979
  <option value="-96">-2\u5206</option>
@@ -530,6 +1047,8 @@ var buildUI = (target, options) => {
530
1047
  const sel = (name) => q(root, `[data-dtm="${name}"]`);
531
1048
  return {
532
1049
  root,
1050
+ topbar: sel("transport"),
1051
+ topbarLoading: sel("topbar-loading"),
533
1052
  playBtn: sel("play"),
534
1053
  recBtn: sel("rec"),
535
1054
  soloCheckbox: sel("solo"),
@@ -1747,8 +2266,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
1747
2266
  ))().then((v) => {
1748
2267
  loaded.set(m, v);
1749
2268
  return v;
1750
- }).catch((err) => {
1751
- console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err);
2269
+ }).catch((err2) => {
2270
+ console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err2);
1752
2271
  return null;
1753
2272
  });
1754
2273
  loading.set(m, p);
@@ -3240,8 +3759,10 @@ var parseMML = (mml, options = {}) => {
3240
3759
  numStr += body[j];
3241
3760
  j++;
3242
3761
  }
3243
- if (ch === "t" && trackIndex === 0 && numStr) {
3244
- bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
3762
+ if (ch === "t" && numStr) {
3763
+ if (bpm === null) {
3764
+ bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
3765
+ }
3245
3766
  }
3246
3767
  pushTok("ctrl", currentStep, 0, tokStart);
3247
3768
  } else if (ch === "[") {
@@ -3743,6 +4264,7 @@ var DAW_CSS = `
3743
4264
  }
3744
4265
  .dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
3745
4266
  .dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
4267
+ .dtm-textarea.dtm-grow { width: 0; }
3746
4268
  .dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
3747
4269
 
3748
4270
  /* \u2500\u2500\u2500 \u30C8\u30E9\u30C3\u30AF\u30D4\u30EB\uFF08\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u9078\u629E\u30DC\u30BF\u30F3\uFF09 \u2500\u2500\u2500 */
@@ -3910,7 +4432,7 @@ var DAW_CSS = `
3910
4432
 
3911
4433
  /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
3912
4434
  .dtm-overlay {
3913
- position: absolute; inset: 0; z-index: 1000;
4435
+ position: absolute; inset: 0; z-index: 10;
3914
4436
  background: rgba(0,0,0,.92);
3915
4437
  display: flex; align-items: center; justify-content: center;
3916
4438
  flex-direction: column; gap: 14px;
@@ -3959,6 +4481,22 @@ var DAW_CSS = `
3959
4481
  letter-spacing: .15em;
3960
4482
  min-height: 1em;
3961
4483
  }
4484
+ .dtm-topbar-loading {
4485
+ display: none;
4486
+ font-family: var(--dtm-font);
4487
+ font-size: 11px;
4488
+ color: var(--dtm-primary);
4489
+ margin-left: 12px;
4490
+ letter-spacing: .15em;
4491
+ align-self: center;
4492
+ }
4493
+ .dtm-topbar.is-loading .dtm-topbar-loading {
4494
+ display: inline-block;
4495
+ }
4496
+ .dtm-topbar.is-loading {
4497
+ pointer-events: none;
4498
+ opacity: 0.7;
4499
+ }
3962
4500
 
3963
4501
  @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
3964
4502
  .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
@@ -4034,6 +4572,14 @@ var DAW_CSS = `
4034
4572
  min-width: 2em;
4035
4573
  margin-left: 4px;
4036
4574
  }
4575
+ .dtm-player-chord {
4576
+ font-family: 'k8x12', monospace;
4577
+ font-size: 11px;
4578
+ color: var(--dtm-accent);
4579
+ min-width: 4em;
4580
+ margin-left: 8px;
4581
+ font-weight: bold;
4582
+ }
4037
4583
  .dtm-player-dots {
4038
4584
  margin-left: auto;
4039
4585
  display: flex;
@@ -4384,7 +4930,7 @@ var mountDAW = (target, options = {}) => {
4384
4930
  const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
4385
4931
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
4386
4932
  const showMidi = !!options.parseMidi;
4387
- const showChord = !!(options.parseChord && options.parseChords);
4933
+ const showChord = true;
4388
4934
  const refs = buildUI(target, {
4389
4935
  tracks: trackConfigs,
4390
4936
  drumPatternNames: Object.keys(drumPatterns),
@@ -5080,13 +5626,15 @@ var mountDAW = (target, options = {}) => {
5080
5626
  const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
5081
5627
  if (streaming && voices) {
5082
5628
  const overlay = showLoadingOverlay(refs.rollContainer);
5629
+ setLoading(true);
5083
5630
  try {
5084
5631
  await voices.loadModels(streamTracks.map((t) => t.model));
5085
5632
  await voices.warm(streamTracks);
5086
- } catch (err) {
5087
- console.warn("[dtm] voice preload failed", err);
5633
+ } catch (err2) {
5634
+ console.warn("[dtm] voice preload failed", err2);
5088
5635
  } finally {
5089
5636
  overlay.remove();
5637
+ setLoading(false);
5090
5638
  }
5091
5639
  }
5092
5640
  if (playbackState !== "paused") {
@@ -5435,21 +5983,28 @@ var mountDAW = (target, options = {}) => {
5435
5983
  barLimit: barLimitBars
5436
5984
  };
5437
5985
  }
5438
- const trackLines = trackStates.map(
5439
- (t, i) => `@${i} ${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim()}`
5440
- );
5441
- const trackLinesMini = trackStates.map(
5442
- (t, i) => `@${i}${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim().replace(/\s+/g, "")}`
5443
- );
5986
+ const trackLines = [];
5987
+ const trackLinesMini = [];
5988
+ trackStates.forEach((t, i) => {
5989
+ const notes = clipNotes(t.core.getNotes());
5990
+ if (notes.length > 0) {
5991
+ const mml = t.core.getMMLFromNotes(notes, bpm, t.volume).trim();
5992
+ trackLines.push(`@${i} ${mml}`);
5993
+ trackLinesMini.push(`@${i}${mml.replace(/\s+/g, "")}`);
5994
+ }
5995
+ });
5444
5996
  const lyricLines = trackStates.map((t, i) => ({
5445
5997
  i,
5446
- text: t.lyrics.trim(),
5998
+ notes: clipNotes(t.core.getNotes()),
5999
+ text: t.lyrics.replace(/[\r\n]+/g, " ").trim(),
5447
6000
  model: t.lyricModel.trim(),
5448
6001
  vol: t.vocalVolume,
5449
6002
  gate: t.vocalGate,
5450
6003
  pan: t.vocalPan,
5451
6004
  oct: t.vocalOctave
5452
- })).filter((x) => x.model.length > 0 && x.text.length > 0).map((x) => {
6005
+ })).filter(
6006
+ (x) => x.model.length > 0 && x.text.length > 0 && x.notes.length > 0
6007
+ ).map((x) => {
5453
6008
  const params = [
5454
6009
  x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
5455
6010
  x.gate === 100 ? "" : `q${x.gate}`,
@@ -5465,7 +6020,7 @@ var mountDAW = (target, options = {}) => {
5465
6020
  full,
5466
6021
  minified,
5467
6022
  ignoredCount: 0,
5468
- trackCount: trackStates.length,
6023
+ trackCount: trackLines.length,
5469
6024
  barLimit: barLimitBars
5470
6025
  };
5471
6026
  };
@@ -5585,7 +6140,6 @@ var mountDAW = (target, options = {}) => {
5585
6140
  updateUndoRedo();
5586
6141
  };
5587
6142
  const applyChord = () => {
5588
- if (!options.parseChord || !options.parseChords) return;
5589
6143
  const active = getActive();
5590
6144
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
5591
6145
  if (!chordTrack) return;
@@ -5594,9 +6148,7 @@ var mountDAW = (target, options = {}) => {
5594
6148
  patternType: active.savedChordPattern,
5595
6149
  rootShift: active.savedChordRoot,
5596
6150
  bpm,
5597
- stepsPerBar: renderConfig.stepsPerBar,
5598
- parseChord: options.parseChord,
5599
- parseChords: options.parseChords
6151
+ stepsPerBar: renderConfig.stepsPerBar
5600
6152
  });
5601
6153
  chordTrack.core.clearNotesWithoutHistory();
5602
6154
  chordTrack.core.beginBatch();
@@ -5681,9 +6233,11 @@ var mountDAW = (target, options = {}) => {
5681
6233
  };
5682
6234
  const overlayDuring = (fn) => {
5683
6235
  refs.overlay.hidden = false;
6236
+ setLoading(true);
5684
6237
  setTimeout(() => {
5685
6238
  fn();
5686
6239
  refs.overlay.hidden = true;
6240
+ setLoading(false);
5687
6241
  }, 30);
5688
6242
  };
5689
6243
  const wireEvents = () => {
@@ -5827,6 +6381,7 @@ var mountDAW = (target, options = {}) => {
5827
6381
  const file = refs.midiInput.files?.[0];
5828
6382
  if (!file || !options.parseMidi) return;
5829
6383
  refs.overlay.hidden = false;
6384
+ setLoading(true);
5830
6385
  const buffer = new Uint8Array(await file.arrayBuffer());
5831
6386
  pendingMidi = options.parseMidi(buffer);
5832
6387
  detectedTracks = analyzeMidiTracks(pendingMidi);
@@ -5847,6 +6402,7 @@ var mountDAW = (target, options = {}) => {
5847
6402
  });
5848
6403
  refs.midiTrackSelection.classList.remove("dtm-hidden");
5849
6404
  refs.overlay.hidden = true;
6405
+ setLoading(false);
5850
6406
  });
5851
6407
  refs.midiLoadBtn.addEventListener("click", () => {
5852
6408
  if (!pendingMidi) return;
@@ -5920,6 +6476,9 @@ var mountDAW = (target, options = {}) => {
5920
6476
  resizeObserver.observe(refs.rollContainer);
5921
6477
  document.addEventListener("pointermove", onPointerMove);
5922
6478
  document.addEventListener("pointerup", onPointerUp);
6479
+ const setLoading = (loading) => {
6480
+ refs.topbar.classList.toggle("is-loading", loading);
6481
+ };
5923
6482
  return {
5924
6483
  play,
5925
6484
  pause,
@@ -5957,6 +6516,7 @@ var mountDAW = (target, options = {}) => {
5957
6516
  exportMIDI: exportMIDI2,
5958
6517
  setBpm,
5959
6518
  getPlaybackState: () => playbackState,
6519
+ setLoading,
5960
6520
  destroy: () => {
5961
6521
  sequencer.stop();
5962
6522
  resizeObserver.disconnect();
@@ -6101,6 +6661,117 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6101
6661
  const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
6102
6662
  (a, b) => a - b
6103
6663
  );
6664
+ const trackStats = trackIndices.map((index) => {
6665
+ const trackPlacements = placements.filter((p) => p.trackIndex === index);
6666
+ if (trackPlacements.length === 0) {
6667
+ return { index, isChords: false, avgPitch: 0, noteCount: 0 };
6668
+ }
6669
+ const sumPitch = trackPlacements.reduce((sum, p) => sum + p.pitch, 0);
6670
+ const avgPitch = sumPitch / trackPlacements.length;
6671
+ const stepCounts = /* @__PURE__ */ new Map();
6672
+ for (const p of trackPlacements) {
6673
+ for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
6674
+ stepCounts.set(s, (stepCounts.get(s) ?? 0) + 1);
6675
+ }
6676
+ }
6677
+ const polyphonicSteps = Array.from(stepCounts.values()).filter(
6678
+ (c) => c >= 2
6679
+ ).length;
6680
+ const isChords = polyphonicSteps > 0;
6681
+ return {
6682
+ index,
6683
+ isChords,
6684
+ avgPitch,
6685
+ noteCount: trackPlacements.length
6686
+ };
6687
+ });
6688
+ const chordTrackIndices = trackStats.filter((s) => s.isChords).map((s) => s.index);
6689
+ const nonChordTracks = trackStats.filter(
6690
+ (s) => !s.isChords && s.noteCount > 0
6691
+ );
6692
+ let bassTrackIndex = null;
6693
+ if (nonChordTracks.length > 0) {
6694
+ nonChordTracks.sort((a, b) => a.avgPitch - b.avgPitch);
6695
+ bassTrackIndex = nonChordTracks[0].index;
6696
+ }
6697
+ const priorityTrackIndices = /* @__PURE__ */ new Set();
6698
+ for (const idx of chordTrackIndices) {
6699
+ priorityTrackIndices.add(idx);
6700
+ }
6701
+ if (bassTrackIndex !== null) {
6702
+ priorityTrackIndices.add(bassTrackIndex);
6703
+ }
6704
+ const hasPriorityTracksInMml = priorityTrackIndices.size > 0;
6705
+ const maxStep = placements.reduce(
6706
+ (max, p) => Math.max(max, p.startStep + p.durationSteps),
6707
+ 0
6708
+ );
6709
+ const priorityPitches = Array.from(
6710
+ { length: maxStep + 1 },
6711
+ () => /* @__PURE__ */ new Set()
6712
+ );
6713
+ const allPitches = Array.from(
6714
+ { length: maxStep + 1 },
6715
+ () => /* @__PURE__ */ new Set()
6716
+ );
6717
+ for (const p of placements) {
6718
+ for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
6719
+ if (s >= 0 && s <= maxStep) {
6720
+ allPitches[s].add(p.pitch);
6721
+ if (priorityTrackIndices.has(p.trackIndex)) {
6722
+ priorityPitches[s].add(p.pitch);
6723
+ }
6724
+ }
6725
+ }
6726
+ }
6727
+ const stepChords = [];
6728
+ const chordCache = /* @__PURE__ */ new Map();
6729
+ let lastChord = "";
6730
+ let hasPriorityStarted = false;
6731
+ const GRID_SIZE = 48;
6732
+ const MIN_DURATION = 12;
6733
+ for (let g = 0; g <= Math.ceil(maxStep / GRID_SIZE); g++) {
6734
+ const startS = g * GRID_SIZE;
6735
+ const endS = Math.min(maxStep, (g + 1) * GRID_SIZE - 1);
6736
+ if (startS > maxStep) break;
6737
+ for (let s = startS; s <= endS; s++) {
6738
+ const priorityPitchesAtStep = Array.from(priorityPitches[s]);
6739
+ if (priorityPitchesAtStep.length > 0) {
6740
+ hasPriorityStarted = true;
6741
+ }
6742
+ }
6743
+ const pitchDurations = /* @__PURE__ */ new Map();
6744
+ for (let s = startS; s <= endS; s++) {
6745
+ const usePriority = hasPriorityStarted && hasPriorityTracksInMml;
6746
+ const pitches = usePriority ? Array.from(priorityPitches[s]) : Array.from(allPitches[s]);
6747
+ for (const p of pitches) {
6748
+ pitchDurations.set(p, (pitchDurations.get(p) ?? 0) + 1);
6749
+ }
6750
+ }
6751
+ let activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur >= MIN_DURATION).map(([p, _]) => p);
6752
+ if (activePitches.length === 0 && pitchDurations.size > 0) {
6753
+ const maxDur = Math.max(...pitchDurations.values());
6754
+ activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur === maxDur).map(([p, _]) => p);
6755
+ }
6756
+ let gridChord = lastChord;
6757
+ if (activePitches.length > 0) {
6758
+ const sortedPitches = activePitches.sort((a, b) => a - b);
6759
+ const cacheKey = sortedPitches.join(",");
6760
+ if (chordCache.has(cacheKey)) {
6761
+ const chordName = chordCache.get(cacheKey);
6762
+ if (chordName) gridChord = chordName;
6763
+ } else {
6764
+ const candidates = detectChord(sortedPitches);
6765
+ const chordName = candidates[0]?.symbol ?? "";
6766
+ if (chordName) gridChord = chordName;
6767
+ chordCache.set(cacheKey, chordName);
6768
+ }
6769
+ }
6770
+ for (let s = startS; s <= endS; s++) {
6771
+ stepChords[s] = gridChord;
6772
+ }
6773
+ lastChord = gridChord;
6774
+ }
6104
6775
  const seqTracks = trackIndices.map((index) => {
6105
6776
  let id = 0;
6106
6777
  const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
@@ -6301,6 +6972,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6301
6972
  barEl.className = "dtm-player-bar";
6302
6973
  barEl.textContent = "-";
6303
6974
  beatRow.appendChild(barEl);
6975
+ const chordEl = doc.createElement("span");
6976
+ chordEl.className = "dtm-player-chord";
6977
+ chordEl.textContent = "";
6978
+ beatRow.appendChild(chordEl);
6304
6979
  const chips = [];
6305
6980
  const makeChip = (label) => {
6306
6981
  const chip = doc.createElement("span");
@@ -6444,10 +7119,20 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6444
7119
  lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
6445
7120
  };
6446
7121
  const renderPlayhead = (step) => {
7122
+ const intStep = Math.floor(step);
6447
7123
  const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
6448
7124
  for (let i = 0; i < 4; i++)
6449
7125
  beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
6450
7126
  barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
7127
+ const chordName = stepChords[intStep] ?? "";
7128
+ if (chordEl.textContent !== chordName) {
7129
+ chordEl.textContent = chordName;
7130
+ if (chordName) {
7131
+ console.log(
7132
+ `[dtm-player-chord] Active Chord: ${chordName} (step: ${intStep})`
7133
+ );
7134
+ }
7135
+ }
6451
7136
  for (const view of laneViews) {
6452
7137
  let active = null;
6453
7138
  for (const t of view.tokens) {
@@ -6461,6 +7146,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6461
7146
  const resetPlayhead = () => {
6462
7147
  for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
6463
7148
  barEl.textContent = "-";
7149
+ chordEl.textContent = "";
6464
7150
  for (const view of laneViews) {
6465
7151
  for (const t of view.tokens) t.el.classList.remove("is-active");
6466
7152
  view.lane.scrollLeft = 0;
@@ -6524,6 +7210,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6524
7210
  });
6525
7211
  }
6526
7212
  return {
7213
+ id: String(index),
6527
7214
  model: lt.model,
6528
7215
  volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
6529
7216
  pan: panToStereo(lt.pan ?? DEFAULT_PAN),
@@ -6539,15 +7226,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6539
7226
  try {
6540
7227
  await v.loadModels(tracks.map((t) => t.model));
6541
7228
  await v.warm(tracks);
6542
- } catch (err) {
6543
- console.warn("[dtm] voice preload failed", err);
7229
+ } catch (err2) {
7230
+ console.warn("[dtm] voice preload failed", err2);
6544
7231
  } finally {
6545
7232
  overlay.remove();
6546
7233
  }
6547
7234
  if (!playing || activePlayer !== instance) return;
6548
7235
  }
6549
7236
  seq.start(0);
6550
- if (streaming) ensureVoices().startStream(tracks, seq.getStartTime());
7237
+ if (streaming) {
7238
+ ensureVoices().startStream(tracks, seq.getStartTime(), {
7239
+ isAudible: (t) => !mutedTracks.has(Number(t.id))
7240
+ });
7241
+ }
6551
7242
  };
6552
7243
  const play = () => {
6553
7244
  if (playing || trackIndices.length === 0) return;
@@ -6882,8 +7573,6 @@ var DEFAULT_CDN = {
6882
7573
  soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
6883
7574
  soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
6884
7575
  soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs",
6885
- parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs",
6886
- parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs",
6887
7576
  midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
6888
7577
  };
6889
7578
  var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
@@ -6927,24 +7616,6 @@ var createDtmStudio = async (options = {}) => {
6927
7616
  eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
6928
7617
  eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
6929
7618
  ]);
6930
- let parseChord = eng.parseChord;
6931
- let parseChords = eng.parseChords;
6932
- if (features.chord && (!parseChord || !parseChords)) {
6933
- try {
6934
- [parseChord, parseChords] = await Promise.all([
6935
- parseChord ?? importFrom(
6936
- cdn.parseChord,
6937
- "parseChord"
6938
- ),
6939
- parseChords ?? importFrom(
6940
- cdn.parseChords,
6941
- "parseChords"
6942
- )
6943
- ]);
6944
- } catch (e) {
6945
- console.warn("[dtm] \u30B3\u30FC\u30C9\u89E3\u6790\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F", e);
6946
- }
6947
- }
6948
7619
  let midiParser = null;
6949
7620
  let parseMidi;
6950
7621
  if (features.midi) {
@@ -7130,8 +7801,6 @@ var createDtmStudio = async (options = {}) => {
7130
7801
  onPlayNote: playNote,
7131
7802
  onPlayDrum: playDrum,
7132
7803
  singingVoices,
7133
- parseChord,
7134
- parseChords,
7135
7804
  parseMidi,
7136
7805
  onToggleRecord,
7137
7806
  ...dawOverrides
@@ -7153,15 +7822,32 @@ var createDtmStudio = async (options = {}) => {
7153
7822
  editorPresetSelects.set(target, select);
7154
7823
  select.addEventListener("change", async () => {
7155
7824
  if (!select) return;
7156
- daw.setInstrument(select.value);
7157
- await loadPreset(select.value, trackIds);
7825
+ const wasPlaying = daw.getPlaybackState() === "playing";
7826
+ if (wasPlaying) {
7827
+ daw.pause();
7828
+ }
7829
+ const overlay = showLoadingOverlay(target);
7830
+ daw.setLoading?.(true);
7831
+ try {
7832
+ daw.setInstrument(select.value);
7833
+ await loadPreset(select.value, trackIds);
7834
+ } finally {
7835
+ overlay.remove();
7836
+ daw.setLoading?.(false);
7837
+ if (wasPlaying) {
7838
+ daw.play();
7839
+ }
7840
+ }
7158
7841
  });
7159
7842
  }
7160
7843
  const daw = mountDAW(target, base);
7161
7844
  mountedEditors.push(daw);
7162
7845
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7163
7846
  daw.setInstrument(presetKey);
7164
- void loadPreset(presetKey, trackIds);
7847
+ daw.setLoading?.(true);
7848
+ void loadPreset(presetKey, trackIds).finally(() => {
7849
+ daw.setLoading?.(false);
7850
+ });
7165
7851
  const destroy = () => {
7166
7852
  daw.destroy();
7167
7853
  select?.remove();