@onjmin/dtm 0.1.8 → 0.1.10

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
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  LinkedList: () => LinkedList,
40
40
  MAX_VOCAL_VOLUME: () => MAX_VOCAL_VOLUME,
41
41
  MMLCore: () => MMLCore,
42
+ MML_END_MARKER: () => MML_END_MARKER,
42
43
  PITCH_MAP: () => PITCH_MAP,
43
44
  PREWARM_NOTES: () => PREWARM_NOTES,
44
45
  TRACKS_ADVANCED: () => TRACKS_ADVANCED,
@@ -159,18 +160,889 @@ async function buildNameToKeyMapping() {
159
160
  return nameToKey;
160
161
  }
161
162
 
162
- // src/chords.ts
163
- var C3 = 48;
164
- var buildChordPlacements = (options) => {
163
+ // node_modules/.pnpm/@onjmin+chord-parser@1.0.2/node_modules/@onjmin/chord-parser/dist/index.mjs
164
+ var SHARP_NAMES = [
165
+ "C",
166
+ "C#",
167
+ "D",
168
+ "D#",
169
+ "E",
170
+ "F",
171
+ "F#",
172
+ "G",
173
+ "G#",
174
+ "A",
175
+ "A#",
176
+ "B"
177
+ ];
178
+ var FLAT_NAMES = [
179
+ "C",
180
+ "Db",
181
+ "D",
182
+ "Eb",
183
+ "E",
184
+ "F",
185
+ "Gb",
186
+ "G",
187
+ "Ab",
188
+ "A",
189
+ "Bb",
190
+ "B"
191
+ ];
192
+ var toPitchClass = (n) => (n % 12 + 12) % 12;
193
+ var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
194
+ var SyntaxErrorWithPos = class extends Error {
195
+ constructor(input, msg) {
196
+ super(
197
+ `SyntaxError: ${msg}
198
+ input.idx: ${input.idx}
199
+ input.str: ${input.str}`
200
+ );
201
+ this.name = "ChordSyntaxError";
202
+ }
203
+ };
204
+ var err = (input, msg) => {
205
+ throw new SyntaxErrorWithPos(input, msg);
206
+ };
207
+ var Input = class _Input {
208
+ static nums = new Set("0123456789");
209
+ str;
210
+ nest;
211
+ idx;
212
+ constructor(str, nest = 0) {
213
+ this.str = str;
214
+ this.nest = nest;
215
+ this.idx = 0;
216
+ }
217
+ get isEOF() {
218
+ return this.str.length <= this.idx;
219
+ }
220
+ get char() {
221
+ return this.str[this.idx];
222
+ }
223
+ /** 先頭の連続する数字を消費して数値で返す(破壊的)。数字が無ければ null。 */
224
+ get num() {
225
+ let str = "";
226
+ while (!this.isEOF) {
227
+ const char = this.char;
228
+ if (!_Input.nums.has(char)) break;
229
+ str += char;
230
+ this.idx++;
231
+ }
232
+ return str.length ? Number(str) : null;
233
+ }
234
+ slice(i) {
235
+ return this.str.slice(this.idx, this.idx + i);
236
+ }
237
+ };
238
+ var Output = class {
239
+ pitch = null;
240
+ chord = null;
241
+ isChord = false;
242
+ pending = null;
243
+ nest = -1;
244
+ get value() {
245
+ const { pitch, chord } = this;
246
+ return new Set(
247
+ [...chord].map((v) => v + pitch)
248
+ );
249
+ }
250
+ set value(chord) {
251
+ const pitch = this.pitch;
252
+ this.chord = new Set([...chord].map((v) => v - pitch));
253
+ }
254
+ };
255
+ var Matcher = class {
256
+ map = /* @__PURE__ */ new Map();
257
+ /** キー長を降順で保持(最長一致のため)。 */
258
+ lengths = [];
259
+ _set(key, value) {
260
+ this.map.set(key, value);
261
+ if (!this.lengths.includes(key.length)) {
262
+ this.lengths.push(key.length);
263
+ this.lengths.sort((a, b) => b - a);
264
+ }
265
+ }
266
+ set(key, value) {
267
+ if (Array.isArray(key)) for (const k of key) this._set(k, value);
268
+ else this._set(key, value);
269
+ }
270
+ parse(input) {
271
+ for (const i of this.lengths) {
272
+ const s = input.slice(i);
273
+ if (this.map.has(s)) {
274
+ input.idx += s.length;
275
+ return this.map.get(s);
276
+ }
277
+ }
278
+ return null;
279
+ }
280
+ };
281
+ var BRACKET_START = 0;
282
+ var BRACKET_END = 1;
283
+ var COMMA = 2;
284
+ var DIVIDE = 3;
285
+ var formulaMatcher = new Matcher();
286
+ formulaMatcher.set("(", BRACKET_START);
287
+ formulaMatcher.set(")", BRACKET_END);
288
+ formulaMatcher.set(",", COMMA);
289
+ formulaMatcher.set(["/", "on"], DIVIDE);
290
+ var parseFormula = (input, output = new Output(), nest = 0) => {
291
+ let start = input.idx;
292
+ const _eval = (idx) => {
293
+ const str = input.str.slice(start, idx);
294
+ if (str.length) parseTerm(new Input(str, nest), output);
295
+ };
296
+ while (true) {
297
+ const { idx } = input;
298
+ if (input.isEOF) {
299
+ if (nest) err(input, `Unclosed ${nest} brackets`);
300
+ _eval(idx);
301
+ return output;
302
+ }
303
+ const res = formulaMatcher.parse(input);
304
+ if (res === null) {
305
+ input.idx++;
306
+ continue;
307
+ }
308
+ const { pending } = output;
309
+ _eval(idx);
310
+ switch (res) {
311
+ case BRACKET_START:
312
+ parseFormula(input, output, nest + 1);
313
+ break;
314
+ case BRACKET_END:
315
+ if (nest - 1 < 0) err(input, "Unable to close brackets");
316
+ return output;
317
+ case COMMA:
318
+ output.pending = pending;
319
+ break;
320
+ case DIVIDE: {
321
+ const o = parseFormula(input, new Output(), nest);
322
+ const v = [...output.value];
323
+ if (o.isChord) {
324
+ output.value = [...o.value].concat(v);
325
+ } else {
326
+ const a = v.sort((x, y) => x - y);
327
+ const pitch = (o.pitch + 3) % 12 - 3;
328
+ if (a[0] < pitch) {
329
+ while (a[0] < pitch) a.push(a.shift() + 12);
330
+ } else {
331
+ while (true) {
332
+ const w = a[a.length - 1] - 12;
333
+ if (w < pitch) break;
334
+ a.pop();
335
+ a.unshift(w);
336
+ }
337
+ }
338
+ a.push(pitch);
339
+ output.value = a;
340
+ }
341
+ break;
342
+ }
343
+ }
344
+ start = input.idx;
345
+ }
346
+ };
347
+ var parseTerm = (input, output) => {
348
+ if (input.isEOF) return output;
349
+ if (output.pitch === null) return parsePitch(input, output);
350
+ if (output.pending === null) return parseFunc(input, output);
351
+ return parsePending(input, output);
352
+ };
353
+ var halfMatcher = new Matcher();
354
+ var halfMatcherStrict = new Matcher();
355
+ for (const m of [halfMatcher, halfMatcherStrict]) {
356
+ m.set(["#", "\u266F"], 1);
357
+ m.set(["b", "\u266D"], -1);
358
+ }
359
+ halfMatcher.set("+", 1);
360
+ halfMatcher.set("-", -1);
361
+ var parseHalf = (input, isPitch = false) => (isPitch ? halfMatcherStrict : halfMatcher).parse(input);
362
+ var idx2pitch = [0, 2, 4, 5, 7, 9, 11];
363
+ for (const i of [...idx2pitch.keys()]) idx2pitch.push(idx2pitch[i] + 12);
364
+ var deg2pitch = (deg) => idx2pitch[deg - 1];
365
+ var pitchMatcher = new Matcher();
366
+ for (const [i, v] of [..."CDEFGAB"].entries())
367
+ pitchMatcher.set(v, idx2pitch[i]);
368
+ var parsePitch = (input, output) => {
369
+ const pitch = pitchMatcher.parse(input);
370
+ if (pitch === null) err(input, "Not found pitch");
371
+ output.pitch = pitch;
372
+ const half = parseHalf(input, true);
373
+ if (half !== null) output.pitch += half;
374
+ return parseBase(input, output);
375
+ };
376
+ var MAJOR = [0, 4, 7];
377
+ var DIM = [0, 3, 6];
378
+ var baseMatcher = new Matcher();
379
+ baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
380
+ baseMatcher.set(["dim", "\u3007"], DIM);
381
+ baseMatcher.set("+", [0, 4, 8]);
382
+ baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
383
+ var parseBase = (input, output) => {
384
+ const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
385
+ const res = isMajMarker ? null : baseMatcher.parse(input);
386
+ if (res !== null) output.isChord = true;
387
+ output.chord = new Set(res || MAJOR);
388
+ if (res === DIM) {
389
+ const { num } = input;
390
+ const chord = output.chord;
391
+ if (num !== null) chord.add(deg2pitch(num) - 2);
392
+ }
393
+ output.nest = input.nest;
394
+ return parseTerm(input, output);
395
+ };
396
+ var add = (chord, n, half) => {
397
+ chord.add(deg2pitch(n) + half);
398
+ };
399
+ var aug = (chord) => {
400
+ chord.delete(deg2pitch(5));
401
+ chord.add(deg2pitch(5) + 1);
402
+ };
403
+ var _7th = (chord, n, _half2, isFlat = false) => {
404
+ if (n === 5) chord.delete(deg2pitch(3));
405
+ else if (n === 6) chord.add(deg2pitch(6));
406
+ else if (n === 69) chord.add(deg2pitch(6)).add(deg2pitch(9));
407
+ else {
408
+ if (n >= 7) chord.add(deg2pitch(7) + (isFlat ? -1 : 0));
409
+ if (n >= 9) chord.add(deg2pitch(9));
410
+ if (n >= 11) chord.add(deg2pitch(11));
411
+ if (n >= 13) chord.add(deg2pitch(13));
412
+ }
413
+ };
414
+ var _half = (chord, n, half) => {
415
+ chord.delete(deg2pitch(n));
416
+ chord.add(deg2pitch(n) + half);
417
+ };
418
+ var funcMatcher = new Matcher();
419
+ funcMatcher.set("add", add);
420
+ funcMatcher.set(["omit", "no"], (chord, n, half) => {
421
+ chord.delete(deg2pitch(n) + half);
422
+ });
423
+ funcMatcher.set("sus", (chord, n, half) => {
424
+ chord.delete(deg2pitch(3));
425
+ chord.add(deg2pitch(n) + half);
426
+ });
427
+ funcMatcher.set(
428
+ ["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
429
+ _7th
430
+ );
431
+ funcMatcher.set("aug", aug);
432
+ var parseFunc = (input, output) => {
433
+ if (!output.isChord) output.isChord = true;
434
+ const func = funcMatcher.parse(input);
435
+ const chord = output.chord;
436
+ if (func === null) {
437
+ const isAug = input.char === "+";
438
+ const half = parseHalf(input);
439
+ const { num } = input;
440
+ if (num === null) {
441
+ if (isAug) aug(chord);
442
+ else err(input, "Not found number");
443
+ }
444
+ if (half === null) {
445
+ if (input.nest === output.nest) _7th(chord, num, 0, true);
446
+ else add(chord, num, 0);
447
+ } else {
448
+ _half(chord, num, half);
449
+ }
450
+ } else if (func === aug) {
451
+ aug(chord);
452
+ } else {
453
+ output.pending = func;
454
+ }
455
+ return parseTerm(input, output);
456
+ };
457
+ var parsePending = (input, output) => {
458
+ const half = parseHalf(input);
459
+ const { num } = input;
460
+ const { pending, chord } = output;
461
+ if (num === null) err(input, "Not found number");
462
+ pending(
463
+ chord,
464
+ num,
465
+ half === null ? 0 : half
466
+ );
467
+ output.pending = null;
468
+ return parseTerm(input, output);
469
+ };
470
+ var parseChord = (symbol) => {
471
+ const output = parseFormula(new Input(symbol));
472
+ const notes = [...output.value].sort((a, b) => a - b);
473
+ const intervals = [...output.chord].sort((a, b) => a - b);
474
+ const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
475
+ (a, b) => a - b
476
+ );
477
+ return {
478
+ symbol,
479
+ root: toPitchClass(output.pitch),
480
+ notes,
481
+ pitchClasses,
482
+ intervals
483
+ };
484
+ };
485
+ var QUALITY_SOURCE = [
486
+ "",
487
+ // major
488
+ "m",
489
+ // minor
490
+ "7",
491
+ // dominant 7th
492
+ "M7",
493
+ // major 7th
494
+ "m7",
495
+ // minor 7th
496
+ "dim",
497
+ // diminished triad
498
+ "m7b5",
499
+ // half-diminished
500
+ "aug",
501
+ // augmented triad
502
+ "6",
503
+ // major 6th
504
+ "m6",
505
+ // minor 6th
506
+ "sus4",
507
+ "sus2",
508
+ "mM7",
509
+ // minor major 7th
510
+ "dim7",
511
+ // diminished 7th
512
+ "7sus4",
513
+ "7#5",
514
+ // augmented 7th
515
+ "add9",
516
+ "madd9",
517
+ "9",
518
+ "M9",
519
+ "m9",
520
+ "69",
521
+ "m69",
522
+ "5"
523
+ // power chord
524
+ ];
525
+ var QUALITIES = QUALITY_SOURCE.map(
526
+ (quality, priority) => ({
527
+ quality,
528
+ pitchClasses: parseChord(`C${quality}`).pitchClasses,
529
+ priority
530
+ })
531
+ );
532
+ var QUALITY_BY_PCSET = (() => {
533
+ const map = /* @__PURE__ */ new Map();
534
+ for (const def of QUALITIES) {
535
+ const key = def.pitchClasses.join(",");
536
+ if (!map.has(key)) map.set(key, def);
537
+ }
538
+ return map;
539
+ })();
540
+ var MAJOR_PROFILE = [
541
+ 6.35,
542
+ 2.23,
543
+ 3.48,
544
+ 2.33,
545
+ 4.38,
546
+ 4.09,
547
+ 2.52,
548
+ 5.19,
549
+ 2.39,
550
+ 3.66,
551
+ 2.29,
552
+ 2.88
553
+ ];
554
+ var MINOR_PROFILE = [
555
+ 6.33,
556
+ 2.68,
557
+ 3.52,
558
+ 5.38,
559
+ 2.6,
560
+ 3.53,
561
+ 2.54,
562
+ 4.75,
563
+ 3.98,
564
+ 2.69,
565
+ 3.34,
566
+ 3.17
567
+ ];
568
+ var mean = (a) => a.reduce((s, v) => s + v, 0) / a.length;
569
+ var pearson = (a, b) => {
570
+ const ma = mean(a);
571
+ const mb = mean(b);
572
+ let num = 0;
573
+ let da = 0;
574
+ let db = 0;
575
+ for (let i = 0; i < a.length; i++) {
576
+ const x = a[i] - ma;
577
+ const y = b[i] - mb;
578
+ num += x * y;
579
+ da += x * x;
580
+ db += y * y;
581
+ }
582
+ const den = Math.sqrt(da * db);
583
+ return den === 0 ? 0 : num / den;
584
+ };
585
+ var keyName = (tonic, mode, flat) => `${noteName(tonic, flat)} ${mode}`;
586
+ var stripScore = (c) => ({
587
+ tonic: c.tonic,
588
+ mode: c.mode,
589
+ name: c.name
590
+ });
591
+ var sameKey = (a, b) => a.tonic === b.tonic && a.mode === b.mode;
592
+ var buildHistogram = (notes) => {
593
+ const h = new Array(12).fill(0);
594
+ for (const n of notes) {
595
+ if (typeof n === "number") h[toPitchClass(n)] += 1;
596
+ else h[toPitchClass(n.pitch)] += n.duration ?? 1;
597
+ }
598
+ return h;
599
+ };
600
+ var windowHistogram = (notes, start, end) => {
601
+ const h = new Array(12).fill(0);
602
+ for (const n of notes) {
603
+ if (n.duration <= 0) {
604
+ if (n.when >= start && n.when < end) h[toPitchClass(n.pitch)] += 1;
605
+ continue;
606
+ }
607
+ const s = Math.max(n.when, start);
608
+ const e = Math.min(n.when + n.duration, end);
609
+ const overlap = e - s;
610
+ if (overlap > 0) h[toPitchClass(n.pitch)] += overlap;
611
+ }
612
+ return h;
613
+ };
614
+ var rankKeys = (histogram, flat) => {
615
+ const candidates = [];
616
+ for (let tonic = 0; tonic < 12; tonic++) {
617
+ for (const mode of ["major", "minor"]) {
618
+ const profile = mode === "major" ? MAJOR_PROFILE : MINOR_PROFILE;
619
+ const rotated = histogram.map(
620
+ (_, pc) => profile[toPitchClass(pc - tonic)]
621
+ );
622
+ candidates.push({
623
+ tonic,
624
+ mode,
625
+ name: keyName(tonic, mode, flat),
626
+ score: pearson(histogram, rotated)
627
+ });
628
+ }
629
+ }
630
+ candidates.sort((a, b) => b.score - a.score);
631
+ return candidates;
632
+ };
633
+ var detectKey = (notes, options = {}) => {
634
+ if (!notes.length) return [];
635
+ const { flat = false } = options;
636
+ const histogram = buildHistogram(notes);
637
+ if (histogram.every((v) => v === 0)) return [];
638
+ return rankKeys(histogram, flat);
639
+ };
640
+ var coalesce = (segments) => {
641
+ const out = [];
642
+ for (const s of segments) {
643
+ const last = out[out.length - 1];
644
+ if (last && sameKey(last.key, s.key)) {
645
+ last.duration = s.when + s.duration - last.when;
646
+ } else {
647
+ out.push({ ...s });
648
+ }
649
+ }
650
+ return out;
651
+ };
652
+ var mergeShortSegments = (segments, min) => {
653
+ if (min <= 0) return segments;
654
+ const result = segments.map((s) => ({ ...s }));
655
+ let i = 0;
656
+ while (i < result.length && result.length > 1) {
657
+ if (result[i].duration >= min) {
658
+ i++;
659
+ continue;
660
+ }
661
+ if (i > 0) {
662
+ result[i - 1].duration += result[i].duration;
663
+ result.splice(i, 1);
664
+ } else {
665
+ result[i + 1].when = result[i].when;
666
+ result[i + 1].duration += result[i].duration;
667
+ result.splice(i, 1);
668
+ }
669
+ }
670
+ return coalesce(result);
671
+ };
672
+ var detectKeyChanges = (notes, options = {}) => {
673
+ if (!notes.length) return [];
674
+ const { flat = false } = options;
675
+ const start = notes.reduce(
676
+ (m, n) => Math.min(m, n.when),
677
+ Number.POSITIVE_INFINITY
678
+ );
679
+ const end = notes.reduce(
680
+ (m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
681
+ Number.NEGATIVE_INFINITY
682
+ );
683
+ const span = end - start;
684
+ if (span <= 0) {
685
+ const top = detectKey(
686
+ notes.map((n) => ({ pitch: n.pitch, duration: Math.max(n.duration, 1) })),
687
+ { flat }
688
+ )[0];
689
+ return top ? [{ key: stripScore(top), when: start, duration: 0 }] : [];
690
+ }
691
+ const windowSize = options.windowSize ?? span / 4;
692
+ const hopSize = options.hopSize ?? windowSize / 2;
693
+ const minSegmentDuration = options.minSegmentDuration ?? 0;
694
+ const switchMargin = options.switchMargin ?? 0.08;
695
+ const segments = [];
696
+ for (let t = start; t < end - 1e-9; t += hopSize) {
697
+ const regionEnd = Math.min(t + hopSize, end);
698
+ const winEnd = Math.min(t + windowSize, end);
699
+ const winStart = Math.max(start, winEnd - windowSize);
700
+ const histogram = windowHistogram(notes, winStart, winEnd);
701
+ const last = segments[segments.length - 1];
702
+ if (histogram.every((v) => v === 0)) {
703
+ if (last) last.duration = regionEnd - last.when;
704
+ continue;
705
+ }
706
+ const candidates = rankKeys(histogram, flat);
707
+ let chosen = candidates[0];
708
+ if (last) {
709
+ const current = candidates.find((c) => sameKey(c, last.key));
710
+ if (current && chosen.score - current.score <= switchMargin)
711
+ chosen = current;
712
+ }
713
+ if (last && sameKey(last.key, chosen)) {
714
+ last.duration = regionEnd - last.when;
715
+ } else {
716
+ segments.push({
717
+ key: stripScore(chosen),
718
+ when: t,
719
+ duration: regionEnd - t
720
+ });
721
+ }
722
+ }
723
+ return mergeShortSegments(coalesce(segments), minSegmentDuration);
724
+ };
725
+ var toneRoleWeight = (rel) => {
726
+ if (rel === 0) return 1.3;
727
+ if (rel === 3 || rel === 4) return 1.2;
728
+ if (rel === 10 || rel === 11) return 0.95;
729
+ if (rel === 6 || rel === 7 || rel === 8) return 0.7;
730
+ return 0.85;
731
+ };
732
+ var CHORD_TEMPLATES = (() => {
733
+ const templates = [];
734
+ for (let root = 0; root < 12; root++) {
735
+ for (const def of QUALITIES) {
736
+ const pcs = /* @__PURE__ */ new Set();
737
+ const weights = new Array(12).fill(0);
738
+ const rel = /* @__PURE__ */ new Set();
739
+ for (const relPc of def.pitchClasses) {
740
+ rel.add(relPc);
741
+ const pc = toPitchClass(relPc + root);
742
+ pcs.add(pc);
743
+ weights[pc] = toneRoleWeight(relPc);
744
+ }
745
+ templates.push({
746
+ root,
747
+ quality: def.quality,
748
+ priority: def.priority,
749
+ pcs,
750
+ weights,
751
+ rel
752
+ });
753
+ }
754
+ }
755
+ return templates;
756
+ })();
757
+ var MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
758
+ var NATURAL_MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10];
759
+ var scaleOf = (key) => {
760
+ const base = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
761
+ return base.map((d) => toPitchClass(d + key.tonic));
762
+ };
763
+ var keyBonus = (tmpl, key) => {
764
+ const scale = scaleOf(key);
765
+ const scaleSet = new Set(scale);
766
+ const rootDiatonic = scaleSet.has(tmpl.root);
767
+ let allDiatonic = true;
768
+ for (const pc of tmpl.pcs)
769
+ if (!scaleSet.has(pc)) {
770
+ allDiatonic = false;
771
+ break;
772
+ }
773
+ let bonus = 0;
774
+ if (allDiatonic) bonus += 0.25;
775
+ else if (rootDiatonic) bonus += 0.1;
776
+ const degree = toPitchClass(tmpl.root - key.tonic);
777
+ if (degree === 0 || degree === 5 || degree === 7) bonus += 0.05;
778
+ return bonus;
779
+ };
780
+ var makeFrame = (notes, start, end) => {
781
+ const raw = new Array(12).fill(0);
782
+ let total = 0;
783
+ let bassPitch = Number.POSITIVE_INFINITY;
784
+ let bass = -1;
785
+ for (const n of notes) {
786
+ const s = Math.max(n.when, start);
787
+ const e = Math.min(n.when + Math.max(n.duration, 0), end);
788
+ const overlap = n.duration <= 0 ? n.when >= start && n.when < end ? 1 : 0 : Math.max(e - s, 0);
789
+ if (overlap <= 0) continue;
790
+ raw[toPitchClass(n.pitch)] += overlap;
791
+ total += overlap;
792
+ if (n.pitch < bassPitch) {
793
+ bassPitch = n.pitch;
794
+ bass = toPitchClass(n.pitch);
795
+ }
796
+ }
797
+ const profile = total > 0 ? raw.map((v) => v / total) : raw;
798
+ return {
799
+ when: start,
800
+ duration: end - start,
801
+ profile,
802
+ bass,
803
+ empty: total === 0
804
+ };
805
+ };
806
+ var emissionScore = (frame, tmpl, key, ncTonePenalty) => {
807
+ let hit = 0;
808
+ let miss = 0;
809
+ for (let pc = 0; pc < 12; pc++) {
810
+ const w = frame.profile[pc];
811
+ if (w === 0) continue;
812
+ if (tmpl.pcs.has(pc)) hit += w * tmpl.weights[pc];
813
+ else miss += w;
814
+ }
815
+ let score = hit - ncTonePenalty * miss;
816
+ if (frame.profile[tmpl.root] === 0) score -= 0.3;
817
+ if (frame.bass !== -1 && tmpl.root === frame.bass) score += 0.3;
818
+ if (key) score += keyBonus(tmpl, key);
819
+ score -= tmpl.priority * 2e-3;
820
+ return score;
821
+ };
822
+ var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
823
+ var chordDegree = (key, tmpl) => {
824
+ const scale = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
825
+ const rel = toPitchClass(tmpl.root - key.tonic);
826
+ let idx = scale.indexOf(rel);
827
+ let accidental = "";
828
+ if (idx === -1) {
829
+ const below = scale.indexOf(toPitchClass(rel - 1));
830
+ const above = scale.indexOf(toPitchClass(rel + 1));
831
+ if (below !== -1) {
832
+ idx = below;
833
+ accidental = "#";
834
+ } else if (above !== -1) {
835
+ idx = above;
836
+ accidental = "b";
837
+ } else {
838
+ idx = 0;
839
+ accidental = "?";
840
+ }
841
+ }
842
+ const hasM3 = tmpl.rel.has(4);
843
+ const hasm3 = tmpl.rel.has(3);
844
+ const hasDim5 = tmpl.rel.has(6);
845
+ const hasAug5 = tmpl.rel.has(8);
846
+ const hasMin7 = tmpl.rel.has(10);
847
+ let numeral = ROMAN[idx];
848
+ let suffix = "";
849
+ if (hasm3 && hasDim5) {
850
+ numeral = numeral.toLowerCase();
851
+ suffix = hasMin7 ? "\xF87" : "\xB0";
852
+ if (tmpl.rel.has(9)) suffix = "\xB07";
853
+ } else if (hasM3 && hasAug5) {
854
+ suffix = "+";
855
+ } else if (hasm3) {
856
+ numeral = numeral.toLowerCase();
857
+ } else if (!hasM3) {
858
+ }
859
+ if (!suffix) {
860
+ if (tmpl.rel.has(11)) suffix = "M7";
861
+ else if (hasMin7) suffix = "7";
862
+ else if (tmpl.rel.has(9) && !tmpl.rel.has(10)) suffix = "6";
863
+ }
864
+ return accidental + numeral + suffix;
865
+ };
866
+ var viterbi = (emissions, changePenalty) => {
867
+ const T = emissions.length;
868
+ const N = CHORD_TEMPLATES.length;
869
+ if (T === 0) return [];
870
+ const back = Array.from(
871
+ { length: T },
872
+ () => new Array(N).fill(-1)
873
+ );
874
+ let prev = emissions[0].slice();
875
+ for (let t = 1; t < T; t++) {
876
+ let bestPrevVal = Number.NEGATIVE_INFINITY;
877
+ let bestPrevIdx = 0;
878
+ for (let j = 0; j < N; j++)
879
+ if (prev[j] > bestPrevVal) {
880
+ bestPrevVal = prev[j];
881
+ bestPrevIdx = j;
882
+ }
883
+ const curr = new Array(N).fill(0);
884
+ const em = emissions[t];
885
+ const switchVal = bestPrevVal - changePenalty;
886
+ for (let i = 0; i < N; i++) {
887
+ if (prev[i] >= switchVal) {
888
+ curr[i] = em[i] + prev[i];
889
+ back[t][i] = i;
890
+ } else {
891
+ curr[i] = em[i] + switchVal;
892
+ back[t][i] = bestPrevIdx;
893
+ }
894
+ }
895
+ prev = curr;
896
+ }
897
+ let bestIdx = 0;
898
+ for (let i = 1; i < N; i++) if (prev[i] > prev[bestIdx]) bestIdx = i;
899
+ const path = new Array(T).fill(0);
900
+ path[T - 1] = bestIdx;
901
+ for (let t = T - 1; t > 0; t--) path[t - 1] = back[t][path[t]];
902
+ return path;
903
+ };
904
+ var keyAt = (keys, when) => {
905
+ for (const k of keys)
906
+ if (when >= k.when && when < k.when + k.duration) return k.key;
907
+ return keys.length ? keys[keys.length - 1].key : null;
908
+ };
909
+ var buildSymbol = (tmpl, bass, flat) => {
910
+ const rootSymbol = noteName(tmpl.root, flat) + tmpl.quality;
911
+ const inversion = bass !== -1 && bass !== tmpl.root && tmpl.pcs.has(bass);
912
+ return {
913
+ symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
914
+ rootSymbol,
915
+ inversion,
916
+ bass: bass === -1 ? tmpl.root : bass
917
+ };
918
+ };
919
+ var detectProgression = (notes, options = {}) => {
920
+ if (!notes.length) return { keys: [], chords: [] };
165
921
  const {
166
- chordStr,
167
- patternType,
168
- rootShift,
922
+ flat = false,
169
923
  bpm,
170
- stepsPerBar,
171
- parseChord,
172
- parseChords
924
+ frameSize = 0.5,
925
+ changePenalty = 0.4,
926
+ nonChordTonePenalty = 0.55,
927
+ useKey = true
173
928
  } = options;
929
+ const keys = detectKeyChanges(notes, options);
930
+ const start = notes.reduce(
931
+ (m, n) => Math.min(m, n.when),
932
+ Number.POSITIVE_INFINITY
933
+ );
934
+ const end = notes.reduce(
935
+ (m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
936
+ Number.NEGATIVE_INFINITY
937
+ );
938
+ if (end <= start) return { keys, chords: [] };
939
+ const frameDur = bpm ? 60 / bpm : Math.max(frameSize, 1e-3);
940
+ const frames = [];
941
+ for (let t = start; t < end - 1e-9; t += frameDur)
942
+ frames.push(makeFrame(notes, t, Math.min(t + frameDur, end)));
943
+ const emissions = frames.map((frame) => {
944
+ if (frame.empty) return new Array(CHORD_TEMPLATES.length).fill(0);
945
+ const key = useKey ? keyAt(keys, frame.when + frame.duration / 2) : null;
946
+ return CHORD_TEMPLATES.map(
947
+ (tmpl) => emissionScore(frame, tmpl, key, nonChordTonePenalty)
948
+ );
949
+ });
950
+ const path = viterbi(emissions, changePenalty);
951
+ const chords = [];
952
+ for (let t = 0; t < frames.length; t++) {
953
+ const frame = frames[t];
954
+ const tmpl = CHORD_TEMPLATES[path[t]];
955
+ const last = chords[chords.length - 1];
956
+ const sameAsLast = last && last.root === tmpl.root && last.quality === tmpl.quality;
957
+ if (sameAsLast) {
958
+ last.duration = frame.when + frame.duration - last.when;
959
+ continue;
960
+ }
961
+ const key = keyAt(keys, frame.when + frame.duration / 2);
962
+ const { symbol, rootSymbol, inversion, bass } = buildSymbol(
963
+ tmpl,
964
+ frame.bass,
965
+ flat
966
+ );
967
+ chords.push({
968
+ symbol,
969
+ rootSymbol,
970
+ root: tmpl.root,
971
+ quality: tmpl.quality,
972
+ bass,
973
+ inversion,
974
+ when: frame.when,
975
+ duration: frame.duration,
976
+ key,
977
+ degree: key ? chordDegree(key, tmpl) : null
978
+ });
979
+ }
980
+ return { keys, chords };
981
+ };
982
+ var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
983
+ var parseChords = (str, bpm = 120) => {
984
+ const output = [];
985
+ const secBar = 60 / bpm * 4;
986
+ const frontChars = new Set("ABCDEFG_=%N");
987
+ let idx = 0;
988
+ let last = null;
989
+ for (const line of toHan(str).split("\n").map((v) => v.trim())) {
990
+ if (!line.length || /^#/.test(line)) continue;
991
+ for (const str2 of line.split(/[|ll→]/)) {
992
+ if (!str2.length) continue;
993
+ const when = idx++ * secBar;
994
+ const a = [];
995
+ for (let i = 0; i < str2.length; i++) {
996
+ const char = str2[i];
997
+ const prev = str2[i - 1];
998
+ const prev2 = str2.slice(i - 2, i);
999
+ if (!frontChars.has(char)) continue;
1000
+ if (prev === "/" || prev2 === "on") continue;
1001
+ if (prev2 === "N." && char === "C") continue;
1002
+ a.push(i);
1003
+ }
1004
+ if (!a.length) continue;
1005
+ const divide = 2 ** Math.ceil(Math.log2(a.length));
1006
+ const unitTime = secBar / divide;
1007
+ for (const [i, v] of a.entries()) {
1008
+ const s = str2.slice(v, i === a.length - 1 ? str2.length : a[i + 1]).replace(/\s+/g, "");
1009
+ const c = s[0];
1010
+ if (c === "_" || c === "N") {
1011
+ last = null;
1012
+ continue;
1013
+ }
1014
+ if (c === "=") {
1015
+ if (last) last.duration += unitTime;
1016
+ continue;
1017
+ }
1018
+ const _when = when + i * unitTime;
1019
+ if (c === "%") {
1020
+ if (last === null) continue;
1021
+ const base = last;
1022
+ last = { ...base, when: _when, duration: unitTime };
1023
+ } else {
1024
+ const key = s.slice(0, s[1] === "#" ? 2 : 1);
1025
+ const chord = s.slice(key.length).replace(/[\s・]/g, "");
1026
+ last = {
1027
+ key,
1028
+ chord,
1029
+ when: _when,
1030
+ duration: unitTime
1031
+ };
1032
+ }
1033
+ output.push(last);
1034
+ }
1035
+ if (last !== null && divide > a.length)
1036
+ last.duration += unitTime * (divide - a.length);
1037
+ }
1038
+ }
1039
+ return output;
1040
+ };
1041
+
1042
+ // src/chords.ts
1043
+ var C3 = 48;
1044
+ var buildChordPlacements = (options) => {
1045
+ const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
174
1046
  const placements = [];
175
1047
  if (!chordStr.trim()) return placements;
176
1048
  const offset = rootShift;
@@ -200,7 +1072,7 @@ var buildChordPlacements = (options) => {
200
1072
  for (const chord of group) {
201
1073
  let notes;
202
1074
  try {
203
- notes = [...parseChord(`${chord.key}${chord.chord}`).value];
1075
+ notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
204
1076
  } catch {
205
1077
  continue;
206
1078
  }
@@ -287,7 +1159,7 @@ var buildChordPlacements = (options) => {
287
1159
  chordNames.forEach((chordName, barIndex) => {
288
1160
  let notes;
289
1161
  try {
290
- notes = [...parseChord(chordName).value];
1162
+ notes = [...parseChord(chordName).notes];
291
1163
  } catch {
292
1164
  return;
293
1165
  }
@@ -361,6 +1233,7 @@ var buildUI = (target, options) => {
361
1233
  <button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
362
1234
  <button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
363
1235
  <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
1236
+ <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
364
1237
  <span class="dtm-grow"></span>
365
1238
  <span class="dtm-label">BPM</span>
366
1239
  <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
@@ -453,10 +1326,10 @@ var buildUI = (target, options) => {
453
1326
  <div class="dtm-row dtm-hidden" data-dtm="midi-track-selection"></div>
454
1327
  <div class="dtm-row">
455
1328
  <span class="dtm-label">MML</span>
456
- <textarea class="dtm-textarea" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1329
+ <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1330
+ <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">\u8AAD\u8FBC</button>
457
1331
  </div>
458
1332
  <div class="dtm-row">
459
- <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">MML\u8AAD\u8FBC</button>
460
1333
  <span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
461
1334
  <select class="dtm-select" data-dtm="shift-select">
462
1335
  <option value="-96">-2\u5206</option>
@@ -530,6 +1403,8 @@ var buildUI = (target, options) => {
530
1403
  const sel = (name) => q(root, `[data-dtm="${name}"]`);
531
1404
  return {
532
1405
  root,
1406
+ topbar: sel("transport"),
1407
+ topbarLoading: sel("topbar-loading"),
533
1408
  playBtn: sel("play"),
534
1409
  recBtn: sel("rec"),
535
1410
  soloCheckbox: sel("solo"),
@@ -1019,6 +1894,7 @@ var DEFAULT_PAN = 64;
1019
1894
  var DEFAULT_VELOCITY = 100;
1020
1895
  var DEFAULT_PLAYBACK_VELOCITY = 127;
1021
1896
  var DEFAULT_STEPS_PER_BAR = 192;
1897
+ var MML_END_MARKER = "#end;";
1022
1898
 
1023
1899
  // src/lyrics.ts
1024
1900
  var kanaTable = {
@@ -1747,8 +2623,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
1747
2623
  ))().then((v) => {
1748
2624
  loaded.set(m, v);
1749
2625
  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);
2626
+ }).catch((err2) => {
2627
+ console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err2);
1752
2628
  return null;
1753
2629
  });
1754
2630
  loading.set(m, p);
@@ -3151,7 +4027,9 @@ var parseMML = (mml, options = {}) => {
3151
4027
  const meta = parseMmlMeta(noComments);
3152
4028
  const noMeta = stripMmlMeta(noComments);
3153
4029
  const lyrics = collectLyrics ? parseLyrics(noMeta) : void 0;
3154
- const fullMML = stripLyrics(noMeta).replace(/[\n\r]+/g, " ").trim();
4030
+ const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
4031
+ const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
4032
+ const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
3155
4033
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
3156
4034
  let trackIndex = 0;
3157
4035
  let octave = 4;
@@ -3240,8 +4118,10 @@ var parseMML = (mml, options = {}) => {
3240
4118
  numStr += body[j];
3241
4119
  j++;
3242
4120
  }
3243
- if (ch === "t" && trackIndex === 0 && numStr) {
3244
- bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
4121
+ if (ch === "t" && numStr) {
4122
+ if (bpm === null) {
4123
+ bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
4124
+ }
3245
4125
  }
3246
4126
  pushTok("ctrl", currentStep, 0, tokStart);
3247
4127
  } else if (ch === "[") {
@@ -3743,6 +4623,7 @@ var DAW_CSS = `
3743
4623
  }
3744
4624
  .dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
3745
4625
  .dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
4626
+ .dtm-textarea.dtm-grow { width: 0; }
3746
4627
  .dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
3747
4628
 
3748
4629
  /* \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 +4791,7 @@ var DAW_CSS = `
3910
4791
 
3911
4792
  /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
3912
4793
  .dtm-overlay {
3913
- position: absolute; inset: 0; z-index: 1000;
4794
+ position: absolute; inset: 0; z-index: 10;
3914
4795
  background: rgba(0,0,0,.92);
3915
4796
  display: flex; align-items: center; justify-content: center;
3916
4797
  flex-direction: column; gap: 14px;
@@ -3959,6 +4840,22 @@ var DAW_CSS = `
3959
4840
  letter-spacing: .15em;
3960
4841
  min-height: 1em;
3961
4842
  }
4843
+ .dtm-topbar-loading {
4844
+ display: none;
4845
+ font-family: var(--dtm-font);
4846
+ font-size: 11px;
4847
+ color: var(--dtm-primary);
4848
+ margin-left: 12px;
4849
+ letter-spacing: .15em;
4850
+ align-self: center;
4851
+ }
4852
+ .dtm-topbar.is-loading .dtm-topbar-loading {
4853
+ display: inline-block;
4854
+ }
4855
+ .dtm-topbar.is-loading {
4856
+ pointer-events: none;
4857
+ opacity: 0.7;
4858
+ }
3962
4859
 
3963
4860
  @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
3964
4861
  .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
@@ -4034,6 +4931,14 @@ var DAW_CSS = `
4034
4931
  min-width: 2em;
4035
4932
  margin-left: 4px;
4036
4933
  }
4934
+ .dtm-player-chord {
4935
+ font-family: 'k8x12', monospace;
4936
+ font-size: 11px;
4937
+ color: var(--dtm-accent);
4938
+ min-width: 4em;
4939
+ margin-left: 8px;
4940
+ font-weight: bold;
4941
+ }
4037
4942
  .dtm-player-dots {
4038
4943
  margin-left: auto;
4039
4944
  display: flex;
@@ -4384,7 +5289,7 @@ var mountDAW = (target, options = {}) => {
4384
5289
  const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
4385
5290
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
4386
5291
  const showMidi = !!options.parseMidi;
4387
- const showChord = !!(options.parseChord && options.parseChords);
5292
+ const showChord = true;
4388
5293
  const refs = buildUI(target, {
4389
5294
  tracks: trackConfigs,
4390
5295
  drumPatternNames: Object.keys(drumPatterns),
@@ -5080,13 +5985,15 @@ var mountDAW = (target, options = {}) => {
5080
5985
  const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
5081
5986
  if (streaming && voices) {
5082
5987
  const overlay = showLoadingOverlay(refs.rollContainer);
5988
+ setLoading(true);
5083
5989
  try {
5084
5990
  await voices.loadModels(streamTracks.map((t) => t.model));
5085
5991
  await voices.warm(streamTracks);
5086
- } catch (err) {
5087
- console.warn("[dtm] voice preload failed", err);
5992
+ } catch (err2) {
5993
+ console.warn("[dtm] voice preload failed", err2);
5088
5994
  } finally {
5089
5995
  overlay.remove();
5996
+ setLoading(false);
5090
5997
  }
5091
5998
  }
5092
5999
  if (playbackState !== "paused") {
@@ -5425,8 +6332,8 @@ var mountDAW = (target, options = {}) => {
5425
6332
  const decomposedMini = monoTracks.map(
5426
6333
  (notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
5427
6334
  );
5428
- const full2 = [metaLine, ...decomposedFull].filter((s) => s.length > 0).join(";\n");
5429
- const minified2 = [metaLine, ...decomposedMini].filter((s) => s.length > 0).join(";");
6335
+ const full2 = [metaLine, ...decomposedFull, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
6336
+ const minified2 = [metaLine, ...decomposedMini, MML_END_MARKER].filter((s) => s.length > 0).join(";");
5430
6337
  return {
5431
6338
  full: full2,
5432
6339
  minified: minified2,
@@ -5435,21 +6342,28 @@ var mountDAW = (target, options = {}) => {
5435
6342
  barLimit: barLimitBars
5436
6343
  };
5437
6344
  }
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
- );
6345
+ const trackLines = [];
6346
+ const trackLinesMini = [];
6347
+ trackStates.forEach((t, i) => {
6348
+ const notes = clipNotes(t.core.getNotes());
6349
+ if (notes.length > 0) {
6350
+ const mml = t.core.getMMLFromNotes(notes, bpm, t.volume).trim();
6351
+ trackLines.push(`@${i} ${mml}`);
6352
+ trackLinesMini.push(`@${i}${mml.replace(/\s+/g, "")}`);
6353
+ }
6354
+ });
5444
6355
  const lyricLines = trackStates.map((t, i) => ({
5445
6356
  i,
5446
- text: t.lyrics.trim(),
6357
+ notes: clipNotes(t.core.getNotes()),
6358
+ text: t.lyrics.replace(/[\r\n]+/g, " ").trim(),
5447
6359
  model: t.lyricModel.trim(),
5448
6360
  vol: t.vocalVolume,
5449
6361
  gate: t.vocalGate,
5450
6362
  pan: t.vocalPan,
5451
6363
  oct: t.vocalOctave
5452
- })).filter((x) => x.model.length > 0 && x.text.length > 0).map((x) => {
6364
+ })).filter(
6365
+ (x) => x.model.length > 0 && x.text.length > 0 && x.notes.length > 0
6366
+ ).map((x) => {
5453
6367
  const params = [
5454
6368
  x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
5455
6369
  x.gate === 100 ? "" : `q${x.gate}`,
@@ -5459,13 +6373,18 @@ var mountDAW = (target, options = {}) => {
5459
6373
  const head = params ? `${x.model} ${params}` : x.model;
5460
6374
  return `@@${x.i} ${head} ${x.text}`;
5461
6375
  });
5462
- const full = [metaLine, ...trackLines, ...lyricLines].filter((s) => s.length > 0).join(";\n");
5463
- const minified = [metaLine, ...trackLinesMini, ...lyricLines].filter((s) => s.length > 0).join(";");
6376
+ const full = [metaLine, ...trackLines, ...lyricLines, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
6377
+ const minified = [
6378
+ metaLine,
6379
+ ...trackLinesMini,
6380
+ ...lyricLines,
6381
+ MML_END_MARKER
6382
+ ].filter((s) => s.length > 0).join(";");
5464
6383
  return {
5465
6384
  full,
5466
6385
  minified,
5467
6386
  ignoredCount: 0,
5468
- trackCount: trackStates.length,
6387
+ trackCount: trackLines.length,
5469
6388
  barLimit: barLimitBars
5470
6389
  };
5471
6390
  };
@@ -5585,7 +6504,6 @@ var mountDAW = (target, options = {}) => {
5585
6504
  updateUndoRedo();
5586
6505
  };
5587
6506
  const applyChord = () => {
5588
- if (!options.parseChord || !options.parseChords) return;
5589
6507
  const active = getActive();
5590
6508
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
5591
6509
  if (!chordTrack) return;
@@ -5594,9 +6512,7 @@ var mountDAW = (target, options = {}) => {
5594
6512
  patternType: active.savedChordPattern,
5595
6513
  rootShift: active.savedChordRoot,
5596
6514
  bpm,
5597
- stepsPerBar: renderConfig.stepsPerBar,
5598
- parseChord: options.parseChord,
5599
- parseChords: options.parseChords
6515
+ stepsPerBar: renderConfig.stepsPerBar
5600
6516
  });
5601
6517
  chordTrack.core.clearNotesWithoutHistory();
5602
6518
  chordTrack.core.beginBatch();
@@ -5681,9 +6597,11 @@ var mountDAW = (target, options = {}) => {
5681
6597
  };
5682
6598
  const overlayDuring = (fn) => {
5683
6599
  refs.overlay.hidden = false;
6600
+ setLoading(true);
5684
6601
  setTimeout(() => {
5685
6602
  fn();
5686
6603
  refs.overlay.hidden = true;
6604
+ setLoading(false);
5687
6605
  }, 30);
5688
6606
  };
5689
6607
  const wireEvents = () => {
@@ -5827,6 +6745,7 @@ var mountDAW = (target, options = {}) => {
5827
6745
  const file = refs.midiInput.files?.[0];
5828
6746
  if (!file || !options.parseMidi) return;
5829
6747
  refs.overlay.hidden = false;
6748
+ setLoading(true);
5830
6749
  const buffer = new Uint8Array(await file.arrayBuffer());
5831
6750
  pendingMidi = options.parseMidi(buffer);
5832
6751
  detectedTracks = analyzeMidiTracks(pendingMidi);
@@ -5847,6 +6766,7 @@ var mountDAW = (target, options = {}) => {
5847
6766
  });
5848
6767
  refs.midiTrackSelection.classList.remove("dtm-hidden");
5849
6768
  refs.overlay.hidden = true;
6769
+ setLoading(false);
5850
6770
  });
5851
6771
  refs.midiLoadBtn.addEventListener("click", () => {
5852
6772
  if (!pendingMidi) return;
@@ -5920,6 +6840,9 @@ var mountDAW = (target, options = {}) => {
5920
6840
  resizeObserver.observe(refs.rollContainer);
5921
6841
  document.addEventListener("pointermove", onPointerMove);
5922
6842
  document.addEventListener("pointerup", onPointerUp);
6843
+ const setLoading = (loading) => {
6844
+ refs.topbar.classList.toggle("is-loading", loading);
6845
+ };
5923
6846
  return {
5924
6847
  play,
5925
6848
  pause,
@@ -5957,6 +6880,7 @@ var mountDAW = (target, options = {}) => {
5957
6880
  exportMIDI: exportMIDI2,
5958
6881
  setBpm,
5959
6882
  getPlaybackState: () => playbackState,
6883
+ setLoading,
5960
6884
  destroy: () => {
5961
6885
  sequencer.stop();
5962
6886
  resizeObserver.disconnect();
@@ -6101,6 +7025,36 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6101
7025
  const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
6102
7026
  (a, b) => a - b
6103
7027
  );
7028
+ const maxStep = placements.reduce(
7029
+ (max, p) => Math.max(max, p.startStep + p.durationSteps),
7030
+ 0
7031
+ );
7032
+ const timedNotes = placements.map((p) => ({
7033
+ pitch: p.pitch,
7034
+ when: p.startStep * secondsPerStep,
7035
+ duration: p.durationSteps * secondsPerStep
7036
+ }));
7037
+ const stepChords = [];
7038
+ if (timedNotes.length > 0) {
7039
+ let chordSegments = [];
7040
+ try {
7041
+ chordSegments = detectProgression(timedNotes, { bpm }).chords;
7042
+ } catch {
7043
+ chordSegments = [];
7044
+ }
7045
+ for (const seg of chordSegments) {
7046
+ const startStep = Math.max(0, Math.round(seg.when / secondsPerStep));
7047
+ const endStep = Math.round((seg.when + seg.duration) / secondsPerStep);
7048
+ for (let s = startStep; s < endStep && s <= maxStep; s++) {
7049
+ stepChords[s] = seg.symbol;
7050
+ }
7051
+ }
7052
+ let lastChord = "";
7053
+ for (let s = 0; s <= maxStep; s++) {
7054
+ if (stepChords[s]) lastChord = stepChords[s];
7055
+ else stepChords[s] = lastChord;
7056
+ }
7057
+ }
6104
7058
  const seqTracks = trackIndices.map((index) => {
6105
7059
  let id = 0;
6106
7060
  const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
@@ -6301,6 +7255,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6301
7255
  barEl.className = "dtm-player-bar";
6302
7256
  barEl.textContent = "-";
6303
7257
  beatRow.appendChild(barEl);
7258
+ const chordEl = doc.createElement("span");
7259
+ chordEl.className = "dtm-player-chord";
7260
+ chordEl.textContent = "";
7261
+ beatRow.appendChild(chordEl);
6304
7262
  const chips = [];
6305
7263
  const makeChip = (label) => {
6306
7264
  const chip = doc.createElement("span");
@@ -6444,10 +7402,20 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6444
7402
  lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
6445
7403
  };
6446
7404
  const renderPlayhead = (step) => {
7405
+ const intStep = Math.floor(step);
6447
7406
  const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
6448
7407
  for (let i = 0; i < 4; i++)
6449
7408
  beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
6450
7409
  barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
7410
+ const chordName = stepChords[intStep] ?? "";
7411
+ if (chordEl.textContent !== chordName) {
7412
+ chordEl.textContent = chordName;
7413
+ if (chordName) {
7414
+ console.log(
7415
+ `[dtm-player-chord] Active Chord: ${chordName} (step: ${intStep})`
7416
+ );
7417
+ }
7418
+ }
6451
7419
  for (const view of laneViews) {
6452
7420
  let active = null;
6453
7421
  for (const t of view.tokens) {
@@ -6461,6 +7429,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6461
7429
  const resetPlayhead = () => {
6462
7430
  for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
6463
7431
  barEl.textContent = "-";
7432
+ chordEl.textContent = "";
6464
7433
  for (const view of laneViews) {
6465
7434
  for (const t of view.tokens) t.el.classList.remove("is-active");
6466
7435
  view.lane.scrollLeft = 0;
@@ -6524,6 +7493,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6524
7493
  });
6525
7494
  }
6526
7495
  return {
7496
+ id: String(index),
6527
7497
  model: lt.model,
6528
7498
  volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
6529
7499
  pan: panToStereo(lt.pan ?? DEFAULT_PAN),
@@ -6539,15 +7509,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6539
7509
  try {
6540
7510
  await v.loadModels(tracks.map((t) => t.model));
6541
7511
  await v.warm(tracks);
6542
- } catch (err) {
6543
- console.warn("[dtm] voice preload failed", err);
7512
+ } catch (err2) {
7513
+ console.warn("[dtm] voice preload failed", err2);
6544
7514
  } finally {
6545
7515
  overlay.remove();
6546
7516
  }
6547
7517
  if (!playing || activePlayer !== instance) return;
6548
7518
  }
6549
7519
  seq.start(0);
6550
- if (streaming) ensureVoices().startStream(tracks, seq.getStartTime());
7520
+ if (streaming) {
7521
+ ensureVoices().startStream(tracks, seq.getStartTime(), {
7522
+ isAudible: (t) => !mutedTracks.has(Number(t.id))
7523
+ });
7524
+ }
6551
7525
  };
6552
7526
  const play = () => {
6553
7527
  if (playing || trackIndices.length === 0) return;
@@ -6882,8 +7856,6 @@ var DEFAULT_CDN = {
6882
7856
  soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
6883
7857
  soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
6884
7858
  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
7859
  midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
6888
7860
  };
6889
7861
  var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
@@ -6927,24 +7899,6 @@ var createDtmStudio = async (options = {}) => {
6927
7899
  eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
6928
7900
  eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
6929
7901
  ]);
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
7902
  let midiParser = null;
6949
7903
  let parseMidi;
6950
7904
  if (features.midi) {
@@ -7130,8 +8084,6 @@ var createDtmStudio = async (options = {}) => {
7130
8084
  onPlayNote: playNote,
7131
8085
  onPlayDrum: playDrum,
7132
8086
  singingVoices,
7133
- parseChord,
7134
- parseChords,
7135
8087
  parseMidi,
7136
8088
  onToggleRecord,
7137
8089
  ...dawOverrides
@@ -7153,15 +8105,32 @@ var createDtmStudio = async (options = {}) => {
7153
8105
  editorPresetSelects.set(target, select);
7154
8106
  select.addEventListener("change", async () => {
7155
8107
  if (!select) return;
7156
- daw.setInstrument(select.value);
7157
- await loadPreset(select.value, trackIds);
8108
+ const wasPlaying = daw.getPlaybackState() === "playing";
8109
+ if (wasPlaying) {
8110
+ daw.pause();
8111
+ }
8112
+ const overlay = showLoadingOverlay(target);
8113
+ daw.setLoading?.(true);
8114
+ try {
8115
+ daw.setInstrument(select.value);
8116
+ await loadPreset(select.value, trackIds);
8117
+ } finally {
8118
+ overlay.remove();
8119
+ daw.setLoading?.(false);
8120
+ if (wasPlaying) {
8121
+ daw.play();
8122
+ }
8123
+ }
7158
8124
  });
7159
8125
  }
7160
8126
  const daw = mountDAW(target, base);
7161
8127
  mountedEditors.push(daw);
7162
8128
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7163
8129
  daw.setInstrument(presetKey);
7164
- void loadPreset(presetKey, trackIds);
8130
+ daw.setLoading?.(true);
8131
+ void loadPreset(presetKey, trackIds).finally(() => {
8132
+ daw.setLoading?.(false);
8133
+ });
7165
8134
  const destroy = () => {
7166
8135
  daw.destroy();
7167
8136
  select?.remove();
@@ -7230,6 +8199,7 @@ var createDtmStudio = async (options = {}) => {
7230
8199
  LinkedList,
7231
8200
  MAX_VOCAL_VOLUME,
7232
8201
  MMLCore,
8202
+ MML_END_MARKER,
7233
8203
  PITCH_MAP,
7234
8204
  PREWARM_NOTES,
7235
8205
  TRACKS_ADVANCED,