@onjmin/dtm 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -55,18 +55,534 @@ async function buildNameToKeyMapping() {
55
55
  return nameToKey;
56
56
  }
57
57
 
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 detectChord = (notes, options = {}) => {
436
+ if (!notes.length) return [];
437
+ const { flat = false } = options;
438
+ const pcs = [...new Set(notes.map(toPitchClass))].sort((a, b) => a - b);
439
+ const bass = toPitchClass(
440
+ options.bass ?? notes.reduce((m, v) => Math.min(m, v), notes[0])
441
+ );
442
+ const scored = [];
443
+ for (const root of pcs) {
444
+ const relSet = new Set(pcs.map((pc) => toPitchClass(pc - root)));
445
+ for (const def of QUALITY_BY_PCSET.values()) {
446
+ let matchCount = 0;
447
+ let hasRoot = false;
448
+ for (const pc of def.pitchClasses) {
449
+ if (relSet.has(pc)) {
450
+ matchCount++;
451
+ if (pc === 0) hasRoot = true;
452
+ }
453
+ }
454
+ if (!hasRoot) continue;
455
+ if (matchCount < Math.min(2, def.pitchClasses.length)) continue;
456
+ const missingCount = def.pitchClasses.length - matchCount;
457
+ let extraCount = 0;
458
+ const defSet = new Set(def.pitchClasses);
459
+ for (const pc of relSet) {
460
+ if (!defSet.has(pc)) {
461
+ extraCount++;
462
+ }
463
+ }
464
+ const score = matchCount * 2 - missingCount * 1 - extraCount * 0.5;
465
+ const rootSymbol = noteName(root, flat) + def.quality;
466
+ const inversion = root !== bass;
467
+ scored.push({
468
+ score,
469
+ priority: def.priority,
470
+ candidate: {
471
+ symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
472
+ rootSymbol,
473
+ root,
474
+ quality: def.quality,
475
+ bass,
476
+ inversion
477
+ }
478
+ });
479
+ }
480
+ }
481
+ scored.sort((a, b) => {
482
+ if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
483
+ if (a.candidate.inversion !== b.candidate.inversion)
484
+ return a.candidate.inversion ? 1 : -1;
485
+ if (a.priority !== b.priority) return a.priority - b.priority;
486
+ return a.candidate.root - b.candidate.root;
487
+ });
488
+ return scored.map((s) => s.candidate);
489
+ };
490
+ var toneRoleWeight = (rel) => {
491
+ if (rel === 0) return 1.3;
492
+ if (rel === 3 || rel === 4) return 1.2;
493
+ if (rel === 10 || rel === 11) return 0.95;
494
+ if (rel === 6 || rel === 7 || rel === 8) return 0.7;
495
+ return 0.85;
496
+ };
497
+ var CHORD_TEMPLATES = (() => {
498
+ const templates = [];
499
+ for (let root = 0; root < 12; root++) {
500
+ for (const def of QUALITIES) {
501
+ const pcs = /* @__PURE__ */ new Set();
502
+ const weights = new Array(12).fill(0);
503
+ const rel = /* @__PURE__ */ new Set();
504
+ for (const relPc of def.pitchClasses) {
505
+ rel.add(relPc);
506
+ const pc = toPitchClass(relPc + root);
507
+ pcs.add(pc);
508
+ weights[pc] = toneRoleWeight(relPc);
509
+ }
510
+ templates.push({
511
+ root,
512
+ quality: def.quality,
513
+ priority: def.priority,
514
+ pcs,
515
+ weights,
516
+ rel
517
+ });
518
+ }
519
+ }
520
+ return templates;
521
+ })();
522
+ var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
523
+ var parseChords = (str, bpm = 120) => {
524
+ const output = [];
525
+ const secBar = 60 / bpm * 4;
526
+ const frontChars = new Set("ABCDEFG_=%N");
527
+ let idx = 0;
528
+ let last = null;
529
+ for (const line of toHan(str).split("\n").map((v) => v.trim())) {
530
+ if (!line.length || /^#/.test(line)) continue;
531
+ for (const str2 of line.split(/[|ll→]/)) {
532
+ if (!str2.length) continue;
533
+ const when = idx++ * secBar;
534
+ const a = [];
535
+ for (let i = 0; i < str2.length; i++) {
536
+ const char = str2[i];
537
+ const prev = str2[i - 1];
538
+ const prev2 = str2.slice(i - 2, i);
539
+ if (!frontChars.has(char)) continue;
540
+ if (prev === "/" || prev2 === "on") continue;
541
+ if (prev2 === "N." && char === "C") continue;
542
+ a.push(i);
543
+ }
544
+ if (!a.length) continue;
545
+ const divide = 2 ** Math.ceil(Math.log2(a.length));
546
+ const unitTime = secBar / divide;
547
+ for (const [i, v] of a.entries()) {
548
+ const s = str2.slice(v, i === a.length - 1 ? str2.length : a[i + 1]).replace(/\s+/g, "");
549
+ const c = s[0];
550
+ if (c === "_" || c === "N") {
551
+ last = null;
552
+ continue;
553
+ }
554
+ if (c === "=") {
555
+ if (last) last.duration += unitTime;
556
+ continue;
557
+ }
558
+ const _when = when + i * unitTime;
559
+ if (c === "%") {
560
+ if (last === null) continue;
561
+ const base = last;
562
+ last = { ...base, when: _when, duration: unitTime };
563
+ } else {
564
+ const key = s.slice(0, s[1] === "#" ? 2 : 1);
565
+ const chord = s.slice(key.length).replace(/[\s・]/g, "");
566
+ last = {
567
+ key,
568
+ chord,
569
+ when: _when,
570
+ duration: unitTime
571
+ };
572
+ }
573
+ output.push(last);
574
+ }
575
+ if (last !== null && divide > a.length)
576
+ last.duration += unitTime * (divide - a.length);
577
+ }
578
+ }
579
+ return output;
580
+ };
581
+
58
582
  // src/chords.ts
59
583
  var C3 = 48;
60
584
  var buildChordPlacements = (options) => {
61
- const {
62
- chordStr,
63
- patternType,
64
- rootShift,
65
- bpm,
66
- stepsPerBar,
67
- parseChord,
68
- parseChords
69
- } = options;
585
+ const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
70
586
  const placements = [];
71
587
  if (!chordStr.trim()) return placements;
72
588
  const offset = rootShift;
@@ -96,7 +612,7 @@ var buildChordPlacements = (options) => {
96
612
  for (const chord of group) {
97
613
  let notes;
98
614
  try {
99
- notes = [...parseChord(`${chord.key}${chord.chord}`).value];
615
+ notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
100
616
  } catch {
101
617
  continue;
102
618
  }
@@ -183,7 +699,7 @@ var buildChordPlacements = (options) => {
183
699
  chordNames.forEach((chordName, barIndex) => {
184
700
  let notes;
185
701
  try {
186
- notes = [...parseChord(chordName).value];
702
+ notes = [...parseChord(chordName).notes];
187
703
  } catch {
188
704
  return;
189
705
  }
@@ -257,6 +773,7 @@ var buildUI = (target, options) => {
257
773
  <button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
258
774
  <button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
259
775
  <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
776
+ <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
260
777
  <span class="dtm-grow"></span>
261
778
  <span class="dtm-label">BPM</span>
262
779
  <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
@@ -349,10 +866,10 @@ var buildUI = (target, options) => {
349
866
  <div class="dtm-row dtm-hidden" data-dtm="midi-track-selection"></div>
350
867
  <div class="dtm-row">
351
868
  <span class="dtm-label">MML</span>
352
- <textarea class="dtm-textarea" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
869
+ <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
870
+ <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">\u8AAD\u8FBC</button>
353
871
  </div>
354
872
  <div class="dtm-row">
355
- <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">MML\u8AAD\u8FBC</button>
356
873
  <span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
357
874
  <select class="dtm-select" data-dtm="shift-select">
358
875
  <option value="-96">-2\u5206</option>
@@ -426,6 +943,8 @@ var buildUI = (target, options) => {
426
943
  const sel = (name) => q(root, `[data-dtm="${name}"]`);
427
944
  return {
428
945
  root,
946
+ topbar: sel("transport"),
947
+ topbarLoading: sel("topbar-loading"),
429
948
  playBtn: sel("play"),
430
949
  recBtn: sel("rec"),
431
950
  soloCheckbox: sel("solo"),
@@ -1643,8 +2162,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
1643
2162
  ))().then((v) => {
1644
2163
  loaded.set(m, v);
1645
2164
  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);
2165
+ }).catch((err2) => {
2166
+ console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err2);
1648
2167
  return null;
1649
2168
  });
1650
2169
  loading.set(m, p);
@@ -3136,8 +3655,10 @@ var parseMML = (mml, options = {}) => {
3136
3655
  numStr += body[j];
3137
3656
  j++;
3138
3657
  }
3139
- if (ch === "t" && trackIndex === 0 && numStr) {
3140
- bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
3658
+ if (ch === "t" && numStr) {
3659
+ if (bpm === null) {
3660
+ bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
3661
+ }
3141
3662
  }
3142
3663
  pushTok("ctrl", currentStep, 0, tokStart);
3143
3664
  } else if (ch === "[") {
@@ -3639,6 +4160,7 @@ var DAW_CSS = `
3639
4160
  }
3640
4161
  .dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
3641
4162
  .dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
4163
+ .dtm-textarea.dtm-grow { width: 0; }
3642
4164
  .dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
3643
4165
 
3644
4166
  /* \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 +4328,7 @@ var DAW_CSS = `
3806
4328
 
3807
4329
  /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
3808
4330
  .dtm-overlay {
3809
- position: absolute; inset: 0; z-index: 1000;
4331
+ position: absolute; inset: 0; z-index: 10;
3810
4332
  background: rgba(0,0,0,.92);
3811
4333
  display: flex; align-items: center; justify-content: center;
3812
4334
  flex-direction: column; gap: 14px;
@@ -3855,6 +4377,22 @@ var DAW_CSS = `
3855
4377
  letter-spacing: .15em;
3856
4378
  min-height: 1em;
3857
4379
  }
4380
+ .dtm-topbar-loading {
4381
+ display: none;
4382
+ font-family: var(--dtm-font);
4383
+ font-size: 11px;
4384
+ color: var(--dtm-primary);
4385
+ margin-left: 12px;
4386
+ letter-spacing: .15em;
4387
+ align-self: center;
4388
+ }
4389
+ .dtm-topbar.is-loading .dtm-topbar-loading {
4390
+ display: inline-block;
4391
+ }
4392
+ .dtm-topbar.is-loading {
4393
+ pointer-events: none;
4394
+ opacity: 0.7;
4395
+ }
3858
4396
 
3859
4397
  @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
3860
4398
  .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
@@ -3930,6 +4468,14 @@ var DAW_CSS = `
3930
4468
  min-width: 2em;
3931
4469
  margin-left: 4px;
3932
4470
  }
4471
+ .dtm-player-chord {
4472
+ font-family: 'k8x12', monospace;
4473
+ font-size: 11px;
4474
+ color: var(--dtm-accent);
4475
+ min-width: 4em;
4476
+ margin-left: 8px;
4477
+ font-weight: bold;
4478
+ }
3933
4479
  .dtm-player-dots {
3934
4480
  margin-left: auto;
3935
4481
  display: flex;
@@ -4280,7 +4826,7 @@ var mountDAW = (target, options = {}) => {
4280
4826
  const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
4281
4827
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
4282
4828
  const showMidi = !!options.parseMidi;
4283
- const showChord = !!(options.parseChord && options.parseChords);
4829
+ const showChord = true;
4284
4830
  const refs = buildUI(target, {
4285
4831
  tracks: trackConfigs,
4286
4832
  drumPatternNames: Object.keys(drumPatterns),
@@ -4976,13 +5522,15 @@ var mountDAW = (target, options = {}) => {
4976
5522
  const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
4977
5523
  if (streaming && voices) {
4978
5524
  const overlay = showLoadingOverlay(refs.rollContainer);
5525
+ setLoading(true);
4979
5526
  try {
4980
5527
  await voices.loadModels(streamTracks.map((t) => t.model));
4981
5528
  await voices.warm(streamTracks);
4982
- } catch (err) {
4983
- console.warn("[dtm] voice preload failed", err);
5529
+ } catch (err2) {
5530
+ console.warn("[dtm] voice preload failed", err2);
4984
5531
  } finally {
4985
5532
  overlay.remove();
5533
+ setLoading(false);
4986
5534
  }
4987
5535
  }
4988
5536
  if (playbackState !== "paused") {
@@ -5331,21 +5879,28 @@ var mountDAW = (target, options = {}) => {
5331
5879
  barLimit: barLimitBars
5332
5880
  };
5333
5881
  }
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
- );
5882
+ const trackLines = [];
5883
+ const trackLinesMini = [];
5884
+ trackStates.forEach((t, i) => {
5885
+ const notes = clipNotes(t.core.getNotes());
5886
+ if (notes.length > 0) {
5887
+ const mml = t.core.getMMLFromNotes(notes, bpm, t.volume).trim();
5888
+ trackLines.push(`@${i} ${mml}`);
5889
+ trackLinesMini.push(`@${i}${mml.replace(/\s+/g, "")}`);
5890
+ }
5891
+ });
5340
5892
  const lyricLines = trackStates.map((t, i) => ({
5341
5893
  i,
5342
- text: t.lyrics.trim(),
5894
+ notes: clipNotes(t.core.getNotes()),
5895
+ text: t.lyrics.replace(/[\r\n]+/g, " ").trim(),
5343
5896
  model: t.lyricModel.trim(),
5344
5897
  vol: t.vocalVolume,
5345
5898
  gate: t.vocalGate,
5346
5899
  pan: t.vocalPan,
5347
5900
  oct: t.vocalOctave
5348
- })).filter((x) => x.model.length > 0 && x.text.length > 0).map((x) => {
5901
+ })).filter(
5902
+ (x) => x.model.length > 0 && x.text.length > 0 && x.notes.length > 0
5903
+ ).map((x) => {
5349
5904
  const params = [
5350
5905
  x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
5351
5906
  x.gate === 100 ? "" : `q${x.gate}`,
@@ -5361,7 +5916,7 @@ var mountDAW = (target, options = {}) => {
5361
5916
  full,
5362
5917
  minified,
5363
5918
  ignoredCount: 0,
5364
- trackCount: trackStates.length,
5919
+ trackCount: trackLines.length,
5365
5920
  barLimit: barLimitBars
5366
5921
  };
5367
5922
  };
@@ -5481,7 +6036,6 @@ var mountDAW = (target, options = {}) => {
5481
6036
  updateUndoRedo();
5482
6037
  };
5483
6038
  const applyChord = () => {
5484
- if (!options.parseChord || !options.parseChords) return;
5485
6039
  const active = getActive();
5486
6040
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
5487
6041
  if (!chordTrack) return;
@@ -5490,9 +6044,7 @@ var mountDAW = (target, options = {}) => {
5490
6044
  patternType: active.savedChordPattern,
5491
6045
  rootShift: active.savedChordRoot,
5492
6046
  bpm,
5493
- stepsPerBar: renderConfig.stepsPerBar,
5494
- parseChord: options.parseChord,
5495
- parseChords: options.parseChords
6047
+ stepsPerBar: renderConfig.stepsPerBar
5496
6048
  });
5497
6049
  chordTrack.core.clearNotesWithoutHistory();
5498
6050
  chordTrack.core.beginBatch();
@@ -5577,9 +6129,11 @@ var mountDAW = (target, options = {}) => {
5577
6129
  };
5578
6130
  const overlayDuring = (fn) => {
5579
6131
  refs.overlay.hidden = false;
6132
+ setLoading(true);
5580
6133
  setTimeout(() => {
5581
6134
  fn();
5582
6135
  refs.overlay.hidden = true;
6136
+ setLoading(false);
5583
6137
  }, 30);
5584
6138
  };
5585
6139
  const wireEvents = () => {
@@ -5723,6 +6277,7 @@ var mountDAW = (target, options = {}) => {
5723
6277
  const file = refs.midiInput.files?.[0];
5724
6278
  if (!file || !options.parseMidi) return;
5725
6279
  refs.overlay.hidden = false;
6280
+ setLoading(true);
5726
6281
  const buffer = new Uint8Array(await file.arrayBuffer());
5727
6282
  pendingMidi = options.parseMidi(buffer);
5728
6283
  detectedTracks = analyzeMidiTracks(pendingMidi);
@@ -5743,6 +6298,7 @@ var mountDAW = (target, options = {}) => {
5743
6298
  });
5744
6299
  refs.midiTrackSelection.classList.remove("dtm-hidden");
5745
6300
  refs.overlay.hidden = true;
6301
+ setLoading(false);
5746
6302
  });
5747
6303
  refs.midiLoadBtn.addEventListener("click", () => {
5748
6304
  if (!pendingMidi) return;
@@ -5816,6 +6372,9 @@ var mountDAW = (target, options = {}) => {
5816
6372
  resizeObserver.observe(refs.rollContainer);
5817
6373
  document.addEventListener("pointermove", onPointerMove);
5818
6374
  document.addEventListener("pointerup", onPointerUp);
6375
+ const setLoading = (loading) => {
6376
+ refs.topbar.classList.toggle("is-loading", loading);
6377
+ };
5819
6378
  return {
5820
6379
  play,
5821
6380
  pause,
@@ -5853,6 +6412,7 @@ var mountDAW = (target, options = {}) => {
5853
6412
  exportMIDI: exportMIDI2,
5854
6413
  setBpm,
5855
6414
  getPlaybackState: () => playbackState,
6415
+ setLoading,
5856
6416
  destroy: () => {
5857
6417
  sequencer.stop();
5858
6418
  resizeObserver.disconnect();
@@ -5997,6 +6557,117 @@ var mountMmlPlayer = (target, mml, options = {}) => {
5997
6557
  const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
5998
6558
  (a, b) => a - b
5999
6559
  );
6560
+ const trackStats = trackIndices.map((index) => {
6561
+ const trackPlacements = placements.filter((p) => p.trackIndex === index);
6562
+ if (trackPlacements.length === 0) {
6563
+ return { index, isChords: false, avgPitch: 0, noteCount: 0 };
6564
+ }
6565
+ const sumPitch = trackPlacements.reduce((sum, p) => sum + p.pitch, 0);
6566
+ const avgPitch = sumPitch / trackPlacements.length;
6567
+ const stepCounts = /* @__PURE__ */ new Map();
6568
+ for (const p of trackPlacements) {
6569
+ for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
6570
+ stepCounts.set(s, (stepCounts.get(s) ?? 0) + 1);
6571
+ }
6572
+ }
6573
+ const polyphonicSteps = Array.from(stepCounts.values()).filter(
6574
+ (c) => c >= 2
6575
+ ).length;
6576
+ const isChords = polyphonicSteps > 0;
6577
+ return {
6578
+ index,
6579
+ isChords,
6580
+ avgPitch,
6581
+ noteCount: trackPlacements.length
6582
+ };
6583
+ });
6584
+ const chordTrackIndices = trackStats.filter((s) => s.isChords).map((s) => s.index);
6585
+ const nonChordTracks = trackStats.filter(
6586
+ (s) => !s.isChords && s.noteCount > 0
6587
+ );
6588
+ let bassTrackIndex = null;
6589
+ if (nonChordTracks.length > 0) {
6590
+ nonChordTracks.sort((a, b) => a.avgPitch - b.avgPitch);
6591
+ bassTrackIndex = nonChordTracks[0].index;
6592
+ }
6593
+ const priorityTrackIndices = /* @__PURE__ */ new Set();
6594
+ for (const idx of chordTrackIndices) {
6595
+ priorityTrackIndices.add(idx);
6596
+ }
6597
+ if (bassTrackIndex !== null) {
6598
+ priorityTrackIndices.add(bassTrackIndex);
6599
+ }
6600
+ const hasPriorityTracksInMml = priorityTrackIndices.size > 0;
6601
+ const maxStep = placements.reduce(
6602
+ (max, p) => Math.max(max, p.startStep + p.durationSteps),
6603
+ 0
6604
+ );
6605
+ const priorityPitches = Array.from(
6606
+ { length: maxStep + 1 },
6607
+ () => /* @__PURE__ */ new Set()
6608
+ );
6609
+ const allPitches = Array.from(
6610
+ { length: maxStep + 1 },
6611
+ () => /* @__PURE__ */ new Set()
6612
+ );
6613
+ for (const p of placements) {
6614
+ for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
6615
+ if (s >= 0 && s <= maxStep) {
6616
+ allPitches[s].add(p.pitch);
6617
+ if (priorityTrackIndices.has(p.trackIndex)) {
6618
+ priorityPitches[s].add(p.pitch);
6619
+ }
6620
+ }
6621
+ }
6622
+ }
6623
+ const stepChords = [];
6624
+ const chordCache = /* @__PURE__ */ new Map();
6625
+ let lastChord = "";
6626
+ let hasPriorityStarted = false;
6627
+ const GRID_SIZE = 48;
6628
+ const MIN_DURATION = 12;
6629
+ for (let g = 0; g <= Math.ceil(maxStep / GRID_SIZE); g++) {
6630
+ const startS = g * GRID_SIZE;
6631
+ const endS = Math.min(maxStep, (g + 1) * GRID_SIZE - 1);
6632
+ if (startS > maxStep) break;
6633
+ for (let s = startS; s <= endS; s++) {
6634
+ const priorityPitchesAtStep = Array.from(priorityPitches[s]);
6635
+ if (priorityPitchesAtStep.length > 0) {
6636
+ hasPriorityStarted = true;
6637
+ }
6638
+ }
6639
+ const pitchDurations = /* @__PURE__ */ new Map();
6640
+ for (let s = startS; s <= endS; s++) {
6641
+ const usePriority = hasPriorityStarted && hasPriorityTracksInMml;
6642
+ const pitches = usePriority ? Array.from(priorityPitches[s]) : Array.from(allPitches[s]);
6643
+ for (const p of pitches) {
6644
+ pitchDurations.set(p, (pitchDurations.get(p) ?? 0) + 1);
6645
+ }
6646
+ }
6647
+ let activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur >= MIN_DURATION).map(([p, _]) => p);
6648
+ if (activePitches.length === 0 && pitchDurations.size > 0) {
6649
+ const maxDur = Math.max(...pitchDurations.values());
6650
+ activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur === maxDur).map(([p, _]) => p);
6651
+ }
6652
+ let gridChord = lastChord;
6653
+ if (activePitches.length > 0) {
6654
+ const sortedPitches = activePitches.sort((a, b) => a - b);
6655
+ const cacheKey = sortedPitches.join(",");
6656
+ if (chordCache.has(cacheKey)) {
6657
+ const chordName = chordCache.get(cacheKey);
6658
+ if (chordName) gridChord = chordName;
6659
+ } else {
6660
+ const candidates = detectChord(sortedPitches);
6661
+ const chordName = candidates[0]?.symbol ?? "";
6662
+ if (chordName) gridChord = chordName;
6663
+ chordCache.set(cacheKey, chordName);
6664
+ }
6665
+ }
6666
+ for (let s = startS; s <= endS; s++) {
6667
+ stepChords[s] = gridChord;
6668
+ }
6669
+ lastChord = gridChord;
6670
+ }
6000
6671
  const seqTracks = trackIndices.map((index) => {
6001
6672
  let id = 0;
6002
6673
  const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
@@ -6197,6 +6868,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6197
6868
  barEl.className = "dtm-player-bar";
6198
6869
  barEl.textContent = "-";
6199
6870
  beatRow.appendChild(barEl);
6871
+ const chordEl = doc.createElement("span");
6872
+ chordEl.className = "dtm-player-chord";
6873
+ chordEl.textContent = "";
6874
+ beatRow.appendChild(chordEl);
6200
6875
  const chips = [];
6201
6876
  const makeChip = (label) => {
6202
6877
  const chip = doc.createElement("span");
@@ -6340,10 +7015,20 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6340
7015
  lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
6341
7016
  };
6342
7017
  const renderPlayhead = (step) => {
7018
+ const intStep = Math.floor(step);
6343
7019
  const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
6344
7020
  for (let i = 0; i < 4; i++)
6345
7021
  beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
6346
7022
  barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
7023
+ const chordName = stepChords[intStep] ?? "";
7024
+ if (chordEl.textContent !== chordName) {
7025
+ chordEl.textContent = chordName;
7026
+ if (chordName) {
7027
+ console.log(
7028
+ `[dtm-player-chord] Active Chord: ${chordName} (step: ${intStep})`
7029
+ );
7030
+ }
7031
+ }
6347
7032
  for (const view of laneViews) {
6348
7033
  let active = null;
6349
7034
  for (const t of view.tokens) {
@@ -6357,6 +7042,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6357
7042
  const resetPlayhead = () => {
6358
7043
  for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
6359
7044
  barEl.textContent = "-";
7045
+ chordEl.textContent = "";
6360
7046
  for (const view of laneViews) {
6361
7047
  for (const t of view.tokens) t.el.classList.remove("is-active");
6362
7048
  view.lane.scrollLeft = 0;
@@ -6420,6 +7106,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6420
7106
  });
6421
7107
  }
6422
7108
  return {
7109
+ id: String(index),
6423
7110
  model: lt.model,
6424
7111
  volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
6425
7112
  pan: panToStereo(lt.pan ?? DEFAULT_PAN),
@@ -6435,15 +7122,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6435
7122
  try {
6436
7123
  await v.loadModels(tracks.map((t) => t.model));
6437
7124
  await v.warm(tracks);
6438
- } catch (err) {
6439
- console.warn("[dtm] voice preload failed", err);
7125
+ } catch (err2) {
7126
+ console.warn("[dtm] voice preload failed", err2);
6440
7127
  } finally {
6441
7128
  overlay.remove();
6442
7129
  }
6443
7130
  if (!playing || activePlayer !== instance) return;
6444
7131
  }
6445
7132
  seq.start(0);
6446
- if (streaming) ensureVoices().startStream(tracks, seq.getStartTime());
7133
+ if (streaming) {
7134
+ ensureVoices().startStream(tracks, seq.getStartTime(), {
7135
+ isAudible: (t) => !mutedTracks.has(Number(t.id))
7136
+ });
7137
+ }
6447
7138
  };
6448
7139
  const play = () => {
6449
7140
  if (playing || trackIndices.length === 0) return;
@@ -6777,8 +7468,6 @@ var DEFAULT_CDN = {
6777
7468
  soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
6778
7469
  soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
6779
7470
  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
7471
  midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
6783
7472
  };
6784
7473
  var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
@@ -6822,24 +7511,6 @@ var createDtmStudio = async (options = {}) => {
6822
7511
  eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
6823
7512
  eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
6824
7513
  ]);
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
7514
  let midiParser = null;
6844
7515
  let parseMidi;
6845
7516
  if (features.midi) {
@@ -7025,8 +7696,6 @@ var createDtmStudio = async (options = {}) => {
7025
7696
  onPlayNote: playNote,
7026
7697
  onPlayDrum: playDrum,
7027
7698
  singingVoices,
7028
- parseChord,
7029
- parseChords,
7030
7699
  parseMidi,
7031
7700
  onToggleRecord,
7032
7701
  ...dawOverrides
@@ -7048,15 +7717,32 @@ var createDtmStudio = async (options = {}) => {
7048
7717
  editorPresetSelects.set(target, select);
7049
7718
  select.addEventListener("change", async () => {
7050
7719
  if (!select) return;
7051
- daw.setInstrument(select.value);
7052
- await loadPreset(select.value, trackIds);
7720
+ const wasPlaying = daw.getPlaybackState() === "playing";
7721
+ if (wasPlaying) {
7722
+ daw.pause();
7723
+ }
7724
+ const overlay = showLoadingOverlay(target);
7725
+ daw.setLoading?.(true);
7726
+ try {
7727
+ daw.setInstrument(select.value);
7728
+ await loadPreset(select.value, trackIds);
7729
+ } finally {
7730
+ overlay.remove();
7731
+ daw.setLoading?.(false);
7732
+ if (wasPlaying) {
7733
+ daw.play();
7734
+ }
7735
+ }
7053
7736
  });
7054
7737
  }
7055
7738
  const daw = mountDAW(target, base);
7056
7739
  mountedEditors.push(daw);
7057
7740
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7058
7741
  daw.setInstrument(presetKey);
7059
- void loadPreset(presetKey, trackIds);
7742
+ daw.setLoading?.(true);
7743
+ void loadPreset(presetKey, trackIds).finally(() => {
7744
+ daw.setLoading?.(false);
7745
+ });
7060
7746
  const destroy = () => {
7061
7747
  daw.destroy();
7062
7748
  select?.remove();