@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/README.md +48 -3
- package/dist/index.d.mts +31 -47
- package/dist/index.d.ts +31 -47
- package/dist/index.js +1037 -67
- package/dist/index.mjs +1036 -67
- package/package.json +2 -1
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
|
-
//
|
|
163
|
-
var
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
patternType,
|
|
168
|
-
rootShift,
|
|
922
|
+
flat = false,
|
|
169
923
|
bpm,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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}`).
|
|
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).
|
|
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((
|
|
1751
|
-
console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`,
|
|
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
|
|
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" &&
|
|
3244
|
-
bpm
|
|
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:
|
|
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 =
|
|
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 (
|
|
5087
|
-
console.warn("[dtm] voice preload failed",
|
|
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 =
|
|
5439
|
-
|
|
5440
|
-
)
|
|
5441
|
-
|
|
5442
|
-
|
|
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
|
-
|
|
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(
|
|
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 = [
|
|
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:
|
|
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 (
|
|
6543
|
-
console.warn("[dtm] voice preload failed",
|
|
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)
|
|
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.
|
|
7157
|
-
|
|
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
|
-
|
|
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,
|