@onjmin/dtm 0.1.6 → 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/README.md +48 -3
- package/dist/index.d.mts +47 -48
- package/dist/index.d.ts +47 -48
- package/dist/index.js +769 -91
- package/dist/index.mjs +769 -91
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -159,18 +159,534 @@ async function buildNameToKeyMapping() {
|
|
|
159
159
|
return nameToKey;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// node_modules/.pnpm/@onjmin+chord-parser@1.0.2/node_modules/@onjmin/chord-parser/dist/index.mjs
|
|
163
|
+
var SHARP_NAMES = [
|
|
164
|
+
"C",
|
|
165
|
+
"C#",
|
|
166
|
+
"D",
|
|
167
|
+
"D#",
|
|
168
|
+
"E",
|
|
169
|
+
"F",
|
|
170
|
+
"F#",
|
|
171
|
+
"G",
|
|
172
|
+
"G#",
|
|
173
|
+
"A",
|
|
174
|
+
"A#",
|
|
175
|
+
"B"
|
|
176
|
+
];
|
|
177
|
+
var FLAT_NAMES = [
|
|
178
|
+
"C",
|
|
179
|
+
"Db",
|
|
180
|
+
"D",
|
|
181
|
+
"Eb",
|
|
182
|
+
"E",
|
|
183
|
+
"F",
|
|
184
|
+
"Gb",
|
|
185
|
+
"G",
|
|
186
|
+
"Ab",
|
|
187
|
+
"A",
|
|
188
|
+
"Bb",
|
|
189
|
+
"B"
|
|
190
|
+
];
|
|
191
|
+
var toPitchClass = (n) => (n % 12 + 12) % 12;
|
|
192
|
+
var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
|
|
193
|
+
var SyntaxErrorWithPos = class extends Error {
|
|
194
|
+
constructor(input, msg) {
|
|
195
|
+
super(
|
|
196
|
+
`SyntaxError: ${msg}
|
|
197
|
+
input.idx: ${input.idx}
|
|
198
|
+
input.str: ${input.str}`
|
|
199
|
+
);
|
|
200
|
+
this.name = "ChordSyntaxError";
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var err = (input, msg) => {
|
|
204
|
+
throw new SyntaxErrorWithPos(input, msg);
|
|
205
|
+
};
|
|
206
|
+
var Input = class _Input {
|
|
207
|
+
static nums = new Set("0123456789");
|
|
208
|
+
str;
|
|
209
|
+
nest;
|
|
210
|
+
idx;
|
|
211
|
+
constructor(str, nest = 0) {
|
|
212
|
+
this.str = str;
|
|
213
|
+
this.nest = nest;
|
|
214
|
+
this.idx = 0;
|
|
215
|
+
}
|
|
216
|
+
get isEOF() {
|
|
217
|
+
return this.str.length <= this.idx;
|
|
218
|
+
}
|
|
219
|
+
get char() {
|
|
220
|
+
return this.str[this.idx];
|
|
221
|
+
}
|
|
222
|
+
/** 先頭の連続する数字を消費して数値で返す(破壊的)。数字が無ければ null。 */
|
|
223
|
+
get num() {
|
|
224
|
+
let str = "";
|
|
225
|
+
while (!this.isEOF) {
|
|
226
|
+
const char = this.char;
|
|
227
|
+
if (!_Input.nums.has(char)) break;
|
|
228
|
+
str += char;
|
|
229
|
+
this.idx++;
|
|
230
|
+
}
|
|
231
|
+
return str.length ? Number(str) : null;
|
|
232
|
+
}
|
|
233
|
+
slice(i) {
|
|
234
|
+
return this.str.slice(this.idx, this.idx + i);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var Output = class {
|
|
238
|
+
pitch = null;
|
|
239
|
+
chord = null;
|
|
240
|
+
isChord = false;
|
|
241
|
+
pending = null;
|
|
242
|
+
nest = -1;
|
|
243
|
+
get value() {
|
|
244
|
+
const { pitch, chord } = this;
|
|
245
|
+
return new Set(
|
|
246
|
+
[...chord].map((v) => v + pitch)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
set value(chord) {
|
|
250
|
+
const pitch = this.pitch;
|
|
251
|
+
this.chord = new Set([...chord].map((v) => v - pitch));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var Matcher = class {
|
|
255
|
+
map = /* @__PURE__ */ new Map();
|
|
256
|
+
/** キー長を降順で保持(最長一致のため)。 */
|
|
257
|
+
lengths = [];
|
|
258
|
+
_set(key, value) {
|
|
259
|
+
this.map.set(key, value);
|
|
260
|
+
if (!this.lengths.includes(key.length)) {
|
|
261
|
+
this.lengths.push(key.length);
|
|
262
|
+
this.lengths.sort((a, b) => b - a);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
set(key, value) {
|
|
266
|
+
if (Array.isArray(key)) for (const k of key) this._set(k, value);
|
|
267
|
+
else this._set(key, value);
|
|
268
|
+
}
|
|
269
|
+
parse(input) {
|
|
270
|
+
for (const i of this.lengths) {
|
|
271
|
+
const s = input.slice(i);
|
|
272
|
+
if (this.map.has(s)) {
|
|
273
|
+
input.idx += s.length;
|
|
274
|
+
return this.map.get(s);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var BRACKET_START = 0;
|
|
281
|
+
var BRACKET_END = 1;
|
|
282
|
+
var COMMA = 2;
|
|
283
|
+
var DIVIDE = 3;
|
|
284
|
+
var formulaMatcher = new Matcher();
|
|
285
|
+
formulaMatcher.set("(", BRACKET_START);
|
|
286
|
+
formulaMatcher.set(")", BRACKET_END);
|
|
287
|
+
formulaMatcher.set(",", COMMA);
|
|
288
|
+
formulaMatcher.set(["/", "on"], DIVIDE);
|
|
289
|
+
var parseFormula = (input, output = new Output(), nest = 0) => {
|
|
290
|
+
let start = input.idx;
|
|
291
|
+
const _eval = (idx) => {
|
|
292
|
+
const str = input.str.slice(start, idx);
|
|
293
|
+
if (str.length) parseTerm(new Input(str, nest), output);
|
|
294
|
+
};
|
|
295
|
+
while (true) {
|
|
296
|
+
const { idx } = input;
|
|
297
|
+
if (input.isEOF) {
|
|
298
|
+
if (nest) err(input, `Unclosed ${nest} brackets`);
|
|
299
|
+
_eval(idx);
|
|
300
|
+
return output;
|
|
301
|
+
}
|
|
302
|
+
const res = formulaMatcher.parse(input);
|
|
303
|
+
if (res === null) {
|
|
304
|
+
input.idx++;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const { pending } = output;
|
|
308
|
+
_eval(idx);
|
|
309
|
+
switch (res) {
|
|
310
|
+
case BRACKET_START:
|
|
311
|
+
parseFormula(input, output, nest + 1);
|
|
312
|
+
break;
|
|
313
|
+
case BRACKET_END:
|
|
314
|
+
if (nest - 1 < 0) err(input, "Unable to close brackets");
|
|
315
|
+
return output;
|
|
316
|
+
case COMMA:
|
|
317
|
+
output.pending = pending;
|
|
318
|
+
break;
|
|
319
|
+
case DIVIDE: {
|
|
320
|
+
const o = parseFormula(input, new Output(), nest);
|
|
321
|
+
const v = [...output.value];
|
|
322
|
+
if (o.isChord) {
|
|
323
|
+
output.value = [...o.value].concat(v);
|
|
324
|
+
} else {
|
|
325
|
+
const a = v.sort((x, y) => x - y);
|
|
326
|
+
const pitch = (o.pitch + 3) % 12 - 3;
|
|
327
|
+
if (a[0] < pitch) {
|
|
328
|
+
while (a[0] < pitch) a.push(a.shift() + 12);
|
|
329
|
+
} else {
|
|
330
|
+
while (true) {
|
|
331
|
+
const w = a[a.length - 1] - 12;
|
|
332
|
+
if (w < pitch) break;
|
|
333
|
+
a.pop();
|
|
334
|
+
a.unshift(w);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
a.push(pitch);
|
|
338
|
+
output.value = a;
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
start = input.idx;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var parseTerm = (input, output) => {
|
|
347
|
+
if (input.isEOF) return output;
|
|
348
|
+
if (output.pitch === null) return parsePitch(input, output);
|
|
349
|
+
if (output.pending === null) return parseFunc(input, output);
|
|
350
|
+
return parsePending(input, output);
|
|
351
|
+
};
|
|
352
|
+
var halfMatcher = new Matcher();
|
|
353
|
+
var halfMatcherStrict = new Matcher();
|
|
354
|
+
for (const m of [halfMatcher, halfMatcherStrict]) {
|
|
355
|
+
m.set(["#", "\u266F"], 1);
|
|
356
|
+
m.set(["b", "\u266D"], -1);
|
|
357
|
+
}
|
|
358
|
+
halfMatcher.set("+", 1);
|
|
359
|
+
halfMatcher.set("-", -1);
|
|
360
|
+
var parseHalf = (input, isPitch = false) => (isPitch ? halfMatcherStrict : halfMatcher).parse(input);
|
|
361
|
+
var idx2pitch = [0, 2, 4, 5, 7, 9, 11];
|
|
362
|
+
for (const i of [...idx2pitch.keys()]) idx2pitch.push(idx2pitch[i] + 12);
|
|
363
|
+
var deg2pitch = (deg) => idx2pitch[deg - 1];
|
|
364
|
+
var pitchMatcher = new Matcher();
|
|
365
|
+
for (const [i, v] of [..."CDEFGAB"].entries())
|
|
366
|
+
pitchMatcher.set(v, idx2pitch[i]);
|
|
367
|
+
var parsePitch = (input, output) => {
|
|
368
|
+
const pitch = pitchMatcher.parse(input);
|
|
369
|
+
if (pitch === null) err(input, "Not found pitch");
|
|
370
|
+
output.pitch = pitch;
|
|
371
|
+
const half = parseHalf(input, true);
|
|
372
|
+
if (half !== null) output.pitch += half;
|
|
373
|
+
return parseBase(input, output);
|
|
374
|
+
};
|
|
375
|
+
var MAJOR = [0, 4, 7];
|
|
376
|
+
var DIM = [0, 3, 6];
|
|
377
|
+
var baseMatcher = new Matcher();
|
|
378
|
+
baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
|
|
379
|
+
baseMatcher.set(["dim", "\u3007"], DIM);
|
|
380
|
+
baseMatcher.set("+", [0, 4, 8]);
|
|
381
|
+
baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
|
|
382
|
+
var parseBase = (input, output) => {
|
|
383
|
+
const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
|
|
384
|
+
const res = isMajMarker ? null : baseMatcher.parse(input);
|
|
385
|
+
if (res !== null) output.isChord = true;
|
|
386
|
+
output.chord = new Set(res || MAJOR);
|
|
387
|
+
if (res === DIM) {
|
|
388
|
+
const { num } = input;
|
|
389
|
+
const chord = output.chord;
|
|
390
|
+
if (num !== null) chord.add(deg2pitch(num) - 2);
|
|
391
|
+
}
|
|
392
|
+
output.nest = input.nest;
|
|
393
|
+
return parseTerm(input, output);
|
|
394
|
+
};
|
|
395
|
+
var add = (chord, n, half) => {
|
|
396
|
+
chord.add(deg2pitch(n) + half);
|
|
397
|
+
};
|
|
398
|
+
var aug = (chord) => {
|
|
399
|
+
chord.delete(deg2pitch(5));
|
|
400
|
+
chord.add(deg2pitch(5) + 1);
|
|
401
|
+
};
|
|
402
|
+
var _7th = (chord, n, _half2, isFlat = false) => {
|
|
403
|
+
if (n === 5) chord.delete(deg2pitch(3));
|
|
404
|
+
else if (n === 6) chord.add(deg2pitch(6));
|
|
405
|
+
else if (n === 69) chord.add(deg2pitch(6)).add(deg2pitch(9));
|
|
406
|
+
else {
|
|
407
|
+
if (n >= 7) chord.add(deg2pitch(7) + (isFlat ? -1 : 0));
|
|
408
|
+
if (n >= 9) chord.add(deg2pitch(9));
|
|
409
|
+
if (n >= 11) chord.add(deg2pitch(11));
|
|
410
|
+
if (n >= 13) chord.add(deg2pitch(13));
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
var _half = (chord, n, half) => {
|
|
414
|
+
chord.delete(deg2pitch(n));
|
|
415
|
+
chord.add(deg2pitch(n) + half);
|
|
416
|
+
};
|
|
417
|
+
var funcMatcher = new Matcher();
|
|
418
|
+
funcMatcher.set("add", add);
|
|
419
|
+
funcMatcher.set(["omit", "no"], (chord, n, half) => {
|
|
420
|
+
chord.delete(deg2pitch(n) + half);
|
|
421
|
+
});
|
|
422
|
+
funcMatcher.set("sus", (chord, n, half) => {
|
|
423
|
+
chord.delete(deg2pitch(3));
|
|
424
|
+
chord.add(deg2pitch(n) + half);
|
|
425
|
+
});
|
|
426
|
+
funcMatcher.set(
|
|
427
|
+
["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
|
|
428
|
+
_7th
|
|
429
|
+
);
|
|
430
|
+
funcMatcher.set("aug", aug);
|
|
431
|
+
var parseFunc = (input, output) => {
|
|
432
|
+
if (!output.isChord) output.isChord = true;
|
|
433
|
+
const func = funcMatcher.parse(input);
|
|
434
|
+
const chord = output.chord;
|
|
435
|
+
if (func === null) {
|
|
436
|
+
const isAug = input.char === "+";
|
|
437
|
+
const half = parseHalf(input);
|
|
438
|
+
const { num } = input;
|
|
439
|
+
if (num === null) {
|
|
440
|
+
if (isAug) aug(chord);
|
|
441
|
+
else err(input, "Not found number");
|
|
442
|
+
}
|
|
443
|
+
if (half === null) {
|
|
444
|
+
if (input.nest === output.nest) _7th(chord, num, 0, true);
|
|
445
|
+
else add(chord, num, 0);
|
|
446
|
+
} else {
|
|
447
|
+
_half(chord, num, half);
|
|
448
|
+
}
|
|
449
|
+
} else if (func === aug) {
|
|
450
|
+
aug(chord);
|
|
451
|
+
} else {
|
|
452
|
+
output.pending = func;
|
|
453
|
+
}
|
|
454
|
+
return parseTerm(input, output);
|
|
455
|
+
};
|
|
456
|
+
var parsePending = (input, output) => {
|
|
457
|
+
const half = parseHalf(input);
|
|
458
|
+
const { num } = input;
|
|
459
|
+
const { pending, chord } = output;
|
|
460
|
+
if (num === null) err(input, "Not found number");
|
|
461
|
+
pending(
|
|
462
|
+
chord,
|
|
463
|
+
num,
|
|
464
|
+
half === null ? 0 : half
|
|
465
|
+
);
|
|
466
|
+
output.pending = null;
|
|
467
|
+
return parseTerm(input, output);
|
|
468
|
+
};
|
|
469
|
+
var parseChord = (symbol) => {
|
|
470
|
+
const output = parseFormula(new Input(symbol));
|
|
471
|
+
const notes = [...output.value].sort((a, b) => a - b);
|
|
472
|
+
const intervals = [...output.chord].sort((a, b) => a - b);
|
|
473
|
+
const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
|
|
474
|
+
(a, b) => a - b
|
|
475
|
+
);
|
|
476
|
+
return {
|
|
477
|
+
symbol,
|
|
478
|
+
root: toPitchClass(output.pitch),
|
|
479
|
+
notes,
|
|
480
|
+
pitchClasses,
|
|
481
|
+
intervals
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
var QUALITY_SOURCE = [
|
|
485
|
+
"",
|
|
486
|
+
// major
|
|
487
|
+
"m",
|
|
488
|
+
// minor
|
|
489
|
+
"7",
|
|
490
|
+
// dominant 7th
|
|
491
|
+
"M7",
|
|
492
|
+
// major 7th
|
|
493
|
+
"m7",
|
|
494
|
+
// minor 7th
|
|
495
|
+
"dim",
|
|
496
|
+
// diminished triad
|
|
497
|
+
"m7b5",
|
|
498
|
+
// half-diminished
|
|
499
|
+
"aug",
|
|
500
|
+
// augmented triad
|
|
501
|
+
"6",
|
|
502
|
+
// major 6th
|
|
503
|
+
"m6",
|
|
504
|
+
// minor 6th
|
|
505
|
+
"sus4",
|
|
506
|
+
"sus2",
|
|
507
|
+
"mM7",
|
|
508
|
+
// minor major 7th
|
|
509
|
+
"dim7",
|
|
510
|
+
// diminished 7th
|
|
511
|
+
"7sus4",
|
|
512
|
+
"7#5",
|
|
513
|
+
// augmented 7th
|
|
514
|
+
"add9",
|
|
515
|
+
"madd9",
|
|
516
|
+
"9",
|
|
517
|
+
"M9",
|
|
518
|
+
"m9",
|
|
519
|
+
"69",
|
|
520
|
+
"m69",
|
|
521
|
+
"5"
|
|
522
|
+
// power chord
|
|
523
|
+
];
|
|
524
|
+
var QUALITIES = QUALITY_SOURCE.map(
|
|
525
|
+
(quality, priority) => ({
|
|
526
|
+
quality,
|
|
527
|
+
pitchClasses: parseChord(`C${quality}`).pitchClasses,
|
|
528
|
+
priority
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
var QUALITY_BY_PCSET = (() => {
|
|
532
|
+
const map = /* @__PURE__ */ new Map();
|
|
533
|
+
for (const def of QUALITIES) {
|
|
534
|
+
const key = def.pitchClasses.join(",");
|
|
535
|
+
if (!map.has(key)) map.set(key, def);
|
|
536
|
+
}
|
|
537
|
+
return map;
|
|
538
|
+
})();
|
|
539
|
+
var detectChord = (notes, options = {}) => {
|
|
540
|
+
if (!notes.length) return [];
|
|
541
|
+
const { flat = false } = options;
|
|
542
|
+
const pcs = [...new Set(notes.map(toPitchClass))].sort((a, b) => a - b);
|
|
543
|
+
const bass = toPitchClass(
|
|
544
|
+
options.bass ?? notes.reduce((m, v) => Math.min(m, v), notes[0])
|
|
545
|
+
);
|
|
546
|
+
const scored = [];
|
|
547
|
+
for (const root of pcs) {
|
|
548
|
+
const relSet = new Set(pcs.map((pc) => toPitchClass(pc - root)));
|
|
549
|
+
for (const def of QUALITY_BY_PCSET.values()) {
|
|
550
|
+
let matchCount = 0;
|
|
551
|
+
let hasRoot = false;
|
|
552
|
+
for (const pc of def.pitchClasses) {
|
|
553
|
+
if (relSet.has(pc)) {
|
|
554
|
+
matchCount++;
|
|
555
|
+
if (pc === 0) hasRoot = true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (!hasRoot) continue;
|
|
559
|
+
if (matchCount < Math.min(2, def.pitchClasses.length)) continue;
|
|
560
|
+
const missingCount = def.pitchClasses.length - matchCount;
|
|
561
|
+
let extraCount = 0;
|
|
562
|
+
const defSet = new Set(def.pitchClasses);
|
|
563
|
+
for (const pc of relSet) {
|
|
564
|
+
if (!defSet.has(pc)) {
|
|
565
|
+
extraCount++;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const score = matchCount * 2 - missingCount * 1 - extraCount * 0.5;
|
|
569
|
+
const rootSymbol = noteName(root, flat) + def.quality;
|
|
570
|
+
const inversion = root !== bass;
|
|
571
|
+
scored.push({
|
|
572
|
+
score,
|
|
573
|
+
priority: def.priority,
|
|
574
|
+
candidate: {
|
|
575
|
+
symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
|
|
576
|
+
rootSymbol,
|
|
577
|
+
root,
|
|
578
|
+
quality: def.quality,
|
|
579
|
+
bass,
|
|
580
|
+
inversion
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
scored.sort((a, b) => {
|
|
586
|
+
if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
|
|
587
|
+
if (a.candidate.inversion !== b.candidate.inversion)
|
|
588
|
+
return a.candidate.inversion ? 1 : -1;
|
|
589
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
590
|
+
return a.candidate.root - b.candidate.root;
|
|
591
|
+
});
|
|
592
|
+
return scored.map((s) => s.candidate);
|
|
593
|
+
};
|
|
594
|
+
var toneRoleWeight = (rel) => {
|
|
595
|
+
if (rel === 0) return 1.3;
|
|
596
|
+
if (rel === 3 || rel === 4) return 1.2;
|
|
597
|
+
if (rel === 10 || rel === 11) return 0.95;
|
|
598
|
+
if (rel === 6 || rel === 7 || rel === 8) return 0.7;
|
|
599
|
+
return 0.85;
|
|
600
|
+
};
|
|
601
|
+
var CHORD_TEMPLATES = (() => {
|
|
602
|
+
const templates = [];
|
|
603
|
+
for (let root = 0; root < 12; root++) {
|
|
604
|
+
for (const def of QUALITIES) {
|
|
605
|
+
const pcs = /* @__PURE__ */ new Set();
|
|
606
|
+
const weights = new Array(12).fill(0);
|
|
607
|
+
const rel = /* @__PURE__ */ new Set();
|
|
608
|
+
for (const relPc of def.pitchClasses) {
|
|
609
|
+
rel.add(relPc);
|
|
610
|
+
const pc = toPitchClass(relPc + root);
|
|
611
|
+
pcs.add(pc);
|
|
612
|
+
weights[pc] = toneRoleWeight(relPc);
|
|
613
|
+
}
|
|
614
|
+
templates.push({
|
|
615
|
+
root,
|
|
616
|
+
quality: def.quality,
|
|
617
|
+
priority: def.priority,
|
|
618
|
+
pcs,
|
|
619
|
+
weights,
|
|
620
|
+
rel
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return templates;
|
|
625
|
+
})();
|
|
626
|
+
var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
|
|
627
|
+
var parseChords = (str, bpm = 120) => {
|
|
628
|
+
const output = [];
|
|
629
|
+
const secBar = 60 / bpm * 4;
|
|
630
|
+
const frontChars = new Set("ABCDEFG_=%N");
|
|
631
|
+
let idx = 0;
|
|
632
|
+
let last = null;
|
|
633
|
+
for (const line of toHan(str).split("\n").map((v) => v.trim())) {
|
|
634
|
+
if (!line.length || /^#/.test(line)) continue;
|
|
635
|
+
for (const str2 of line.split(/[|ll→]/)) {
|
|
636
|
+
if (!str2.length) continue;
|
|
637
|
+
const when = idx++ * secBar;
|
|
638
|
+
const a = [];
|
|
639
|
+
for (let i = 0; i < str2.length; i++) {
|
|
640
|
+
const char = str2[i];
|
|
641
|
+
const prev = str2[i - 1];
|
|
642
|
+
const prev2 = str2.slice(i - 2, i);
|
|
643
|
+
if (!frontChars.has(char)) continue;
|
|
644
|
+
if (prev === "/" || prev2 === "on") continue;
|
|
645
|
+
if (prev2 === "N." && char === "C") continue;
|
|
646
|
+
a.push(i);
|
|
647
|
+
}
|
|
648
|
+
if (!a.length) continue;
|
|
649
|
+
const divide = 2 ** Math.ceil(Math.log2(a.length));
|
|
650
|
+
const unitTime = secBar / divide;
|
|
651
|
+
for (const [i, v] of a.entries()) {
|
|
652
|
+
const s = str2.slice(v, i === a.length - 1 ? str2.length : a[i + 1]).replace(/\s+/g, "");
|
|
653
|
+
const c = s[0];
|
|
654
|
+
if (c === "_" || c === "N") {
|
|
655
|
+
last = null;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (c === "=") {
|
|
659
|
+
if (last) last.duration += unitTime;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const _when = when + i * unitTime;
|
|
663
|
+
if (c === "%") {
|
|
664
|
+
if (last === null) continue;
|
|
665
|
+
const base = last;
|
|
666
|
+
last = { ...base, when: _when, duration: unitTime };
|
|
667
|
+
} else {
|
|
668
|
+
const key = s.slice(0, s[1] === "#" ? 2 : 1);
|
|
669
|
+
const chord = s.slice(key.length).replace(/[\s・]/g, "");
|
|
670
|
+
last = {
|
|
671
|
+
key,
|
|
672
|
+
chord,
|
|
673
|
+
when: _when,
|
|
674
|
+
duration: unitTime
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
output.push(last);
|
|
678
|
+
}
|
|
679
|
+
if (last !== null && divide > a.length)
|
|
680
|
+
last.duration += unitTime * (divide - a.length);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return output;
|
|
684
|
+
};
|
|
685
|
+
|
|
162
686
|
// src/chords.ts
|
|
163
687
|
var C3 = 48;
|
|
164
688
|
var buildChordPlacements = (options) => {
|
|
165
|
-
const {
|
|
166
|
-
chordStr,
|
|
167
|
-
patternType,
|
|
168
|
-
rootShift,
|
|
169
|
-
bpm,
|
|
170
|
-
stepsPerBar,
|
|
171
|
-
parseChord,
|
|
172
|
-
parseChords
|
|
173
|
-
} = options;
|
|
689
|
+
const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
|
|
174
690
|
const placements = [];
|
|
175
691
|
if (!chordStr.trim()) return placements;
|
|
176
692
|
const offset = rootShift;
|
|
@@ -200,7 +716,7 @@ var buildChordPlacements = (options) => {
|
|
|
200
716
|
for (const chord of group) {
|
|
201
717
|
let notes;
|
|
202
718
|
try {
|
|
203
|
-
notes = [...parseChord(`${chord.key}${chord.chord}`).
|
|
719
|
+
notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
|
|
204
720
|
} catch {
|
|
205
721
|
continue;
|
|
206
722
|
}
|
|
@@ -287,7 +803,7 @@ var buildChordPlacements = (options) => {
|
|
|
287
803
|
chordNames.forEach((chordName, barIndex) => {
|
|
288
804
|
let notes;
|
|
289
805
|
try {
|
|
290
|
-
notes = [...parseChord(chordName).
|
|
806
|
+
notes = [...parseChord(chordName).notes];
|
|
291
807
|
} catch {
|
|
292
808
|
return;
|
|
293
809
|
}
|
|
@@ -361,6 +877,7 @@ var buildUI = (target, options) => {
|
|
|
361
877
|
<button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
|
|
362
878
|
<button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
|
|
363
879
|
<label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
|
|
880
|
+
<span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
|
|
364
881
|
<span class="dtm-grow"></span>
|
|
365
882
|
<span class="dtm-label">BPM</span>
|
|
366
883
|
<input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
|
|
@@ -453,10 +970,10 @@ var buildUI = (target, options) => {
|
|
|
453
970
|
<div class="dtm-row dtm-hidden" data-dtm="midi-track-selection"></div>
|
|
454
971
|
<div class="dtm-row">
|
|
455
972
|
<span class="dtm-label">MML</span>
|
|
456
|
-
<textarea class="dtm-textarea" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
|
|
973
|
+
<textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
|
|
974
|
+
<button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">\u8AAD\u8FBC</button>
|
|
457
975
|
</div>
|
|
458
976
|
<div class="dtm-row">
|
|
459
|
-
<button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">MML\u8AAD\u8FBC</button>
|
|
460
977
|
<span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
|
|
461
978
|
<select class="dtm-select" data-dtm="shift-select">
|
|
462
979
|
<option value="-96">-2\u5206</option>
|
|
@@ -530,6 +1047,8 @@ var buildUI = (target, options) => {
|
|
|
530
1047
|
const sel = (name) => q(root, `[data-dtm="${name}"]`);
|
|
531
1048
|
return {
|
|
532
1049
|
root,
|
|
1050
|
+
topbar: sel("transport"),
|
|
1051
|
+
topbarLoading: sel("topbar-loading"),
|
|
533
1052
|
playBtn: sel("play"),
|
|
534
1053
|
recBtn: sel("rec"),
|
|
535
1054
|
soloCheckbox: sel("solo"),
|
|
@@ -1747,8 +2266,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
1747
2266
|
))().then((v) => {
|
|
1748
2267
|
loaded.set(m, v);
|
|
1749
2268
|
return v;
|
|
1750
|
-
}).catch((
|
|
1751
|
-
console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`,
|
|
2269
|
+
}).catch((err2) => {
|
|
2270
|
+
console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err2);
|
|
1752
2271
|
return null;
|
|
1753
2272
|
});
|
|
1754
2273
|
loading.set(m, p);
|
|
@@ -1789,42 +2308,33 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
1789
2308
|
}
|
|
1790
2309
|
await Promise.all(promises);
|
|
1791
2310
|
};
|
|
1792
|
-
const startStream = (tracks, anchorTime) => {
|
|
2311
|
+
const startStream = (tracks, anchorTime, opts) => {
|
|
1793
2312
|
const session = ++streamSession;
|
|
1794
|
-
const
|
|
1795
|
-
for (const track of tracks) {
|
|
2313
|
+
const runTrack = async (track) => {
|
|
1796
2314
|
const model = loaded.get(track.model.toLowerCase());
|
|
1797
|
-
if (!model)
|
|
2315
|
+
if (!model) return;
|
|
2316
|
+
const items = [];
|
|
1798
2317
|
forEachSungNote(track, (note, prevVowel) => {
|
|
1799
|
-
items.push({
|
|
1800
|
-
model,
|
|
1801
|
-
note,
|
|
1802
|
-
prevVowel,
|
|
1803
|
-
volume: track.volume,
|
|
1804
|
-
pan: track.pan
|
|
1805
|
-
});
|
|
2318
|
+
items.push({ note, prevVowel });
|
|
1806
2319
|
});
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
void (async () => {
|
|
1810
|
-
for (const item of items) {
|
|
2320
|
+
const peak = Math.max(1e-4, track.volume);
|
|
2321
|
+
for (const { note, prevVowel } of items) {
|
|
1811
2322
|
if (session !== streamSession) return;
|
|
1812
|
-
while (
|
|
2323
|
+
while (note.startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
|
|
1813
2324
|
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
|
|
1814
2325
|
if (session !== streamSession) return;
|
|
1815
2326
|
}
|
|
1816
|
-
|
|
1817
|
-
const
|
|
1818
|
-
const { model, note } = item;
|
|
2327
|
+
if (opts?.isAudible && !opts.isAudible(track)) continue;
|
|
2328
|
+
const t0 = anchorTime + note.startSec;
|
|
1819
2329
|
if (model.renderToCache && model.scheduleCached) {
|
|
1820
2330
|
const key = await model.renderToCache(
|
|
1821
2331
|
note.syllable,
|
|
1822
|
-
|
|
2332
|
+
prevVowel,
|
|
1823
2333
|
note.pitch,
|
|
1824
2334
|
note.durationSec * 1e3
|
|
1825
2335
|
);
|
|
1826
2336
|
if (session !== streamSession) return;
|
|
1827
|
-
if (key) model.scheduleCached(key, t0, peak,
|
|
2337
|
+
if (key) model.scheduleCached(key, t0, peak, track.pan);
|
|
1828
2338
|
} else {
|
|
1829
2339
|
const when = t0 - ctx.currentTime;
|
|
1830
2340
|
model(note.syllable, {
|
|
@@ -1834,12 +2344,13 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
1834
2344
|
volume: peak,
|
|
1835
2345
|
when,
|
|
1836
2346
|
duration: note.durationSec,
|
|
1837
|
-
pan:
|
|
2347
|
+
pan: track.pan
|
|
1838
2348
|
});
|
|
2349
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1839
2350
|
}
|
|
1840
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1841
2351
|
}
|
|
1842
|
-
}
|
|
2352
|
+
};
|
|
2353
|
+
for (const track of tracks) void runTrack(track);
|
|
1843
2354
|
};
|
|
1844
2355
|
const stopStream = () => {
|
|
1845
2356
|
streamSession++;
|
|
@@ -3248,8 +3759,10 @@ var parseMML = (mml, options = {}) => {
|
|
|
3248
3759
|
numStr += body[j];
|
|
3249
3760
|
j++;
|
|
3250
3761
|
}
|
|
3251
|
-
if (ch === "t" &&
|
|
3252
|
-
bpm
|
|
3762
|
+
if (ch === "t" && numStr) {
|
|
3763
|
+
if (bpm === null) {
|
|
3764
|
+
bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
|
|
3765
|
+
}
|
|
3253
3766
|
}
|
|
3254
3767
|
pushTok("ctrl", currentStep, 0, tokStart);
|
|
3255
3768
|
} else if (ch === "[") {
|
|
@@ -3379,13 +3892,10 @@ var createSequencer = (options) => {
|
|
|
3379
3892
|
}
|
|
3380
3893
|
while (nowIndex < timeline.length) {
|
|
3381
3894
|
const ev = timeline[nowIndex];
|
|
3382
|
-
if (soloId && ev.trackId !== soloId) {
|
|
3383
|
-
nowIndex++;
|
|
3384
|
-
continue;
|
|
3385
|
-
}
|
|
3386
3895
|
const _when = ev.when - time;
|
|
3387
3896
|
if (_when > PLAN_TIME) break;
|
|
3388
3897
|
nowIndex++;
|
|
3898
|
+
if (soloId && ev.trackId !== soloId) continue;
|
|
3389
3899
|
const velocityVolume = ev.velocity / 127;
|
|
3390
3900
|
const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
|
|
3391
3901
|
options.onPlayNote({
|
|
@@ -3754,6 +4264,7 @@ var DAW_CSS = `
|
|
|
3754
4264
|
}
|
|
3755
4265
|
.dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
|
|
3756
4266
|
.dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
|
|
4267
|
+
.dtm-textarea.dtm-grow { width: 0; }
|
|
3757
4268
|
.dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
|
|
3758
4269
|
|
|
3759
4270
|
/* \u2500\u2500\u2500 \u30C8\u30E9\u30C3\u30AF\u30D4\u30EB\uFF08\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u9078\u629E\u30DC\u30BF\u30F3\uFF09 \u2500\u2500\u2500 */
|
|
@@ -3921,7 +4432,7 @@ var DAW_CSS = `
|
|
|
3921
4432
|
|
|
3922
4433
|
/* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
|
|
3923
4434
|
.dtm-overlay {
|
|
3924
|
-
position: absolute; inset: 0; z-index:
|
|
4435
|
+
position: absolute; inset: 0; z-index: 10;
|
|
3925
4436
|
background: rgba(0,0,0,.92);
|
|
3926
4437
|
display: flex; align-items: center; justify-content: center;
|
|
3927
4438
|
flex-direction: column; gap: 14px;
|
|
@@ -3970,6 +4481,22 @@ var DAW_CSS = `
|
|
|
3970
4481
|
letter-spacing: .15em;
|
|
3971
4482
|
min-height: 1em;
|
|
3972
4483
|
}
|
|
4484
|
+
.dtm-topbar-loading {
|
|
4485
|
+
display: none;
|
|
4486
|
+
font-family: var(--dtm-font);
|
|
4487
|
+
font-size: 11px;
|
|
4488
|
+
color: var(--dtm-primary);
|
|
4489
|
+
margin-left: 12px;
|
|
4490
|
+
letter-spacing: .15em;
|
|
4491
|
+
align-self: center;
|
|
4492
|
+
}
|
|
4493
|
+
.dtm-topbar.is-loading .dtm-topbar-loading {
|
|
4494
|
+
display: inline-block;
|
|
4495
|
+
}
|
|
4496
|
+
.dtm-topbar.is-loading {
|
|
4497
|
+
pointer-events: none;
|
|
4498
|
+
opacity: 0.7;
|
|
4499
|
+
}
|
|
3973
4500
|
|
|
3974
4501
|
@keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
|
3975
4502
|
.dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
|
|
@@ -4045,6 +4572,14 @@ var DAW_CSS = `
|
|
|
4045
4572
|
min-width: 2em;
|
|
4046
4573
|
margin-left: 4px;
|
|
4047
4574
|
}
|
|
4575
|
+
.dtm-player-chord {
|
|
4576
|
+
font-family: 'k8x12', monospace;
|
|
4577
|
+
font-size: 11px;
|
|
4578
|
+
color: var(--dtm-accent);
|
|
4579
|
+
min-width: 4em;
|
|
4580
|
+
margin-left: 8px;
|
|
4581
|
+
font-weight: bold;
|
|
4582
|
+
}
|
|
4048
4583
|
.dtm-player-dots {
|
|
4049
4584
|
margin-left: auto;
|
|
4050
4585
|
display: flex;
|
|
@@ -4395,7 +4930,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
4395
4930
|
const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
|
|
4396
4931
|
const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
|
|
4397
4932
|
const showMidi = !!options.parseMidi;
|
|
4398
|
-
const showChord =
|
|
4933
|
+
const showChord = true;
|
|
4399
4934
|
const refs = buildUI(target, {
|
|
4400
4935
|
tracks: trackConfigs,
|
|
4401
4936
|
drumPatternNames: Object.keys(drumPatterns),
|
|
@@ -5080,6 +5615,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5080
5615
|
});
|
|
5081
5616
|
}
|
|
5082
5617
|
return {
|
|
5618
|
+
id: trackState?.config.id,
|
|
5083
5619
|
model: lt.model,
|
|
5084
5620
|
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (masterVolume / 100),
|
|
5085
5621
|
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
@@ -5090,13 +5626,15 @@ var mountDAW = (target, options = {}) => {
|
|
|
5090
5626
|
const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
|
|
5091
5627
|
if (streaming && voices) {
|
|
5092
5628
|
const overlay = showLoadingOverlay(refs.rollContainer);
|
|
5629
|
+
setLoading(true);
|
|
5093
5630
|
try {
|
|
5094
5631
|
await voices.loadModels(streamTracks.map((t) => t.model));
|
|
5095
5632
|
await voices.warm(streamTracks);
|
|
5096
|
-
} catch (
|
|
5097
|
-
console.warn("[dtm] voice preload failed",
|
|
5633
|
+
} catch (err2) {
|
|
5634
|
+
console.warn("[dtm] voice preload failed", err2);
|
|
5098
5635
|
} finally {
|
|
5099
5636
|
overlay.remove();
|
|
5637
|
+
setLoading(false);
|
|
5100
5638
|
}
|
|
5101
5639
|
}
|
|
5102
5640
|
if (playbackState !== "paused") {
|
|
@@ -5110,7 +5648,9 @@ var mountDAW = (target, options = {}) => {
|
|
|
5110
5648
|
playbackState = "playing";
|
|
5111
5649
|
sequencer.start(fromStep);
|
|
5112
5650
|
if (streaming && voices) {
|
|
5113
|
-
voices.startStream(streamTracks, sequencer.getStartTime()
|
|
5651
|
+
voices.startStream(streamTracks, sequencer.getStartTime(), {
|
|
5652
|
+
isAudible: (t) => !isSolo || t.id === activeTrackId
|
|
5653
|
+
});
|
|
5114
5654
|
}
|
|
5115
5655
|
updateTransport();
|
|
5116
5656
|
};
|
|
@@ -5443,21 +5983,28 @@ var mountDAW = (target, options = {}) => {
|
|
|
5443
5983
|
barLimit: barLimitBars
|
|
5444
5984
|
};
|
|
5445
5985
|
}
|
|
5446
|
-
const trackLines =
|
|
5447
|
-
|
|
5448
|
-
)
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5986
|
+
const trackLines = [];
|
|
5987
|
+
const trackLinesMini = [];
|
|
5988
|
+
trackStates.forEach((t, i) => {
|
|
5989
|
+
const notes = clipNotes(t.core.getNotes());
|
|
5990
|
+
if (notes.length > 0) {
|
|
5991
|
+
const mml = t.core.getMMLFromNotes(notes, bpm, t.volume).trim();
|
|
5992
|
+
trackLines.push(`@${i} ${mml}`);
|
|
5993
|
+
trackLinesMini.push(`@${i}${mml.replace(/\s+/g, "")}`);
|
|
5994
|
+
}
|
|
5995
|
+
});
|
|
5452
5996
|
const lyricLines = trackStates.map((t, i) => ({
|
|
5453
5997
|
i,
|
|
5454
|
-
|
|
5998
|
+
notes: clipNotes(t.core.getNotes()),
|
|
5999
|
+
text: t.lyrics.replace(/[\r\n]+/g, " ").trim(),
|
|
5455
6000
|
model: t.lyricModel.trim(),
|
|
5456
6001
|
vol: t.vocalVolume,
|
|
5457
6002
|
gate: t.vocalGate,
|
|
5458
6003
|
pan: t.vocalPan,
|
|
5459
6004
|
oct: t.vocalOctave
|
|
5460
|
-
})).filter(
|
|
6005
|
+
})).filter(
|
|
6006
|
+
(x) => x.model.length > 0 && x.text.length > 0 && x.notes.length > 0
|
|
6007
|
+
).map((x) => {
|
|
5461
6008
|
const params = [
|
|
5462
6009
|
x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
|
|
5463
6010
|
x.gate === 100 ? "" : `q${x.gate}`,
|
|
@@ -5473,7 +6020,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5473
6020
|
full,
|
|
5474
6021
|
minified,
|
|
5475
6022
|
ignoredCount: 0,
|
|
5476
|
-
trackCount:
|
|
6023
|
+
trackCount: trackLines.length,
|
|
5477
6024
|
barLimit: barLimitBars
|
|
5478
6025
|
};
|
|
5479
6026
|
};
|
|
@@ -5593,7 +6140,6 @@ var mountDAW = (target, options = {}) => {
|
|
|
5593
6140
|
updateUndoRedo();
|
|
5594
6141
|
};
|
|
5595
6142
|
const applyChord = () => {
|
|
5596
|
-
if (!options.parseChord || !options.parseChords) return;
|
|
5597
6143
|
const active = getActive();
|
|
5598
6144
|
const chordTrack = trackStates.find((t) => t.config.id === "chord");
|
|
5599
6145
|
if (!chordTrack) return;
|
|
@@ -5602,9 +6148,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5602
6148
|
patternType: active.savedChordPattern,
|
|
5603
6149
|
rootShift: active.savedChordRoot,
|
|
5604
6150
|
bpm,
|
|
5605
|
-
stepsPerBar: renderConfig.stepsPerBar
|
|
5606
|
-
parseChord: options.parseChord,
|
|
5607
|
-
parseChords: options.parseChords
|
|
6151
|
+
stepsPerBar: renderConfig.stepsPerBar
|
|
5608
6152
|
});
|
|
5609
6153
|
chordTrack.core.clearNotesWithoutHistory();
|
|
5610
6154
|
chordTrack.core.beginBatch();
|
|
@@ -5689,9 +6233,11 @@ var mountDAW = (target, options = {}) => {
|
|
|
5689
6233
|
};
|
|
5690
6234
|
const overlayDuring = (fn) => {
|
|
5691
6235
|
refs.overlay.hidden = false;
|
|
6236
|
+
setLoading(true);
|
|
5692
6237
|
setTimeout(() => {
|
|
5693
6238
|
fn();
|
|
5694
6239
|
refs.overlay.hidden = true;
|
|
6240
|
+
setLoading(false);
|
|
5695
6241
|
}, 30);
|
|
5696
6242
|
};
|
|
5697
6243
|
const wireEvents = () => {
|
|
@@ -5835,6 +6381,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5835
6381
|
const file = refs.midiInput.files?.[0];
|
|
5836
6382
|
if (!file || !options.parseMidi) return;
|
|
5837
6383
|
refs.overlay.hidden = false;
|
|
6384
|
+
setLoading(true);
|
|
5838
6385
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
5839
6386
|
pendingMidi = options.parseMidi(buffer);
|
|
5840
6387
|
detectedTracks = analyzeMidiTracks(pendingMidi);
|
|
@@ -5855,6 +6402,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5855
6402
|
});
|
|
5856
6403
|
refs.midiTrackSelection.classList.remove("dtm-hidden");
|
|
5857
6404
|
refs.overlay.hidden = true;
|
|
6405
|
+
setLoading(false);
|
|
5858
6406
|
});
|
|
5859
6407
|
refs.midiLoadBtn.addEventListener("click", () => {
|
|
5860
6408
|
if (!pendingMidi) return;
|
|
@@ -5928,6 +6476,9 @@ var mountDAW = (target, options = {}) => {
|
|
|
5928
6476
|
resizeObserver.observe(refs.rollContainer);
|
|
5929
6477
|
document.addEventListener("pointermove", onPointerMove);
|
|
5930
6478
|
document.addEventListener("pointerup", onPointerUp);
|
|
6479
|
+
const setLoading = (loading) => {
|
|
6480
|
+
refs.topbar.classList.toggle("is-loading", loading);
|
|
6481
|
+
};
|
|
5931
6482
|
return {
|
|
5932
6483
|
play,
|
|
5933
6484
|
pause,
|
|
@@ -5965,6 +6516,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5965
6516
|
exportMIDI: exportMIDI2,
|
|
5966
6517
|
setBpm,
|
|
5967
6518
|
getPlaybackState: () => playbackState,
|
|
6519
|
+
setLoading,
|
|
5968
6520
|
destroy: () => {
|
|
5969
6521
|
sequencer.stop();
|
|
5970
6522
|
resizeObserver.disconnect();
|
|
@@ -6109,6 +6661,117 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6109
6661
|
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
6110
6662
|
(a, b) => a - b
|
|
6111
6663
|
);
|
|
6664
|
+
const trackStats = trackIndices.map((index) => {
|
|
6665
|
+
const trackPlacements = placements.filter((p) => p.trackIndex === index);
|
|
6666
|
+
if (trackPlacements.length === 0) {
|
|
6667
|
+
return { index, isChords: false, avgPitch: 0, noteCount: 0 };
|
|
6668
|
+
}
|
|
6669
|
+
const sumPitch = trackPlacements.reduce((sum, p) => sum + p.pitch, 0);
|
|
6670
|
+
const avgPitch = sumPitch / trackPlacements.length;
|
|
6671
|
+
const stepCounts = /* @__PURE__ */ new Map();
|
|
6672
|
+
for (const p of trackPlacements) {
|
|
6673
|
+
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6674
|
+
stepCounts.set(s, (stepCounts.get(s) ?? 0) + 1);
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
const polyphonicSteps = Array.from(stepCounts.values()).filter(
|
|
6678
|
+
(c) => c >= 2
|
|
6679
|
+
).length;
|
|
6680
|
+
const isChords = polyphonicSteps > 0;
|
|
6681
|
+
return {
|
|
6682
|
+
index,
|
|
6683
|
+
isChords,
|
|
6684
|
+
avgPitch,
|
|
6685
|
+
noteCount: trackPlacements.length
|
|
6686
|
+
};
|
|
6687
|
+
});
|
|
6688
|
+
const chordTrackIndices = trackStats.filter((s) => s.isChords).map((s) => s.index);
|
|
6689
|
+
const nonChordTracks = trackStats.filter(
|
|
6690
|
+
(s) => !s.isChords && s.noteCount > 0
|
|
6691
|
+
);
|
|
6692
|
+
let bassTrackIndex = null;
|
|
6693
|
+
if (nonChordTracks.length > 0) {
|
|
6694
|
+
nonChordTracks.sort((a, b) => a.avgPitch - b.avgPitch);
|
|
6695
|
+
bassTrackIndex = nonChordTracks[0].index;
|
|
6696
|
+
}
|
|
6697
|
+
const priorityTrackIndices = /* @__PURE__ */ new Set();
|
|
6698
|
+
for (const idx of chordTrackIndices) {
|
|
6699
|
+
priorityTrackIndices.add(idx);
|
|
6700
|
+
}
|
|
6701
|
+
if (bassTrackIndex !== null) {
|
|
6702
|
+
priorityTrackIndices.add(bassTrackIndex);
|
|
6703
|
+
}
|
|
6704
|
+
const hasPriorityTracksInMml = priorityTrackIndices.size > 0;
|
|
6705
|
+
const maxStep = placements.reduce(
|
|
6706
|
+
(max, p) => Math.max(max, p.startStep + p.durationSteps),
|
|
6707
|
+
0
|
|
6708
|
+
);
|
|
6709
|
+
const priorityPitches = Array.from(
|
|
6710
|
+
{ length: maxStep + 1 },
|
|
6711
|
+
() => /* @__PURE__ */ new Set()
|
|
6712
|
+
);
|
|
6713
|
+
const allPitches = Array.from(
|
|
6714
|
+
{ length: maxStep + 1 },
|
|
6715
|
+
() => /* @__PURE__ */ new Set()
|
|
6716
|
+
);
|
|
6717
|
+
for (const p of placements) {
|
|
6718
|
+
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6719
|
+
if (s >= 0 && s <= maxStep) {
|
|
6720
|
+
allPitches[s].add(p.pitch);
|
|
6721
|
+
if (priorityTrackIndices.has(p.trackIndex)) {
|
|
6722
|
+
priorityPitches[s].add(p.pitch);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
const stepChords = [];
|
|
6728
|
+
const chordCache = /* @__PURE__ */ new Map();
|
|
6729
|
+
let lastChord = "";
|
|
6730
|
+
let hasPriorityStarted = false;
|
|
6731
|
+
const GRID_SIZE = 48;
|
|
6732
|
+
const MIN_DURATION = 12;
|
|
6733
|
+
for (let g = 0; g <= Math.ceil(maxStep / GRID_SIZE); g++) {
|
|
6734
|
+
const startS = g * GRID_SIZE;
|
|
6735
|
+
const endS = Math.min(maxStep, (g + 1) * GRID_SIZE - 1);
|
|
6736
|
+
if (startS > maxStep) break;
|
|
6737
|
+
for (let s = startS; s <= endS; s++) {
|
|
6738
|
+
const priorityPitchesAtStep = Array.from(priorityPitches[s]);
|
|
6739
|
+
if (priorityPitchesAtStep.length > 0) {
|
|
6740
|
+
hasPriorityStarted = true;
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
const pitchDurations = /* @__PURE__ */ new Map();
|
|
6744
|
+
for (let s = startS; s <= endS; s++) {
|
|
6745
|
+
const usePriority = hasPriorityStarted && hasPriorityTracksInMml;
|
|
6746
|
+
const pitches = usePriority ? Array.from(priorityPitches[s]) : Array.from(allPitches[s]);
|
|
6747
|
+
for (const p of pitches) {
|
|
6748
|
+
pitchDurations.set(p, (pitchDurations.get(p) ?? 0) + 1);
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
let activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur >= MIN_DURATION).map(([p, _]) => p);
|
|
6752
|
+
if (activePitches.length === 0 && pitchDurations.size > 0) {
|
|
6753
|
+
const maxDur = Math.max(...pitchDurations.values());
|
|
6754
|
+
activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur === maxDur).map(([p, _]) => p);
|
|
6755
|
+
}
|
|
6756
|
+
let gridChord = lastChord;
|
|
6757
|
+
if (activePitches.length > 0) {
|
|
6758
|
+
const sortedPitches = activePitches.sort((a, b) => a - b);
|
|
6759
|
+
const cacheKey = sortedPitches.join(",");
|
|
6760
|
+
if (chordCache.has(cacheKey)) {
|
|
6761
|
+
const chordName = chordCache.get(cacheKey);
|
|
6762
|
+
if (chordName) gridChord = chordName;
|
|
6763
|
+
} else {
|
|
6764
|
+
const candidates = detectChord(sortedPitches);
|
|
6765
|
+
const chordName = candidates[0]?.symbol ?? "";
|
|
6766
|
+
if (chordName) gridChord = chordName;
|
|
6767
|
+
chordCache.set(cacheKey, chordName);
|
|
6768
|
+
}
|
|
6769
|
+
}
|
|
6770
|
+
for (let s = startS; s <= endS; s++) {
|
|
6771
|
+
stepChords[s] = gridChord;
|
|
6772
|
+
}
|
|
6773
|
+
lastChord = gridChord;
|
|
6774
|
+
}
|
|
6112
6775
|
const seqTracks = trackIndices.map((index) => {
|
|
6113
6776
|
let id = 0;
|
|
6114
6777
|
const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
|
|
@@ -6309,6 +6972,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6309
6972
|
barEl.className = "dtm-player-bar";
|
|
6310
6973
|
barEl.textContent = "-";
|
|
6311
6974
|
beatRow.appendChild(barEl);
|
|
6975
|
+
const chordEl = doc.createElement("span");
|
|
6976
|
+
chordEl.className = "dtm-player-chord";
|
|
6977
|
+
chordEl.textContent = "";
|
|
6978
|
+
beatRow.appendChild(chordEl);
|
|
6312
6979
|
const chips = [];
|
|
6313
6980
|
const makeChip = (label) => {
|
|
6314
6981
|
const chip = doc.createElement("span");
|
|
@@ -6452,10 +7119,20 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6452
7119
|
lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
|
|
6453
7120
|
};
|
|
6454
7121
|
const renderPlayhead = (step) => {
|
|
7122
|
+
const intStep = Math.floor(step);
|
|
6455
7123
|
const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
|
|
6456
7124
|
for (let i = 0; i < 4; i++)
|
|
6457
7125
|
beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
|
|
6458
7126
|
barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
|
|
7127
|
+
const chordName = stepChords[intStep] ?? "";
|
|
7128
|
+
if (chordEl.textContent !== chordName) {
|
|
7129
|
+
chordEl.textContent = chordName;
|
|
7130
|
+
if (chordName) {
|
|
7131
|
+
console.log(
|
|
7132
|
+
`[dtm-player-chord] Active Chord: ${chordName} (step: ${intStep})`
|
|
7133
|
+
);
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
6459
7136
|
for (const view of laneViews) {
|
|
6460
7137
|
let active = null;
|
|
6461
7138
|
for (const t of view.tokens) {
|
|
@@ -6469,6 +7146,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6469
7146
|
const resetPlayhead = () => {
|
|
6470
7147
|
for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
|
|
6471
7148
|
barEl.textContent = "-";
|
|
7149
|
+
chordEl.textContent = "";
|
|
6472
7150
|
for (const view of laneViews) {
|
|
6473
7151
|
for (const t of view.tokens) t.el.classList.remove("is-active");
|
|
6474
7152
|
view.lane.scrollLeft = 0;
|
|
@@ -6532,6 +7210,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6532
7210
|
});
|
|
6533
7211
|
}
|
|
6534
7212
|
return {
|
|
7213
|
+
id: String(index),
|
|
6535
7214
|
model: lt.model,
|
|
6536
7215
|
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
|
|
6537
7216
|
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
@@ -6547,15 +7226,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6547
7226
|
try {
|
|
6548
7227
|
await v.loadModels(tracks.map((t) => t.model));
|
|
6549
7228
|
await v.warm(tracks);
|
|
6550
|
-
} catch (
|
|
6551
|
-
console.warn("[dtm] voice preload failed",
|
|
7229
|
+
} catch (err2) {
|
|
7230
|
+
console.warn("[dtm] voice preload failed", err2);
|
|
6552
7231
|
} finally {
|
|
6553
7232
|
overlay.remove();
|
|
6554
7233
|
}
|
|
6555
7234
|
if (!playing || activePlayer !== instance) return;
|
|
6556
7235
|
}
|
|
6557
7236
|
seq.start(0);
|
|
6558
|
-
if (streaming)
|
|
7237
|
+
if (streaming) {
|
|
7238
|
+
ensureVoices().startStream(tracks, seq.getStartTime(), {
|
|
7239
|
+
isAudible: (t) => !mutedTracks.has(Number(t.id))
|
|
7240
|
+
});
|
|
7241
|
+
}
|
|
6559
7242
|
};
|
|
6560
7243
|
const play = () => {
|
|
6561
7244
|
if (playing || trackIndices.length === 0) return;
|
|
@@ -6890,8 +7573,6 @@ var DEFAULT_CDN = {
|
|
|
6890
7573
|
soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
|
|
6891
7574
|
soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
|
|
6892
7575
|
soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs",
|
|
6893
|
-
parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs",
|
|
6894
|
-
parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs",
|
|
6895
7576
|
midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
|
|
6896
7577
|
};
|
|
6897
7578
|
var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
|
|
@@ -6935,24 +7616,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
6935
7616
|
eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
|
|
6936
7617
|
eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
|
|
6937
7618
|
]);
|
|
6938
|
-
let parseChord = eng.parseChord;
|
|
6939
|
-
let parseChords = eng.parseChords;
|
|
6940
|
-
if (features.chord && (!parseChord || !parseChords)) {
|
|
6941
|
-
try {
|
|
6942
|
-
[parseChord, parseChords] = await Promise.all([
|
|
6943
|
-
parseChord ?? importFrom(
|
|
6944
|
-
cdn.parseChord,
|
|
6945
|
-
"parseChord"
|
|
6946
|
-
),
|
|
6947
|
-
parseChords ?? importFrom(
|
|
6948
|
-
cdn.parseChords,
|
|
6949
|
-
"parseChords"
|
|
6950
|
-
)
|
|
6951
|
-
]);
|
|
6952
|
-
} catch (e) {
|
|
6953
|
-
console.warn("[dtm] \u30B3\u30FC\u30C9\u89E3\u6790\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F", e);
|
|
6954
|
-
}
|
|
6955
|
-
}
|
|
6956
7619
|
let midiParser = null;
|
|
6957
7620
|
let parseMidi;
|
|
6958
7621
|
if (features.midi) {
|
|
@@ -7138,8 +7801,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
7138
7801
|
onPlayNote: playNote,
|
|
7139
7802
|
onPlayDrum: playDrum,
|
|
7140
7803
|
singingVoices,
|
|
7141
|
-
parseChord,
|
|
7142
|
-
parseChords,
|
|
7143
7804
|
parseMidi,
|
|
7144
7805
|
onToggleRecord,
|
|
7145
7806
|
...dawOverrides
|
|
@@ -7161,15 +7822,32 @@ var createDtmStudio = async (options = {}) => {
|
|
|
7161
7822
|
editorPresetSelects.set(target, select);
|
|
7162
7823
|
select.addEventListener("change", async () => {
|
|
7163
7824
|
if (!select) return;
|
|
7164
|
-
daw.
|
|
7165
|
-
|
|
7825
|
+
const wasPlaying = daw.getPlaybackState() === "playing";
|
|
7826
|
+
if (wasPlaying) {
|
|
7827
|
+
daw.pause();
|
|
7828
|
+
}
|
|
7829
|
+
const overlay = showLoadingOverlay(target);
|
|
7830
|
+
daw.setLoading?.(true);
|
|
7831
|
+
try {
|
|
7832
|
+
daw.setInstrument(select.value);
|
|
7833
|
+
await loadPreset(select.value, trackIds);
|
|
7834
|
+
} finally {
|
|
7835
|
+
overlay.remove();
|
|
7836
|
+
daw.setLoading?.(false);
|
|
7837
|
+
if (wasPlaying) {
|
|
7838
|
+
daw.play();
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7166
7841
|
});
|
|
7167
7842
|
}
|
|
7168
7843
|
const daw = mountDAW(target, base);
|
|
7169
7844
|
mountedEditors.push(daw);
|
|
7170
7845
|
const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
|
|
7171
7846
|
daw.setInstrument(presetKey);
|
|
7172
|
-
|
|
7847
|
+
daw.setLoading?.(true);
|
|
7848
|
+
void loadPreset(presetKey, trackIds).finally(() => {
|
|
7849
|
+
daw.setLoading?.(false);
|
|
7850
|
+
});
|
|
7173
7851
|
const destroy = () => {
|
|
7174
7852
|
daw.destroy();
|
|
7175
7853
|
select?.remove();
|