@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.mjs CHANGED
@@ -55,18 +55,889 @@ async function buildNameToKeyMapping() {
55
55
  return nameToKey;
56
56
  }
57
57
 
58
- // src/chords.ts
59
- var C3 = 48;
60
- var buildChordPlacements = (options) => {
58
+ // node_modules/.pnpm/@onjmin+chord-parser@1.0.2/node_modules/@onjmin/chord-parser/dist/index.mjs
59
+ var SHARP_NAMES = [
60
+ "C",
61
+ "C#",
62
+ "D",
63
+ "D#",
64
+ "E",
65
+ "F",
66
+ "F#",
67
+ "G",
68
+ "G#",
69
+ "A",
70
+ "A#",
71
+ "B"
72
+ ];
73
+ var FLAT_NAMES = [
74
+ "C",
75
+ "Db",
76
+ "D",
77
+ "Eb",
78
+ "E",
79
+ "F",
80
+ "Gb",
81
+ "G",
82
+ "Ab",
83
+ "A",
84
+ "Bb",
85
+ "B"
86
+ ];
87
+ var toPitchClass = (n) => (n % 12 + 12) % 12;
88
+ var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
89
+ var SyntaxErrorWithPos = class extends Error {
90
+ constructor(input, msg) {
91
+ super(
92
+ `SyntaxError: ${msg}
93
+ input.idx: ${input.idx}
94
+ input.str: ${input.str}`
95
+ );
96
+ this.name = "ChordSyntaxError";
97
+ }
98
+ };
99
+ var err = (input, msg) => {
100
+ throw new SyntaxErrorWithPos(input, msg);
101
+ };
102
+ var Input = class _Input {
103
+ static nums = new Set("0123456789");
104
+ str;
105
+ nest;
106
+ idx;
107
+ constructor(str, nest = 0) {
108
+ this.str = str;
109
+ this.nest = nest;
110
+ this.idx = 0;
111
+ }
112
+ get isEOF() {
113
+ return this.str.length <= this.idx;
114
+ }
115
+ get char() {
116
+ return this.str[this.idx];
117
+ }
118
+ /** 先頭の連続する数字を消費して数値で返す(破壊的)。数字が無ければ null。 */
119
+ get num() {
120
+ let str = "";
121
+ while (!this.isEOF) {
122
+ const char = this.char;
123
+ if (!_Input.nums.has(char)) break;
124
+ str += char;
125
+ this.idx++;
126
+ }
127
+ return str.length ? Number(str) : null;
128
+ }
129
+ slice(i) {
130
+ return this.str.slice(this.idx, this.idx + i);
131
+ }
132
+ };
133
+ var Output = class {
134
+ pitch = null;
135
+ chord = null;
136
+ isChord = false;
137
+ pending = null;
138
+ nest = -1;
139
+ get value() {
140
+ const { pitch, chord } = this;
141
+ return new Set(
142
+ [...chord].map((v) => v + pitch)
143
+ );
144
+ }
145
+ set value(chord) {
146
+ const pitch = this.pitch;
147
+ this.chord = new Set([...chord].map((v) => v - pitch));
148
+ }
149
+ };
150
+ var Matcher = class {
151
+ map = /* @__PURE__ */ new Map();
152
+ /** キー長を降順で保持(最長一致のため)。 */
153
+ lengths = [];
154
+ _set(key, value) {
155
+ this.map.set(key, value);
156
+ if (!this.lengths.includes(key.length)) {
157
+ this.lengths.push(key.length);
158
+ this.lengths.sort((a, b) => b - a);
159
+ }
160
+ }
161
+ set(key, value) {
162
+ if (Array.isArray(key)) for (const k of key) this._set(k, value);
163
+ else this._set(key, value);
164
+ }
165
+ parse(input) {
166
+ for (const i of this.lengths) {
167
+ const s = input.slice(i);
168
+ if (this.map.has(s)) {
169
+ input.idx += s.length;
170
+ return this.map.get(s);
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+ };
176
+ var BRACKET_START = 0;
177
+ var BRACKET_END = 1;
178
+ var COMMA = 2;
179
+ var DIVIDE = 3;
180
+ var formulaMatcher = new Matcher();
181
+ formulaMatcher.set("(", BRACKET_START);
182
+ formulaMatcher.set(")", BRACKET_END);
183
+ formulaMatcher.set(",", COMMA);
184
+ formulaMatcher.set(["/", "on"], DIVIDE);
185
+ var parseFormula = (input, output = new Output(), nest = 0) => {
186
+ let start = input.idx;
187
+ const _eval = (idx) => {
188
+ const str = input.str.slice(start, idx);
189
+ if (str.length) parseTerm(new Input(str, nest), output);
190
+ };
191
+ while (true) {
192
+ const { idx } = input;
193
+ if (input.isEOF) {
194
+ if (nest) err(input, `Unclosed ${nest} brackets`);
195
+ _eval(idx);
196
+ return output;
197
+ }
198
+ const res = formulaMatcher.parse(input);
199
+ if (res === null) {
200
+ input.idx++;
201
+ continue;
202
+ }
203
+ const { pending } = output;
204
+ _eval(idx);
205
+ switch (res) {
206
+ case BRACKET_START:
207
+ parseFormula(input, output, nest + 1);
208
+ break;
209
+ case BRACKET_END:
210
+ if (nest - 1 < 0) err(input, "Unable to close brackets");
211
+ return output;
212
+ case COMMA:
213
+ output.pending = pending;
214
+ break;
215
+ case DIVIDE: {
216
+ const o = parseFormula(input, new Output(), nest);
217
+ const v = [...output.value];
218
+ if (o.isChord) {
219
+ output.value = [...o.value].concat(v);
220
+ } else {
221
+ const a = v.sort((x, y) => x - y);
222
+ const pitch = (o.pitch + 3) % 12 - 3;
223
+ if (a[0] < pitch) {
224
+ while (a[0] < pitch) a.push(a.shift() + 12);
225
+ } else {
226
+ while (true) {
227
+ const w = a[a.length - 1] - 12;
228
+ if (w < pitch) break;
229
+ a.pop();
230
+ a.unshift(w);
231
+ }
232
+ }
233
+ a.push(pitch);
234
+ output.value = a;
235
+ }
236
+ break;
237
+ }
238
+ }
239
+ start = input.idx;
240
+ }
241
+ };
242
+ var parseTerm = (input, output) => {
243
+ if (input.isEOF) return output;
244
+ if (output.pitch === null) return parsePitch(input, output);
245
+ if (output.pending === null) return parseFunc(input, output);
246
+ return parsePending(input, output);
247
+ };
248
+ var halfMatcher = new Matcher();
249
+ var halfMatcherStrict = new Matcher();
250
+ for (const m of [halfMatcher, halfMatcherStrict]) {
251
+ m.set(["#", "\u266F"], 1);
252
+ m.set(["b", "\u266D"], -1);
253
+ }
254
+ halfMatcher.set("+", 1);
255
+ halfMatcher.set("-", -1);
256
+ var parseHalf = (input, isPitch = false) => (isPitch ? halfMatcherStrict : halfMatcher).parse(input);
257
+ var idx2pitch = [0, 2, 4, 5, 7, 9, 11];
258
+ for (const i of [...idx2pitch.keys()]) idx2pitch.push(idx2pitch[i] + 12);
259
+ var deg2pitch = (deg) => idx2pitch[deg - 1];
260
+ var pitchMatcher = new Matcher();
261
+ for (const [i, v] of [..."CDEFGAB"].entries())
262
+ pitchMatcher.set(v, idx2pitch[i]);
263
+ var parsePitch = (input, output) => {
264
+ const pitch = pitchMatcher.parse(input);
265
+ if (pitch === null) err(input, "Not found pitch");
266
+ output.pitch = pitch;
267
+ const half = parseHalf(input, true);
268
+ if (half !== null) output.pitch += half;
269
+ return parseBase(input, output);
270
+ };
271
+ var MAJOR = [0, 4, 7];
272
+ var DIM = [0, 3, 6];
273
+ var baseMatcher = new Matcher();
274
+ baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
275
+ baseMatcher.set(["dim", "\u3007"], DIM);
276
+ baseMatcher.set("+", [0, 4, 8]);
277
+ baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
278
+ var parseBase = (input, output) => {
279
+ const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
280
+ const res = isMajMarker ? null : baseMatcher.parse(input);
281
+ if (res !== null) output.isChord = true;
282
+ output.chord = new Set(res || MAJOR);
283
+ if (res === DIM) {
284
+ const { num } = input;
285
+ const chord = output.chord;
286
+ if (num !== null) chord.add(deg2pitch(num) - 2);
287
+ }
288
+ output.nest = input.nest;
289
+ return parseTerm(input, output);
290
+ };
291
+ var add = (chord, n, half) => {
292
+ chord.add(deg2pitch(n) + half);
293
+ };
294
+ var aug = (chord) => {
295
+ chord.delete(deg2pitch(5));
296
+ chord.add(deg2pitch(5) + 1);
297
+ };
298
+ var _7th = (chord, n, _half2, isFlat = false) => {
299
+ if (n === 5) chord.delete(deg2pitch(3));
300
+ else if (n === 6) chord.add(deg2pitch(6));
301
+ else if (n === 69) chord.add(deg2pitch(6)).add(deg2pitch(9));
302
+ else {
303
+ if (n >= 7) chord.add(deg2pitch(7) + (isFlat ? -1 : 0));
304
+ if (n >= 9) chord.add(deg2pitch(9));
305
+ if (n >= 11) chord.add(deg2pitch(11));
306
+ if (n >= 13) chord.add(deg2pitch(13));
307
+ }
308
+ };
309
+ var _half = (chord, n, half) => {
310
+ chord.delete(deg2pitch(n));
311
+ chord.add(deg2pitch(n) + half);
312
+ };
313
+ var funcMatcher = new Matcher();
314
+ funcMatcher.set("add", add);
315
+ funcMatcher.set(["omit", "no"], (chord, n, half) => {
316
+ chord.delete(deg2pitch(n) + half);
317
+ });
318
+ funcMatcher.set("sus", (chord, n, half) => {
319
+ chord.delete(deg2pitch(3));
320
+ chord.add(deg2pitch(n) + half);
321
+ });
322
+ funcMatcher.set(
323
+ ["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
324
+ _7th
325
+ );
326
+ funcMatcher.set("aug", aug);
327
+ var parseFunc = (input, output) => {
328
+ if (!output.isChord) output.isChord = true;
329
+ const func = funcMatcher.parse(input);
330
+ const chord = output.chord;
331
+ if (func === null) {
332
+ const isAug = input.char === "+";
333
+ const half = parseHalf(input);
334
+ const { num } = input;
335
+ if (num === null) {
336
+ if (isAug) aug(chord);
337
+ else err(input, "Not found number");
338
+ }
339
+ if (half === null) {
340
+ if (input.nest === output.nest) _7th(chord, num, 0, true);
341
+ else add(chord, num, 0);
342
+ } else {
343
+ _half(chord, num, half);
344
+ }
345
+ } else if (func === aug) {
346
+ aug(chord);
347
+ } else {
348
+ output.pending = func;
349
+ }
350
+ return parseTerm(input, output);
351
+ };
352
+ var parsePending = (input, output) => {
353
+ const half = parseHalf(input);
354
+ const { num } = input;
355
+ const { pending, chord } = output;
356
+ if (num === null) err(input, "Not found number");
357
+ pending(
358
+ chord,
359
+ num,
360
+ half === null ? 0 : half
361
+ );
362
+ output.pending = null;
363
+ return parseTerm(input, output);
364
+ };
365
+ var parseChord = (symbol) => {
366
+ const output = parseFormula(new Input(symbol));
367
+ const notes = [...output.value].sort((a, b) => a - b);
368
+ const intervals = [...output.chord].sort((a, b) => a - b);
369
+ const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
370
+ (a, b) => a - b
371
+ );
372
+ return {
373
+ symbol,
374
+ root: toPitchClass(output.pitch),
375
+ notes,
376
+ pitchClasses,
377
+ intervals
378
+ };
379
+ };
380
+ var QUALITY_SOURCE = [
381
+ "",
382
+ // major
383
+ "m",
384
+ // minor
385
+ "7",
386
+ // dominant 7th
387
+ "M7",
388
+ // major 7th
389
+ "m7",
390
+ // minor 7th
391
+ "dim",
392
+ // diminished triad
393
+ "m7b5",
394
+ // half-diminished
395
+ "aug",
396
+ // augmented triad
397
+ "6",
398
+ // major 6th
399
+ "m6",
400
+ // minor 6th
401
+ "sus4",
402
+ "sus2",
403
+ "mM7",
404
+ // minor major 7th
405
+ "dim7",
406
+ // diminished 7th
407
+ "7sus4",
408
+ "7#5",
409
+ // augmented 7th
410
+ "add9",
411
+ "madd9",
412
+ "9",
413
+ "M9",
414
+ "m9",
415
+ "69",
416
+ "m69",
417
+ "5"
418
+ // power chord
419
+ ];
420
+ var QUALITIES = QUALITY_SOURCE.map(
421
+ (quality, priority) => ({
422
+ quality,
423
+ pitchClasses: parseChord(`C${quality}`).pitchClasses,
424
+ priority
425
+ })
426
+ );
427
+ var QUALITY_BY_PCSET = (() => {
428
+ const map = /* @__PURE__ */ new Map();
429
+ for (const def of QUALITIES) {
430
+ const key = def.pitchClasses.join(",");
431
+ if (!map.has(key)) map.set(key, def);
432
+ }
433
+ return map;
434
+ })();
435
+ var MAJOR_PROFILE = [
436
+ 6.35,
437
+ 2.23,
438
+ 3.48,
439
+ 2.33,
440
+ 4.38,
441
+ 4.09,
442
+ 2.52,
443
+ 5.19,
444
+ 2.39,
445
+ 3.66,
446
+ 2.29,
447
+ 2.88
448
+ ];
449
+ var MINOR_PROFILE = [
450
+ 6.33,
451
+ 2.68,
452
+ 3.52,
453
+ 5.38,
454
+ 2.6,
455
+ 3.53,
456
+ 2.54,
457
+ 4.75,
458
+ 3.98,
459
+ 2.69,
460
+ 3.34,
461
+ 3.17
462
+ ];
463
+ var mean = (a) => a.reduce((s, v) => s + v, 0) / a.length;
464
+ var pearson = (a, b) => {
465
+ const ma = mean(a);
466
+ const mb = mean(b);
467
+ let num = 0;
468
+ let da = 0;
469
+ let db = 0;
470
+ for (let i = 0; i < a.length; i++) {
471
+ const x = a[i] - ma;
472
+ const y = b[i] - mb;
473
+ num += x * y;
474
+ da += x * x;
475
+ db += y * y;
476
+ }
477
+ const den = Math.sqrt(da * db);
478
+ return den === 0 ? 0 : num / den;
479
+ };
480
+ var keyName = (tonic, mode, flat) => `${noteName(tonic, flat)} ${mode}`;
481
+ var stripScore = (c) => ({
482
+ tonic: c.tonic,
483
+ mode: c.mode,
484
+ name: c.name
485
+ });
486
+ var sameKey = (a, b) => a.tonic === b.tonic && a.mode === b.mode;
487
+ var buildHistogram = (notes) => {
488
+ const h = new Array(12).fill(0);
489
+ for (const n of notes) {
490
+ if (typeof n === "number") h[toPitchClass(n)] += 1;
491
+ else h[toPitchClass(n.pitch)] += n.duration ?? 1;
492
+ }
493
+ return h;
494
+ };
495
+ var windowHistogram = (notes, start, end) => {
496
+ const h = new Array(12).fill(0);
497
+ for (const n of notes) {
498
+ if (n.duration <= 0) {
499
+ if (n.when >= start && n.when < end) h[toPitchClass(n.pitch)] += 1;
500
+ continue;
501
+ }
502
+ const s = Math.max(n.when, start);
503
+ const e = Math.min(n.when + n.duration, end);
504
+ const overlap = e - s;
505
+ if (overlap > 0) h[toPitchClass(n.pitch)] += overlap;
506
+ }
507
+ return h;
508
+ };
509
+ var rankKeys = (histogram, flat) => {
510
+ const candidates = [];
511
+ for (let tonic = 0; tonic < 12; tonic++) {
512
+ for (const mode of ["major", "minor"]) {
513
+ const profile = mode === "major" ? MAJOR_PROFILE : MINOR_PROFILE;
514
+ const rotated = histogram.map(
515
+ (_, pc) => profile[toPitchClass(pc - tonic)]
516
+ );
517
+ candidates.push({
518
+ tonic,
519
+ mode,
520
+ name: keyName(tonic, mode, flat),
521
+ score: pearson(histogram, rotated)
522
+ });
523
+ }
524
+ }
525
+ candidates.sort((a, b) => b.score - a.score);
526
+ return candidates;
527
+ };
528
+ var detectKey = (notes, options = {}) => {
529
+ if (!notes.length) return [];
530
+ const { flat = false } = options;
531
+ const histogram = buildHistogram(notes);
532
+ if (histogram.every((v) => v === 0)) return [];
533
+ return rankKeys(histogram, flat);
534
+ };
535
+ var coalesce = (segments) => {
536
+ const out = [];
537
+ for (const s of segments) {
538
+ const last = out[out.length - 1];
539
+ if (last && sameKey(last.key, s.key)) {
540
+ last.duration = s.when + s.duration - last.when;
541
+ } else {
542
+ out.push({ ...s });
543
+ }
544
+ }
545
+ return out;
546
+ };
547
+ var mergeShortSegments = (segments, min) => {
548
+ if (min <= 0) return segments;
549
+ const result = segments.map((s) => ({ ...s }));
550
+ let i = 0;
551
+ while (i < result.length && result.length > 1) {
552
+ if (result[i].duration >= min) {
553
+ i++;
554
+ continue;
555
+ }
556
+ if (i > 0) {
557
+ result[i - 1].duration += result[i].duration;
558
+ result.splice(i, 1);
559
+ } else {
560
+ result[i + 1].when = result[i].when;
561
+ result[i + 1].duration += result[i].duration;
562
+ result.splice(i, 1);
563
+ }
564
+ }
565
+ return coalesce(result);
566
+ };
567
+ var detectKeyChanges = (notes, options = {}) => {
568
+ if (!notes.length) return [];
569
+ const { flat = false } = options;
570
+ const start = notes.reduce(
571
+ (m, n) => Math.min(m, n.when),
572
+ Number.POSITIVE_INFINITY
573
+ );
574
+ const end = notes.reduce(
575
+ (m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
576
+ Number.NEGATIVE_INFINITY
577
+ );
578
+ const span = end - start;
579
+ if (span <= 0) {
580
+ const top = detectKey(
581
+ notes.map((n) => ({ pitch: n.pitch, duration: Math.max(n.duration, 1) })),
582
+ { flat }
583
+ )[0];
584
+ return top ? [{ key: stripScore(top), when: start, duration: 0 }] : [];
585
+ }
586
+ const windowSize = options.windowSize ?? span / 4;
587
+ const hopSize = options.hopSize ?? windowSize / 2;
588
+ const minSegmentDuration = options.minSegmentDuration ?? 0;
589
+ const switchMargin = options.switchMargin ?? 0.08;
590
+ const segments = [];
591
+ for (let t = start; t < end - 1e-9; t += hopSize) {
592
+ const regionEnd = Math.min(t + hopSize, end);
593
+ const winEnd = Math.min(t + windowSize, end);
594
+ const winStart = Math.max(start, winEnd - windowSize);
595
+ const histogram = windowHistogram(notes, winStart, winEnd);
596
+ const last = segments[segments.length - 1];
597
+ if (histogram.every((v) => v === 0)) {
598
+ if (last) last.duration = regionEnd - last.when;
599
+ continue;
600
+ }
601
+ const candidates = rankKeys(histogram, flat);
602
+ let chosen = candidates[0];
603
+ if (last) {
604
+ const current = candidates.find((c) => sameKey(c, last.key));
605
+ if (current && chosen.score - current.score <= switchMargin)
606
+ chosen = current;
607
+ }
608
+ if (last && sameKey(last.key, chosen)) {
609
+ last.duration = regionEnd - last.when;
610
+ } else {
611
+ segments.push({
612
+ key: stripScore(chosen),
613
+ when: t,
614
+ duration: regionEnd - t
615
+ });
616
+ }
617
+ }
618
+ return mergeShortSegments(coalesce(segments), minSegmentDuration);
619
+ };
620
+ var toneRoleWeight = (rel) => {
621
+ if (rel === 0) return 1.3;
622
+ if (rel === 3 || rel === 4) return 1.2;
623
+ if (rel === 10 || rel === 11) return 0.95;
624
+ if (rel === 6 || rel === 7 || rel === 8) return 0.7;
625
+ return 0.85;
626
+ };
627
+ var CHORD_TEMPLATES = (() => {
628
+ const templates = [];
629
+ for (let root = 0; root < 12; root++) {
630
+ for (const def of QUALITIES) {
631
+ const pcs = /* @__PURE__ */ new Set();
632
+ const weights = new Array(12).fill(0);
633
+ const rel = /* @__PURE__ */ new Set();
634
+ for (const relPc of def.pitchClasses) {
635
+ rel.add(relPc);
636
+ const pc = toPitchClass(relPc + root);
637
+ pcs.add(pc);
638
+ weights[pc] = toneRoleWeight(relPc);
639
+ }
640
+ templates.push({
641
+ root,
642
+ quality: def.quality,
643
+ priority: def.priority,
644
+ pcs,
645
+ weights,
646
+ rel
647
+ });
648
+ }
649
+ }
650
+ return templates;
651
+ })();
652
+ var MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
653
+ var NATURAL_MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10];
654
+ var scaleOf = (key) => {
655
+ const base = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
656
+ return base.map((d) => toPitchClass(d + key.tonic));
657
+ };
658
+ var keyBonus = (tmpl, key) => {
659
+ const scale = scaleOf(key);
660
+ const scaleSet = new Set(scale);
661
+ const rootDiatonic = scaleSet.has(tmpl.root);
662
+ let allDiatonic = true;
663
+ for (const pc of tmpl.pcs)
664
+ if (!scaleSet.has(pc)) {
665
+ allDiatonic = false;
666
+ break;
667
+ }
668
+ let bonus = 0;
669
+ if (allDiatonic) bonus += 0.25;
670
+ else if (rootDiatonic) bonus += 0.1;
671
+ const degree = toPitchClass(tmpl.root - key.tonic);
672
+ if (degree === 0 || degree === 5 || degree === 7) bonus += 0.05;
673
+ return bonus;
674
+ };
675
+ var makeFrame = (notes, start, end) => {
676
+ const raw = new Array(12).fill(0);
677
+ let total = 0;
678
+ let bassPitch = Number.POSITIVE_INFINITY;
679
+ let bass = -1;
680
+ for (const n of notes) {
681
+ const s = Math.max(n.when, start);
682
+ const e = Math.min(n.when + Math.max(n.duration, 0), end);
683
+ const overlap = n.duration <= 0 ? n.when >= start && n.when < end ? 1 : 0 : Math.max(e - s, 0);
684
+ if (overlap <= 0) continue;
685
+ raw[toPitchClass(n.pitch)] += overlap;
686
+ total += overlap;
687
+ if (n.pitch < bassPitch) {
688
+ bassPitch = n.pitch;
689
+ bass = toPitchClass(n.pitch);
690
+ }
691
+ }
692
+ const profile = total > 0 ? raw.map((v) => v / total) : raw;
693
+ return {
694
+ when: start,
695
+ duration: end - start,
696
+ profile,
697
+ bass,
698
+ empty: total === 0
699
+ };
700
+ };
701
+ var emissionScore = (frame, tmpl, key, ncTonePenalty) => {
702
+ let hit = 0;
703
+ let miss = 0;
704
+ for (let pc = 0; pc < 12; pc++) {
705
+ const w = frame.profile[pc];
706
+ if (w === 0) continue;
707
+ if (tmpl.pcs.has(pc)) hit += w * tmpl.weights[pc];
708
+ else miss += w;
709
+ }
710
+ let score = hit - ncTonePenalty * miss;
711
+ if (frame.profile[tmpl.root] === 0) score -= 0.3;
712
+ if (frame.bass !== -1 && tmpl.root === frame.bass) score += 0.3;
713
+ if (key) score += keyBonus(tmpl, key);
714
+ score -= tmpl.priority * 2e-3;
715
+ return score;
716
+ };
717
+ var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
718
+ var chordDegree = (key, tmpl) => {
719
+ const scale = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
720
+ const rel = toPitchClass(tmpl.root - key.tonic);
721
+ let idx = scale.indexOf(rel);
722
+ let accidental = "";
723
+ if (idx === -1) {
724
+ const below = scale.indexOf(toPitchClass(rel - 1));
725
+ const above = scale.indexOf(toPitchClass(rel + 1));
726
+ if (below !== -1) {
727
+ idx = below;
728
+ accidental = "#";
729
+ } else if (above !== -1) {
730
+ idx = above;
731
+ accidental = "b";
732
+ } else {
733
+ idx = 0;
734
+ accidental = "?";
735
+ }
736
+ }
737
+ const hasM3 = tmpl.rel.has(4);
738
+ const hasm3 = tmpl.rel.has(3);
739
+ const hasDim5 = tmpl.rel.has(6);
740
+ const hasAug5 = tmpl.rel.has(8);
741
+ const hasMin7 = tmpl.rel.has(10);
742
+ let numeral = ROMAN[idx];
743
+ let suffix = "";
744
+ if (hasm3 && hasDim5) {
745
+ numeral = numeral.toLowerCase();
746
+ suffix = hasMin7 ? "\xF87" : "\xB0";
747
+ if (tmpl.rel.has(9)) suffix = "\xB07";
748
+ } else if (hasM3 && hasAug5) {
749
+ suffix = "+";
750
+ } else if (hasm3) {
751
+ numeral = numeral.toLowerCase();
752
+ } else if (!hasM3) {
753
+ }
754
+ if (!suffix) {
755
+ if (tmpl.rel.has(11)) suffix = "M7";
756
+ else if (hasMin7) suffix = "7";
757
+ else if (tmpl.rel.has(9) && !tmpl.rel.has(10)) suffix = "6";
758
+ }
759
+ return accidental + numeral + suffix;
760
+ };
761
+ var viterbi = (emissions, changePenalty) => {
762
+ const T = emissions.length;
763
+ const N = CHORD_TEMPLATES.length;
764
+ if (T === 0) return [];
765
+ const back = Array.from(
766
+ { length: T },
767
+ () => new Array(N).fill(-1)
768
+ );
769
+ let prev = emissions[0].slice();
770
+ for (let t = 1; t < T; t++) {
771
+ let bestPrevVal = Number.NEGATIVE_INFINITY;
772
+ let bestPrevIdx = 0;
773
+ for (let j = 0; j < N; j++)
774
+ if (prev[j] > bestPrevVal) {
775
+ bestPrevVal = prev[j];
776
+ bestPrevIdx = j;
777
+ }
778
+ const curr = new Array(N).fill(0);
779
+ const em = emissions[t];
780
+ const switchVal = bestPrevVal - changePenalty;
781
+ for (let i = 0; i < N; i++) {
782
+ if (prev[i] >= switchVal) {
783
+ curr[i] = em[i] + prev[i];
784
+ back[t][i] = i;
785
+ } else {
786
+ curr[i] = em[i] + switchVal;
787
+ back[t][i] = bestPrevIdx;
788
+ }
789
+ }
790
+ prev = curr;
791
+ }
792
+ let bestIdx = 0;
793
+ for (let i = 1; i < N; i++) if (prev[i] > prev[bestIdx]) bestIdx = i;
794
+ const path = new Array(T).fill(0);
795
+ path[T - 1] = bestIdx;
796
+ for (let t = T - 1; t > 0; t--) path[t - 1] = back[t][path[t]];
797
+ return path;
798
+ };
799
+ var keyAt = (keys, when) => {
800
+ for (const k of keys)
801
+ if (when >= k.when && when < k.when + k.duration) return k.key;
802
+ return keys.length ? keys[keys.length - 1].key : null;
803
+ };
804
+ var buildSymbol = (tmpl, bass, flat) => {
805
+ const rootSymbol = noteName(tmpl.root, flat) + tmpl.quality;
806
+ const inversion = bass !== -1 && bass !== tmpl.root && tmpl.pcs.has(bass);
807
+ return {
808
+ symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
809
+ rootSymbol,
810
+ inversion,
811
+ bass: bass === -1 ? tmpl.root : bass
812
+ };
813
+ };
814
+ var detectProgression = (notes, options = {}) => {
815
+ if (!notes.length) return { keys: [], chords: [] };
61
816
  const {
62
- chordStr,
63
- patternType,
64
- rootShift,
817
+ flat = false,
65
818
  bpm,
66
- stepsPerBar,
67
- parseChord,
68
- parseChords
819
+ frameSize = 0.5,
820
+ changePenalty = 0.4,
821
+ nonChordTonePenalty = 0.55,
822
+ useKey = true
69
823
  } = options;
824
+ const keys = detectKeyChanges(notes, options);
825
+ const start = notes.reduce(
826
+ (m, n) => Math.min(m, n.when),
827
+ Number.POSITIVE_INFINITY
828
+ );
829
+ const end = notes.reduce(
830
+ (m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
831
+ Number.NEGATIVE_INFINITY
832
+ );
833
+ if (end <= start) return { keys, chords: [] };
834
+ const frameDur = bpm ? 60 / bpm : Math.max(frameSize, 1e-3);
835
+ const frames = [];
836
+ for (let t = start; t < end - 1e-9; t += frameDur)
837
+ frames.push(makeFrame(notes, t, Math.min(t + frameDur, end)));
838
+ const emissions = frames.map((frame) => {
839
+ if (frame.empty) return new Array(CHORD_TEMPLATES.length).fill(0);
840
+ const key = useKey ? keyAt(keys, frame.when + frame.duration / 2) : null;
841
+ return CHORD_TEMPLATES.map(
842
+ (tmpl) => emissionScore(frame, tmpl, key, nonChordTonePenalty)
843
+ );
844
+ });
845
+ const path = viterbi(emissions, changePenalty);
846
+ const chords = [];
847
+ for (let t = 0; t < frames.length; t++) {
848
+ const frame = frames[t];
849
+ const tmpl = CHORD_TEMPLATES[path[t]];
850
+ const last = chords[chords.length - 1];
851
+ const sameAsLast = last && last.root === tmpl.root && last.quality === tmpl.quality;
852
+ if (sameAsLast) {
853
+ last.duration = frame.when + frame.duration - last.when;
854
+ continue;
855
+ }
856
+ const key = keyAt(keys, frame.when + frame.duration / 2);
857
+ const { symbol, rootSymbol, inversion, bass } = buildSymbol(
858
+ tmpl,
859
+ frame.bass,
860
+ flat
861
+ );
862
+ chords.push({
863
+ symbol,
864
+ rootSymbol,
865
+ root: tmpl.root,
866
+ quality: tmpl.quality,
867
+ bass,
868
+ inversion,
869
+ when: frame.when,
870
+ duration: frame.duration,
871
+ key,
872
+ degree: key ? chordDegree(key, tmpl) : null
873
+ });
874
+ }
875
+ return { keys, chords };
876
+ };
877
+ var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
878
+ var parseChords = (str, bpm = 120) => {
879
+ const output = [];
880
+ const secBar = 60 / bpm * 4;
881
+ const frontChars = new Set("ABCDEFG_=%N");
882
+ let idx = 0;
883
+ let last = null;
884
+ for (const line of toHan(str).split("\n").map((v) => v.trim())) {
885
+ if (!line.length || /^#/.test(line)) continue;
886
+ for (const str2 of line.split(/[|ll→]/)) {
887
+ if (!str2.length) continue;
888
+ const when = idx++ * secBar;
889
+ const a = [];
890
+ for (let i = 0; i < str2.length; i++) {
891
+ const char = str2[i];
892
+ const prev = str2[i - 1];
893
+ const prev2 = str2.slice(i - 2, i);
894
+ if (!frontChars.has(char)) continue;
895
+ if (prev === "/" || prev2 === "on") continue;
896
+ if (prev2 === "N." && char === "C") continue;
897
+ a.push(i);
898
+ }
899
+ if (!a.length) continue;
900
+ const divide = 2 ** Math.ceil(Math.log2(a.length));
901
+ const unitTime = secBar / divide;
902
+ for (const [i, v] of a.entries()) {
903
+ const s = str2.slice(v, i === a.length - 1 ? str2.length : a[i + 1]).replace(/\s+/g, "");
904
+ const c = s[0];
905
+ if (c === "_" || c === "N") {
906
+ last = null;
907
+ continue;
908
+ }
909
+ if (c === "=") {
910
+ if (last) last.duration += unitTime;
911
+ continue;
912
+ }
913
+ const _when = when + i * unitTime;
914
+ if (c === "%") {
915
+ if (last === null) continue;
916
+ const base = last;
917
+ last = { ...base, when: _when, duration: unitTime };
918
+ } else {
919
+ const key = s.slice(0, s[1] === "#" ? 2 : 1);
920
+ const chord = s.slice(key.length).replace(/[\s・]/g, "");
921
+ last = {
922
+ key,
923
+ chord,
924
+ when: _when,
925
+ duration: unitTime
926
+ };
927
+ }
928
+ output.push(last);
929
+ }
930
+ if (last !== null && divide > a.length)
931
+ last.duration += unitTime * (divide - a.length);
932
+ }
933
+ }
934
+ return output;
935
+ };
936
+
937
+ // src/chords.ts
938
+ var C3 = 48;
939
+ var buildChordPlacements = (options) => {
940
+ const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
70
941
  const placements = [];
71
942
  if (!chordStr.trim()) return placements;
72
943
  const offset = rootShift;
@@ -96,7 +967,7 @@ var buildChordPlacements = (options) => {
96
967
  for (const chord of group) {
97
968
  let notes;
98
969
  try {
99
- notes = [...parseChord(`${chord.key}${chord.chord}`).value];
970
+ notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
100
971
  } catch {
101
972
  continue;
102
973
  }
@@ -183,7 +1054,7 @@ var buildChordPlacements = (options) => {
183
1054
  chordNames.forEach((chordName, barIndex) => {
184
1055
  let notes;
185
1056
  try {
186
- notes = [...parseChord(chordName).value];
1057
+ notes = [...parseChord(chordName).notes];
187
1058
  } catch {
188
1059
  return;
189
1060
  }
@@ -257,6 +1128,7 @@ var buildUI = (target, options) => {
257
1128
  <button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
258
1129
  <button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
259
1130
  <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
1131
+ <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
260
1132
  <span class="dtm-grow"></span>
261
1133
  <span class="dtm-label">BPM</span>
262
1134
  <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
@@ -349,10 +1221,10 @@ var buildUI = (target, options) => {
349
1221
  <div class="dtm-row dtm-hidden" data-dtm="midi-track-selection"></div>
350
1222
  <div class="dtm-row">
351
1223
  <span class="dtm-label">MML</span>
352
- <textarea class="dtm-textarea" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1224
+ <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1225
+ <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">\u8AAD\u8FBC</button>
353
1226
  </div>
354
1227
  <div class="dtm-row">
355
- <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">MML\u8AAD\u8FBC</button>
356
1228
  <span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
357
1229
  <select class="dtm-select" data-dtm="shift-select">
358
1230
  <option value="-96">-2\u5206</option>
@@ -426,6 +1298,8 @@ var buildUI = (target, options) => {
426
1298
  const sel = (name) => q(root, `[data-dtm="${name}"]`);
427
1299
  return {
428
1300
  root,
1301
+ topbar: sel("transport"),
1302
+ topbarLoading: sel("topbar-loading"),
429
1303
  playBtn: sel("play"),
430
1304
  recBtn: sel("rec"),
431
1305
  soloCheckbox: sel("solo"),
@@ -915,6 +1789,7 @@ var DEFAULT_PAN = 64;
915
1789
  var DEFAULT_VELOCITY = 100;
916
1790
  var DEFAULT_PLAYBACK_VELOCITY = 127;
917
1791
  var DEFAULT_STEPS_PER_BAR = 192;
1792
+ var MML_END_MARKER = "#end;";
918
1793
 
919
1794
  // src/lyrics.ts
920
1795
  var kanaTable = {
@@ -1643,8 +2518,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
1643
2518
  ))().then((v) => {
1644
2519
  loaded.set(m, v);
1645
2520
  return v;
1646
- }).catch((err) => {
1647
- console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err);
2521
+ }).catch((err2) => {
2522
+ console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err2);
1648
2523
  return null;
1649
2524
  });
1650
2525
  loading.set(m, p);
@@ -3047,7 +3922,9 @@ var parseMML = (mml, options = {}) => {
3047
3922
  const meta = parseMmlMeta(noComments);
3048
3923
  const noMeta = stripMmlMeta(noComments);
3049
3924
  const lyrics = collectLyrics ? parseLyrics(noMeta) : void 0;
3050
- const fullMML = stripLyrics(noMeta).replace(/[\n\r]+/g, " ").trim();
3925
+ const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
3926
+ const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
3927
+ const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
3051
3928
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
3052
3929
  let trackIndex = 0;
3053
3930
  let octave = 4;
@@ -3136,8 +4013,10 @@ var parseMML = (mml, options = {}) => {
3136
4013
  numStr += body[j];
3137
4014
  j++;
3138
4015
  }
3139
- if (ch === "t" && trackIndex === 0 && numStr) {
3140
- bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
4016
+ if (ch === "t" && numStr) {
4017
+ if (bpm === null) {
4018
+ bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
4019
+ }
3141
4020
  }
3142
4021
  pushTok("ctrl", currentStep, 0, tokStart);
3143
4022
  } else if (ch === "[") {
@@ -3639,6 +4518,7 @@ var DAW_CSS = `
3639
4518
  }
3640
4519
  .dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
3641
4520
  .dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
4521
+ .dtm-textarea.dtm-grow { width: 0; }
3642
4522
  .dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
3643
4523
 
3644
4524
  /* \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 */
@@ -3806,7 +4686,7 @@ var DAW_CSS = `
3806
4686
 
3807
4687
  /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
3808
4688
  .dtm-overlay {
3809
- position: absolute; inset: 0; z-index: 1000;
4689
+ position: absolute; inset: 0; z-index: 10;
3810
4690
  background: rgba(0,0,0,.92);
3811
4691
  display: flex; align-items: center; justify-content: center;
3812
4692
  flex-direction: column; gap: 14px;
@@ -3855,6 +4735,22 @@ var DAW_CSS = `
3855
4735
  letter-spacing: .15em;
3856
4736
  min-height: 1em;
3857
4737
  }
4738
+ .dtm-topbar-loading {
4739
+ display: none;
4740
+ font-family: var(--dtm-font);
4741
+ font-size: 11px;
4742
+ color: var(--dtm-primary);
4743
+ margin-left: 12px;
4744
+ letter-spacing: .15em;
4745
+ align-self: center;
4746
+ }
4747
+ .dtm-topbar.is-loading .dtm-topbar-loading {
4748
+ display: inline-block;
4749
+ }
4750
+ .dtm-topbar.is-loading {
4751
+ pointer-events: none;
4752
+ opacity: 0.7;
4753
+ }
3858
4754
 
3859
4755
  @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
3860
4756
  .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
@@ -3930,6 +4826,14 @@ var DAW_CSS = `
3930
4826
  min-width: 2em;
3931
4827
  margin-left: 4px;
3932
4828
  }
4829
+ .dtm-player-chord {
4830
+ font-family: 'k8x12', monospace;
4831
+ font-size: 11px;
4832
+ color: var(--dtm-accent);
4833
+ min-width: 4em;
4834
+ margin-left: 8px;
4835
+ font-weight: bold;
4836
+ }
3933
4837
  .dtm-player-dots {
3934
4838
  margin-left: auto;
3935
4839
  display: flex;
@@ -4280,7 +5184,7 @@ var mountDAW = (target, options = {}) => {
4280
5184
  const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
4281
5185
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
4282
5186
  const showMidi = !!options.parseMidi;
4283
- const showChord = !!(options.parseChord && options.parseChords);
5187
+ const showChord = true;
4284
5188
  const refs = buildUI(target, {
4285
5189
  tracks: trackConfigs,
4286
5190
  drumPatternNames: Object.keys(drumPatterns),
@@ -4976,13 +5880,15 @@ var mountDAW = (target, options = {}) => {
4976
5880
  const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
4977
5881
  if (streaming && voices) {
4978
5882
  const overlay = showLoadingOverlay(refs.rollContainer);
5883
+ setLoading(true);
4979
5884
  try {
4980
5885
  await voices.loadModels(streamTracks.map((t) => t.model));
4981
5886
  await voices.warm(streamTracks);
4982
- } catch (err) {
4983
- console.warn("[dtm] voice preload failed", err);
5887
+ } catch (err2) {
5888
+ console.warn("[dtm] voice preload failed", err2);
4984
5889
  } finally {
4985
5890
  overlay.remove();
5891
+ setLoading(false);
4986
5892
  }
4987
5893
  }
4988
5894
  if (playbackState !== "paused") {
@@ -5321,8 +6227,8 @@ var mountDAW = (target, options = {}) => {
5321
6227
  const decomposedMini = monoTracks.map(
5322
6228
  (notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
5323
6229
  );
5324
- const full2 = [metaLine, ...decomposedFull].filter((s) => s.length > 0).join(";\n");
5325
- const minified2 = [metaLine, ...decomposedMini].filter((s) => s.length > 0).join(";");
6230
+ const full2 = [metaLine, ...decomposedFull, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
6231
+ const minified2 = [metaLine, ...decomposedMini, MML_END_MARKER].filter((s) => s.length > 0).join(";");
5326
6232
  return {
5327
6233
  full: full2,
5328
6234
  minified: minified2,
@@ -5331,21 +6237,28 @@ var mountDAW = (target, options = {}) => {
5331
6237
  barLimit: barLimitBars
5332
6238
  };
5333
6239
  }
5334
- const trackLines = trackStates.map(
5335
- (t, i) => `@${i} ${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim()}`
5336
- );
5337
- const trackLinesMini = trackStates.map(
5338
- (t, i) => `@${i}${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim().replace(/\s+/g, "")}`
5339
- );
6240
+ const trackLines = [];
6241
+ const trackLinesMini = [];
6242
+ trackStates.forEach((t, i) => {
6243
+ const notes = clipNotes(t.core.getNotes());
6244
+ if (notes.length > 0) {
6245
+ const mml = t.core.getMMLFromNotes(notes, bpm, t.volume).trim();
6246
+ trackLines.push(`@${i} ${mml}`);
6247
+ trackLinesMini.push(`@${i}${mml.replace(/\s+/g, "")}`);
6248
+ }
6249
+ });
5340
6250
  const lyricLines = trackStates.map((t, i) => ({
5341
6251
  i,
5342
- text: t.lyrics.trim(),
6252
+ notes: clipNotes(t.core.getNotes()),
6253
+ text: t.lyrics.replace(/[\r\n]+/g, " ").trim(),
5343
6254
  model: t.lyricModel.trim(),
5344
6255
  vol: t.vocalVolume,
5345
6256
  gate: t.vocalGate,
5346
6257
  pan: t.vocalPan,
5347
6258
  oct: t.vocalOctave
5348
- })).filter((x) => x.model.length > 0 && x.text.length > 0).map((x) => {
6259
+ })).filter(
6260
+ (x) => x.model.length > 0 && x.text.length > 0 && x.notes.length > 0
6261
+ ).map((x) => {
5349
6262
  const params = [
5350
6263
  x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
5351
6264
  x.gate === 100 ? "" : `q${x.gate}`,
@@ -5355,13 +6268,18 @@ var mountDAW = (target, options = {}) => {
5355
6268
  const head = params ? `${x.model} ${params}` : x.model;
5356
6269
  return `@@${x.i} ${head} ${x.text}`;
5357
6270
  });
5358
- const full = [metaLine, ...trackLines, ...lyricLines].filter((s) => s.length > 0).join(";\n");
5359
- const minified = [metaLine, ...trackLinesMini, ...lyricLines].filter((s) => s.length > 0).join(";");
6271
+ const full = [metaLine, ...trackLines, ...lyricLines, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
6272
+ const minified = [
6273
+ metaLine,
6274
+ ...trackLinesMini,
6275
+ ...lyricLines,
6276
+ MML_END_MARKER
6277
+ ].filter((s) => s.length > 0).join(";");
5360
6278
  return {
5361
6279
  full,
5362
6280
  minified,
5363
6281
  ignoredCount: 0,
5364
- trackCount: trackStates.length,
6282
+ trackCount: trackLines.length,
5365
6283
  barLimit: barLimitBars
5366
6284
  };
5367
6285
  };
@@ -5481,7 +6399,6 @@ var mountDAW = (target, options = {}) => {
5481
6399
  updateUndoRedo();
5482
6400
  };
5483
6401
  const applyChord = () => {
5484
- if (!options.parseChord || !options.parseChords) return;
5485
6402
  const active = getActive();
5486
6403
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
5487
6404
  if (!chordTrack) return;
@@ -5490,9 +6407,7 @@ var mountDAW = (target, options = {}) => {
5490
6407
  patternType: active.savedChordPattern,
5491
6408
  rootShift: active.savedChordRoot,
5492
6409
  bpm,
5493
- stepsPerBar: renderConfig.stepsPerBar,
5494
- parseChord: options.parseChord,
5495
- parseChords: options.parseChords
6410
+ stepsPerBar: renderConfig.stepsPerBar
5496
6411
  });
5497
6412
  chordTrack.core.clearNotesWithoutHistory();
5498
6413
  chordTrack.core.beginBatch();
@@ -5577,9 +6492,11 @@ var mountDAW = (target, options = {}) => {
5577
6492
  };
5578
6493
  const overlayDuring = (fn) => {
5579
6494
  refs.overlay.hidden = false;
6495
+ setLoading(true);
5580
6496
  setTimeout(() => {
5581
6497
  fn();
5582
6498
  refs.overlay.hidden = true;
6499
+ setLoading(false);
5583
6500
  }, 30);
5584
6501
  };
5585
6502
  const wireEvents = () => {
@@ -5723,6 +6640,7 @@ var mountDAW = (target, options = {}) => {
5723
6640
  const file = refs.midiInput.files?.[0];
5724
6641
  if (!file || !options.parseMidi) return;
5725
6642
  refs.overlay.hidden = false;
6643
+ setLoading(true);
5726
6644
  const buffer = new Uint8Array(await file.arrayBuffer());
5727
6645
  pendingMidi = options.parseMidi(buffer);
5728
6646
  detectedTracks = analyzeMidiTracks(pendingMidi);
@@ -5743,6 +6661,7 @@ var mountDAW = (target, options = {}) => {
5743
6661
  });
5744
6662
  refs.midiTrackSelection.classList.remove("dtm-hidden");
5745
6663
  refs.overlay.hidden = true;
6664
+ setLoading(false);
5746
6665
  });
5747
6666
  refs.midiLoadBtn.addEventListener("click", () => {
5748
6667
  if (!pendingMidi) return;
@@ -5816,6 +6735,9 @@ var mountDAW = (target, options = {}) => {
5816
6735
  resizeObserver.observe(refs.rollContainer);
5817
6736
  document.addEventListener("pointermove", onPointerMove);
5818
6737
  document.addEventListener("pointerup", onPointerUp);
6738
+ const setLoading = (loading) => {
6739
+ refs.topbar.classList.toggle("is-loading", loading);
6740
+ };
5819
6741
  return {
5820
6742
  play,
5821
6743
  pause,
@@ -5853,6 +6775,7 @@ var mountDAW = (target, options = {}) => {
5853
6775
  exportMIDI: exportMIDI2,
5854
6776
  setBpm,
5855
6777
  getPlaybackState: () => playbackState,
6778
+ setLoading,
5856
6779
  destroy: () => {
5857
6780
  sequencer.stop();
5858
6781
  resizeObserver.disconnect();
@@ -5997,6 +6920,36 @@ var mountMmlPlayer = (target, mml, options = {}) => {
5997
6920
  const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
5998
6921
  (a, b) => a - b
5999
6922
  );
6923
+ const maxStep = placements.reduce(
6924
+ (max, p) => Math.max(max, p.startStep + p.durationSteps),
6925
+ 0
6926
+ );
6927
+ const timedNotes = placements.map((p) => ({
6928
+ pitch: p.pitch,
6929
+ when: p.startStep * secondsPerStep,
6930
+ duration: p.durationSteps * secondsPerStep
6931
+ }));
6932
+ const stepChords = [];
6933
+ if (timedNotes.length > 0) {
6934
+ let chordSegments = [];
6935
+ try {
6936
+ chordSegments = detectProgression(timedNotes, { bpm }).chords;
6937
+ } catch {
6938
+ chordSegments = [];
6939
+ }
6940
+ for (const seg of chordSegments) {
6941
+ const startStep = Math.max(0, Math.round(seg.when / secondsPerStep));
6942
+ const endStep = Math.round((seg.when + seg.duration) / secondsPerStep);
6943
+ for (let s = startStep; s < endStep && s <= maxStep; s++) {
6944
+ stepChords[s] = seg.symbol;
6945
+ }
6946
+ }
6947
+ let lastChord = "";
6948
+ for (let s = 0; s <= maxStep; s++) {
6949
+ if (stepChords[s]) lastChord = stepChords[s];
6950
+ else stepChords[s] = lastChord;
6951
+ }
6952
+ }
6000
6953
  const seqTracks = trackIndices.map((index) => {
6001
6954
  let id = 0;
6002
6955
  const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
@@ -6197,6 +7150,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6197
7150
  barEl.className = "dtm-player-bar";
6198
7151
  barEl.textContent = "-";
6199
7152
  beatRow.appendChild(barEl);
7153
+ const chordEl = doc.createElement("span");
7154
+ chordEl.className = "dtm-player-chord";
7155
+ chordEl.textContent = "";
7156
+ beatRow.appendChild(chordEl);
6200
7157
  const chips = [];
6201
7158
  const makeChip = (label) => {
6202
7159
  const chip = doc.createElement("span");
@@ -6340,10 +7297,20 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6340
7297
  lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
6341
7298
  };
6342
7299
  const renderPlayhead = (step) => {
7300
+ const intStep = Math.floor(step);
6343
7301
  const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
6344
7302
  for (let i = 0; i < 4; i++)
6345
7303
  beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
6346
7304
  barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
7305
+ const chordName = stepChords[intStep] ?? "";
7306
+ if (chordEl.textContent !== chordName) {
7307
+ chordEl.textContent = chordName;
7308
+ if (chordName) {
7309
+ console.log(
7310
+ `[dtm-player-chord] Active Chord: ${chordName} (step: ${intStep})`
7311
+ );
7312
+ }
7313
+ }
6347
7314
  for (const view of laneViews) {
6348
7315
  let active = null;
6349
7316
  for (const t of view.tokens) {
@@ -6357,6 +7324,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6357
7324
  const resetPlayhead = () => {
6358
7325
  for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
6359
7326
  barEl.textContent = "-";
7327
+ chordEl.textContent = "";
6360
7328
  for (const view of laneViews) {
6361
7329
  for (const t of view.tokens) t.el.classList.remove("is-active");
6362
7330
  view.lane.scrollLeft = 0;
@@ -6420,6 +7388,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6420
7388
  });
6421
7389
  }
6422
7390
  return {
7391
+ id: String(index),
6423
7392
  model: lt.model,
6424
7393
  volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
6425
7394
  pan: panToStereo(lt.pan ?? DEFAULT_PAN),
@@ -6435,15 +7404,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6435
7404
  try {
6436
7405
  await v.loadModels(tracks.map((t) => t.model));
6437
7406
  await v.warm(tracks);
6438
- } catch (err) {
6439
- console.warn("[dtm] voice preload failed", err);
7407
+ } catch (err2) {
7408
+ console.warn("[dtm] voice preload failed", err2);
6440
7409
  } finally {
6441
7410
  overlay.remove();
6442
7411
  }
6443
7412
  if (!playing || activePlayer !== instance) return;
6444
7413
  }
6445
7414
  seq.start(0);
6446
- if (streaming) ensureVoices().startStream(tracks, seq.getStartTime());
7415
+ if (streaming) {
7416
+ ensureVoices().startStream(tracks, seq.getStartTime(), {
7417
+ isAudible: (t) => !mutedTracks.has(Number(t.id))
7418
+ });
7419
+ }
6447
7420
  };
6448
7421
  const play = () => {
6449
7422
  if (playing || trackIndices.length === 0) return;
@@ -6777,8 +7750,6 @@ var DEFAULT_CDN = {
6777
7750
  soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
6778
7751
  soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
6779
7752
  soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs",
6780
- parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs",
6781
- parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs",
6782
7753
  midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
6783
7754
  };
6784
7755
  var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
@@ -6822,24 +7793,6 @@ var createDtmStudio = async (options = {}) => {
6822
7793
  eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
6823
7794
  eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
6824
7795
  ]);
6825
- let parseChord = eng.parseChord;
6826
- let parseChords = eng.parseChords;
6827
- if (features.chord && (!parseChord || !parseChords)) {
6828
- try {
6829
- [parseChord, parseChords] = await Promise.all([
6830
- parseChord ?? importFrom(
6831
- cdn.parseChord,
6832
- "parseChord"
6833
- ),
6834
- parseChords ?? importFrom(
6835
- cdn.parseChords,
6836
- "parseChords"
6837
- )
6838
- ]);
6839
- } catch (e) {
6840
- console.warn("[dtm] \u30B3\u30FC\u30C9\u89E3\u6790\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F", e);
6841
- }
6842
- }
6843
7796
  let midiParser = null;
6844
7797
  let parseMidi;
6845
7798
  if (features.midi) {
@@ -7025,8 +7978,6 @@ var createDtmStudio = async (options = {}) => {
7025
7978
  onPlayNote: playNote,
7026
7979
  onPlayDrum: playDrum,
7027
7980
  singingVoices,
7028
- parseChord,
7029
- parseChords,
7030
7981
  parseMidi,
7031
7982
  onToggleRecord,
7032
7983
  ...dawOverrides
@@ -7048,15 +7999,32 @@ var createDtmStudio = async (options = {}) => {
7048
7999
  editorPresetSelects.set(target, select);
7049
8000
  select.addEventListener("change", async () => {
7050
8001
  if (!select) return;
7051
- daw.setInstrument(select.value);
7052
- await loadPreset(select.value, trackIds);
8002
+ const wasPlaying = daw.getPlaybackState() === "playing";
8003
+ if (wasPlaying) {
8004
+ daw.pause();
8005
+ }
8006
+ const overlay = showLoadingOverlay(target);
8007
+ daw.setLoading?.(true);
8008
+ try {
8009
+ daw.setInstrument(select.value);
8010
+ await loadPreset(select.value, trackIds);
8011
+ } finally {
8012
+ overlay.remove();
8013
+ daw.setLoading?.(false);
8014
+ if (wasPlaying) {
8015
+ daw.play();
8016
+ }
8017
+ }
7053
8018
  });
7054
8019
  }
7055
8020
  const daw = mountDAW(target, base);
7056
8021
  mountedEditors.push(daw);
7057
8022
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7058
8023
  daw.setInstrument(presetKey);
7059
- void loadPreset(presetKey, trackIds);
8024
+ daw.setLoading?.(true);
8025
+ void loadPreset(presetKey, trackIds).finally(() => {
8026
+ daw.setLoading?.(false);
8027
+ });
7060
8028
  const destroy = () => {
7061
8029
  daw.destroy();
7062
8030
  select?.remove();
@@ -7124,6 +8092,7 @@ export {
7124
8092
  LinkedList,
7125
8093
  MAX_VOCAL_VOLUME,
7126
8094
  MMLCore,
8095
+ MML_END_MARKER,
7127
8096
  PITCH_MAP,
7128
8097
  PREWARM_NOTES,
7129
8098
  TRACKS_ADVANCED,