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