@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.mjs
CHANGED
|
@@ -55,18 +55,534 @@ async function buildNameToKeyMapping() {
|
|
|
55
55
|
return nameToKey;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// node_modules/.pnpm/@onjmin+chord-parser@1.0.2/node_modules/@onjmin/chord-parser/dist/index.mjs
|
|
59
|
+
var SHARP_NAMES = [
|
|
60
|
+
"C",
|
|
61
|
+
"C#",
|
|
62
|
+
"D",
|
|
63
|
+
"D#",
|
|
64
|
+
"E",
|
|
65
|
+
"F",
|
|
66
|
+
"F#",
|
|
67
|
+
"G",
|
|
68
|
+
"G#",
|
|
69
|
+
"A",
|
|
70
|
+
"A#",
|
|
71
|
+
"B"
|
|
72
|
+
];
|
|
73
|
+
var FLAT_NAMES = [
|
|
74
|
+
"C",
|
|
75
|
+
"Db",
|
|
76
|
+
"D",
|
|
77
|
+
"Eb",
|
|
78
|
+
"E",
|
|
79
|
+
"F",
|
|
80
|
+
"Gb",
|
|
81
|
+
"G",
|
|
82
|
+
"Ab",
|
|
83
|
+
"A",
|
|
84
|
+
"Bb",
|
|
85
|
+
"B"
|
|
86
|
+
];
|
|
87
|
+
var toPitchClass = (n) => (n % 12 + 12) % 12;
|
|
88
|
+
var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
|
|
89
|
+
var SyntaxErrorWithPos = class extends Error {
|
|
90
|
+
constructor(input, msg) {
|
|
91
|
+
super(
|
|
92
|
+
`SyntaxError: ${msg}
|
|
93
|
+
input.idx: ${input.idx}
|
|
94
|
+
input.str: ${input.str}`
|
|
95
|
+
);
|
|
96
|
+
this.name = "ChordSyntaxError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var err = (input, msg) => {
|
|
100
|
+
throw new SyntaxErrorWithPos(input, msg);
|
|
101
|
+
};
|
|
102
|
+
var Input = class _Input {
|
|
103
|
+
static nums = new Set("0123456789");
|
|
104
|
+
str;
|
|
105
|
+
nest;
|
|
106
|
+
idx;
|
|
107
|
+
constructor(str, nest = 0) {
|
|
108
|
+
this.str = str;
|
|
109
|
+
this.nest = nest;
|
|
110
|
+
this.idx = 0;
|
|
111
|
+
}
|
|
112
|
+
get isEOF() {
|
|
113
|
+
return this.str.length <= this.idx;
|
|
114
|
+
}
|
|
115
|
+
get char() {
|
|
116
|
+
return this.str[this.idx];
|
|
117
|
+
}
|
|
118
|
+
/** 先頭の連続する数字を消費して数値で返す(破壊的)。数字が無ければ null。 */
|
|
119
|
+
get num() {
|
|
120
|
+
let str = "";
|
|
121
|
+
while (!this.isEOF) {
|
|
122
|
+
const char = this.char;
|
|
123
|
+
if (!_Input.nums.has(char)) break;
|
|
124
|
+
str += char;
|
|
125
|
+
this.idx++;
|
|
126
|
+
}
|
|
127
|
+
return str.length ? Number(str) : null;
|
|
128
|
+
}
|
|
129
|
+
slice(i) {
|
|
130
|
+
return this.str.slice(this.idx, this.idx + i);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var Output = class {
|
|
134
|
+
pitch = null;
|
|
135
|
+
chord = null;
|
|
136
|
+
isChord = false;
|
|
137
|
+
pending = null;
|
|
138
|
+
nest = -1;
|
|
139
|
+
get value() {
|
|
140
|
+
const { pitch, chord } = this;
|
|
141
|
+
return new Set(
|
|
142
|
+
[...chord].map((v) => v + pitch)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
set value(chord) {
|
|
146
|
+
const pitch = this.pitch;
|
|
147
|
+
this.chord = new Set([...chord].map((v) => v - pitch));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var Matcher = class {
|
|
151
|
+
map = /* @__PURE__ */ new Map();
|
|
152
|
+
/** キー長を降順で保持(最長一致のため)。 */
|
|
153
|
+
lengths = [];
|
|
154
|
+
_set(key, value) {
|
|
155
|
+
this.map.set(key, value);
|
|
156
|
+
if (!this.lengths.includes(key.length)) {
|
|
157
|
+
this.lengths.push(key.length);
|
|
158
|
+
this.lengths.sort((a, b) => b - a);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
set(key, value) {
|
|
162
|
+
if (Array.isArray(key)) for (const k of key) this._set(k, value);
|
|
163
|
+
else this._set(key, value);
|
|
164
|
+
}
|
|
165
|
+
parse(input) {
|
|
166
|
+
for (const i of this.lengths) {
|
|
167
|
+
const s = input.slice(i);
|
|
168
|
+
if (this.map.has(s)) {
|
|
169
|
+
input.idx += s.length;
|
|
170
|
+
return this.map.get(s);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var BRACKET_START = 0;
|
|
177
|
+
var BRACKET_END = 1;
|
|
178
|
+
var COMMA = 2;
|
|
179
|
+
var DIVIDE = 3;
|
|
180
|
+
var formulaMatcher = new Matcher();
|
|
181
|
+
formulaMatcher.set("(", BRACKET_START);
|
|
182
|
+
formulaMatcher.set(")", BRACKET_END);
|
|
183
|
+
formulaMatcher.set(",", COMMA);
|
|
184
|
+
formulaMatcher.set(["/", "on"], DIVIDE);
|
|
185
|
+
var parseFormula = (input, output = new Output(), nest = 0) => {
|
|
186
|
+
let start = input.idx;
|
|
187
|
+
const _eval = (idx) => {
|
|
188
|
+
const str = input.str.slice(start, idx);
|
|
189
|
+
if (str.length) parseTerm(new Input(str, nest), output);
|
|
190
|
+
};
|
|
191
|
+
while (true) {
|
|
192
|
+
const { idx } = input;
|
|
193
|
+
if (input.isEOF) {
|
|
194
|
+
if (nest) err(input, `Unclosed ${nest} brackets`);
|
|
195
|
+
_eval(idx);
|
|
196
|
+
return output;
|
|
197
|
+
}
|
|
198
|
+
const res = formulaMatcher.parse(input);
|
|
199
|
+
if (res === null) {
|
|
200
|
+
input.idx++;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const { pending } = output;
|
|
204
|
+
_eval(idx);
|
|
205
|
+
switch (res) {
|
|
206
|
+
case BRACKET_START:
|
|
207
|
+
parseFormula(input, output, nest + 1);
|
|
208
|
+
break;
|
|
209
|
+
case BRACKET_END:
|
|
210
|
+
if (nest - 1 < 0) err(input, "Unable to close brackets");
|
|
211
|
+
return output;
|
|
212
|
+
case COMMA:
|
|
213
|
+
output.pending = pending;
|
|
214
|
+
break;
|
|
215
|
+
case DIVIDE: {
|
|
216
|
+
const o = parseFormula(input, new Output(), nest);
|
|
217
|
+
const v = [...output.value];
|
|
218
|
+
if (o.isChord) {
|
|
219
|
+
output.value = [...o.value].concat(v);
|
|
220
|
+
} else {
|
|
221
|
+
const a = v.sort((x, y) => x - y);
|
|
222
|
+
const pitch = (o.pitch + 3) % 12 - 3;
|
|
223
|
+
if (a[0] < pitch) {
|
|
224
|
+
while (a[0] < pitch) a.push(a.shift() + 12);
|
|
225
|
+
} else {
|
|
226
|
+
while (true) {
|
|
227
|
+
const w = a[a.length - 1] - 12;
|
|
228
|
+
if (w < pitch) break;
|
|
229
|
+
a.pop();
|
|
230
|
+
a.unshift(w);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
a.push(pitch);
|
|
234
|
+
output.value = a;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
start = input.idx;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var parseTerm = (input, output) => {
|
|
243
|
+
if (input.isEOF) return output;
|
|
244
|
+
if (output.pitch === null) return parsePitch(input, output);
|
|
245
|
+
if (output.pending === null) return parseFunc(input, output);
|
|
246
|
+
return parsePending(input, output);
|
|
247
|
+
};
|
|
248
|
+
var halfMatcher = new Matcher();
|
|
249
|
+
var halfMatcherStrict = new Matcher();
|
|
250
|
+
for (const m of [halfMatcher, halfMatcherStrict]) {
|
|
251
|
+
m.set(["#", "\u266F"], 1);
|
|
252
|
+
m.set(["b", "\u266D"], -1);
|
|
253
|
+
}
|
|
254
|
+
halfMatcher.set("+", 1);
|
|
255
|
+
halfMatcher.set("-", -1);
|
|
256
|
+
var parseHalf = (input, isPitch = false) => (isPitch ? halfMatcherStrict : halfMatcher).parse(input);
|
|
257
|
+
var idx2pitch = [0, 2, 4, 5, 7, 9, 11];
|
|
258
|
+
for (const i of [...idx2pitch.keys()]) idx2pitch.push(idx2pitch[i] + 12);
|
|
259
|
+
var deg2pitch = (deg) => idx2pitch[deg - 1];
|
|
260
|
+
var pitchMatcher = new Matcher();
|
|
261
|
+
for (const [i, v] of [..."CDEFGAB"].entries())
|
|
262
|
+
pitchMatcher.set(v, idx2pitch[i]);
|
|
263
|
+
var parsePitch = (input, output) => {
|
|
264
|
+
const pitch = pitchMatcher.parse(input);
|
|
265
|
+
if (pitch === null) err(input, "Not found pitch");
|
|
266
|
+
output.pitch = pitch;
|
|
267
|
+
const half = parseHalf(input, true);
|
|
268
|
+
if (half !== null) output.pitch += half;
|
|
269
|
+
return parseBase(input, output);
|
|
270
|
+
};
|
|
271
|
+
var MAJOR = [0, 4, 7];
|
|
272
|
+
var DIM = [0, 3, 6];
|
|
273
|
+
var baseMatcher = new Matcher();
|
|
274
|
+
baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
|
|
275
|
+
baseMatcher.set(["dim", "\u3007"], DIM);
|
|
276
|
+
baseMatcher.set("+", [0, 4, 8]);
|
|
277
|
+
baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
|
|
278
|
+
var parseBase = (input, output) => {
|
|
279
|
+
const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
|
|
280
|
+
const res = isMajMarker ? null : baseMatcher.parse(input);
|
|
281
|
+
if (res !== null) output.isChord = true;
|
|
282
|
+
output.chord = new Set(res || MAJOR);
|
|
283
|
+
if (res === DIM) {
|
|
284
|
+
const { num } = input;
|
|
285
|
+
const chord = output.chord;
|
|
286
|
+
if (num !== null) chord.add(deg2pitch(num) - 2);
|
|
287
|
+
}
|
|
288
|
+
output.nest = input.nest;
|
|
289
|
+
return parseTerm(input, output);
|
|
290
|
+
};
|
|
291
|
+
var add = (chord, n, half) => {
|
|
292
|
+
chord.add(deg2pitch(n) + half);
|
|
293
|
+
};
|
|
294
|
+
var aug = (chord) => {
|
|
295
|
+
chord.delete(deg2pitch(5));
|
|
296
|
+
chord.add(deg2pitch(5) + 1);
|
|
297
|
+
};
|
|
298
|
+
var _7th = (chord, n, _half2, isFlat = false) => {
|
|
299
|
+
if (n === 5) chord.delete(deg2pitch(3));
|
|
300
|
+
else if (n === 6) chord.add(deg2pitch(6));
|
|
301
|
+
else if (n === 69) chord.add(deg2pitch(6)).add(deg2pitch(9));
|
|
302
|
+
else {
|
|
303
|
+
if (n >= 7) chord.add(deg2pitch(7) + (isFlat ? -1 : 0));
|
|
304
|
+
if (n >= 9) chord.add(deg2pitch(9));
|
|
305
|
+
if (n >= 11) chord.add(deg2pitch(11));
|
|
306
|
+
if (n >= 13) chord.add(deg2pitch(13));
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
var _half = (chord, n, half) => {
|
|
310
|
+
chord.delete(deg2pitch(n));
|
|
311
|
+
chord.add(deg2pitch(n) + half);
|
|
312
|
+
};
|
|
313
|
+
var funcMatcher = new Matcher();
|
|
314
|
+
funcMatcher.set("add", add);
|
|
315
|
+
funcMatcher.set(["omit", "no"], (chord, n, half) => {
|
|
316
|
+
chord.delete(deg2pitch(n) + half);
|
|
317
|
+
});
|
|
318
|
+
funcMatcher.set("sus", (chord, n, half) => {
|
|
319
|
+
chord.delete(deg2pitch(3));
|
|
320
|
+
chord.add(deg2pitch(n) + half);
|
|
321
|
+
});
|
|
322
|
+
funcMatcher.set(
|
|
323
|
+
["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
|
|
324
|
+
_7th
|
|
325
|
+
);
|
|
326
|
+
funcMatcher.set("aug", aug);
|
|
327
|
+
var parseFunc = (input, output) => {
|
|
328
|
+
if (!output.isChord) output.isChord = true;
|
|
329
|
+
const func = funcMatcher.parse(input);
|
|
330
|
+
const chord = output.chord;
|
|
331
|
+
if (func === null) {
|
|
332
|
+
const isAug = input.char === "+";
|
|
333
|
+
const half = parseHalf(input);
|
|
334
|
+
const { num } = input;
|
|
335
|
+
if (num === null) {
|
|
336
|
+
if (isAug) aug(chord);
|
|
337
|
+
else err(input, "Not found number");
|
|
338
|
+
}
|
|
339
|
+
if (half === null) {
|
|
340
|
+
if (input.nest === output.nest) _7th(chord, num, 0, true);
|
|
341
|
+
else add(chord, num, 0);
|
|
342
|
+
} else {
|
|
343
|
+
_half(chord, num, half);
|
|
344
|
+
}
|
|
345
|
+
} else if (func === aug) {
|
|
346
|
+
aug(chord);
|
|
347
|
+
} else {
|
|
348
|
+
output.pending = func;
|
|
349
|
+
}
|
|
350
|
+
return parseTerm(input, output);
|
|
351
|
+
};
|
|
352
|
+
var parsePending = (input, output) => {
|
|
353
|
+
const half = parseHalf(input);
|
|
354
|
+
const { num } = input;
|
|
355
|
+
const { pending, chord } = output;
|
|
356
|
+
if (num === null) err(input, "Not found number");
|
|
357
|
+
pending(
|
|
358
|
+
chord,
|
|
359
|
+
num,
|
|
360
|
+
half === null ? 0 : half
|
|
361
|
+
);
|
|
362
|
+
output.pending = null;
|
|
363
|
+
return parseTerm(input, output);
|
|
364
|
+
};
|
|
365
|
+
var parseChord = (symbol) => {
|
|
366
|
+
const output = parseFormula(new Input(symbol));
|
|
367
|
+
const notes = [...output.value].sort((a, b) => a - b);
|
|
368
|
+
const intervals = [...output.chord].sort((a, b) => a - b);
|
|
369
|
+
const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
|
|
370
|
+
(a, b) => a - b
|
|
371
|
+
);
|
|
372
|
+
return {
|
|
373
|
+
symbol,
|
|
374
|
+
root: toPitchClass(output.pitch),
|
|
375
|
+
notes,
|
|
376
|
+
pitchClasses,
|
|
377
|
+
intervals
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
var QUALITY_SOURCE = [
|
|
381
|
+
"",
|
|
382
|
+
// major
|
|
383
|
+
"m",
|
|
384
|
+
// minor
|
|
385
|
+
"7",
|
|
386
|
+
// dominant 7th
|
|
387
|
+
"M7",
|
|
388
|
+
// major 7th
|
|
389
|
+
"m7",
|
|
390
|
+
// minor 7th
|
|
391
|
+
"dim",
|
|
392
|
+
// diminished triad
|
|
393
|
+
"m7b5",
|
|
394
|
+
// half-diminished
|
|
395
|
+
"aug",
|
|
396
|
+
// augmented triad
|
|
397
|
+
"6",
|
|
398
|
+
// major 6th
|
|
399
|
+
"m6",
|
|
400
|
+
// minor 6th
|
|
401
|
+
"sus4",
|
|
402
|
+
"sus2",
|
|
403
|
+
"mM7",
|
|
404
|
+
// minor major 7th
|
|
405
|
+
"dim7",
|
|
406
|
+
// diminished 7th
|
|
407
|
+
"7sus4",
|
|
408
|
+
"7#5",
|
|
409
|
+
// augmented 7th
|
|
410
|
+
"add9",
|
|
411
|
+
"madd9",
|
|
412
|
+
"9",
|
|
413
|
+
"M9",
|
|
414
|
+
"m9",
|
|
415
|
+
"69",
|
|
416
|
+
"m69",
|
|
417
|
+
"5"
|
|
418
|
+
// power chord
|
|
419
|
+
];
|
|
420
|
+
var QUALITIES = QUALITY_SOURCE.map(
|
|
421
|
+
(quality, priority) => ({
|
|
422
|
+
quality,
|
|
423
|
+
pitchClasses: parseChord(`C${quality}`).pitchClasses,
|
|
424
|
+
priority
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
var QUALITY_BY_PCSET = (() => {
|
|
428
|
+
const map = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const def of QUALITIES) {
|
|
430
|
+
const key = def.pitchClasses.join(",");
|
|
431
|
+
if (!map.has(key)) map.set(key, def);
|
|
432
|
+
}
|
|
433
|
+
return map;
|
|
434
|
+
})();
|
|
435
|
+
var detectChord = (notes, options = {}) => {
|
|
436
|
+
if (!notes.length) return [];
|
|
437
|
+
const { flat = false } = options;
|
|
438
|
+
const pcs = [...new Set(notes.map(toPitchClass))].sort((a, b) => a - b);
|
|
439
|
+
const bass = toPitchClass(
|
|
440
|
+
options.bass ?? notes.reduce((m, v) => Math.min(m, v), notes[0])
|
|
441
|
+
);
|
|
442
|
+
const scored = [];
|
|
443
|
+
for (const root of pcs) {
|
|
444
|
+
const relSet = new Set(pcs.map((pc) => toPitchClass(pc - root)));
|
|
445
|
+
for (const def of QUALITY_BY_PCSET.values()) {
|
|
446
|
+
let matchCount = 0;
|
|
447
|
+
let hasRoot = false;
|
|
448
|
+
for (const pc of def.pitchClasses) {
|
|
449
|
+
if (relSet.has(pc)) {
|
|
450
|
+
matchCount++;
|
|
451
|
+
if (pc === 0) hasRoot = true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (!hasRoot) continue;
|
|
455
|
+
if (matchCount < Math.min(2, def.pitchClasses.length)) continue;
|
|
456
|
+
const missingCount = def.pitchClasses.length - matchCount;
|
|
457
|
+
let extraCount = 0;
|
|
458
|
+
const defSet = new Set(def.pitchClasses);
|
|
459
|
+
for (const pc of relSet) {
|
|
460
|
+
if (!defSet.has(pc)) {
|
|
461
|
+
extraCount++;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const score = matchCount * 2 - missingCount * 1 - extraCount * 0.5;
|
|
465
|
+
const rootSymbol = noteName(root, flat) + def.quality;
|
|
466
|
+
const inversion = root !== bass;
|
|
467
|
+
scored.push({
|
|
468
|
+
score,
|
|
469
|
+
priority: def.priority,
|
|
470
|
+
candidate: {
|
|
471
|
+
symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
|
|
472
|
+
rootSymbol,
|
|
473
|
+
root,
|
|
474
|
+
quality: def.quality,
|
|
475
|
+
bass,
|
|
476
|
+
inversion
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
scored.sort((a, b) => {
|
|
482
|
+
if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
|
|
483
|
+
if (a.candidate.inversion !== b.candidate.inversion)
|
|
484
|
+
return a.candidate.inversion ? 1 : -1;
|
|
485
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
486
|
+
return a.candidate.root - b.candidate.root;
|
|
487
|
+
});
|
|
488
|
+
return scored.map((s) => s.candidate);
|
|
489
|
+
};
|
|
490
|
+
var toneRoleWeight = (rel) => {
|
|
491
|
+
if (rel === 0) return 1.3;
|
|
492
|
+
if (rel === 3 || rel === 4) return 1.2;
|
|
493
|
+
if (rel === 10 || rel === 11) return 0.95;
|
|
494
|
+
if (rel === 6 || rel === 7 || rel === 8) return 0.7;
|
|
495
|
+
return 0.85;
|
|
496
|
+
};
|
|
497
|
+
var CHORD_TEMPLATES = (() => {
|
|
498
|
+
const templates = [];
|
|
499
|
+
for (let root = 0; root < 12; root++) {
|
|
500
|
+
for (const def of QUALITIES) {
|
|
501
|
+
const pcs = /* @__PURE__ */ new Set();
|
|
502
|
+
const weights = new Array(12).fill(0);
|
|
503
|
+
const rel = /* @__PURE__ */ new Set();
|
|
504
|
+
for (const relPc of def.pitchClasses) {
|
|
505
|
+
rel.add(relPc);
|
|
506
|
+
const pc = toPitchClass(relPc + root);
|
|
507
|
+
pcs.add(pc);
|
|
508
|
+
weights[pc] = toneRoleWeight(relPc);
|
|
509
|
+
}
|
|
510
|
+
templates.push({
|
|
511
|
+
root,
|
|
512
|
+
quality: def.quality,
|
|
513
|
+
priority: def.priority,
|
|
514
|
+
pcs,
|
|
515
|
+
weights,
|
|
516
|
+
rel
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return templates;
|
|
521
|
+
})();
|
|
522
|
+
var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
|
|
523
|
+
var parseChords = (str, bpm = 120) => {
|
|
524
|
+
const output = [];
|
|
525
|
+
const secBar = 60 / bpm * 4;
|
|
526
|
+
const frontChars = new Set("ABCDEFG_=%N");
|
|
527
|
+
let idx = 0;
|
|
528
|
+
let last = null;
|
|
529
|
+
for (const line of toHan(str).split("\n").map((v) => v.trim())) {
|
|
530
|
+
if (!line.length || /^#/.test(line)) continue;
|
|
531
|
+
for (const str2 of line.split(/[|ll→]/)) {
|
|
532
|
+
if (!str2.length) continue;
|
|
533
|
+
const when = idx++ * secBar;
|
|
534
|
+
const a = [];
|
|
535
|
+
for (let i = 0; i < str2.length; i++) {
|
|
536
|
+
const char = str2[i];
|
|
537
|
+
const prev = str2[i - 1];
|
|
538
|
+
const prev2 = str2.slice(i - 2, i);
|
|
539
|
+
if (!frontChars.has(char)) continue;
|
|
540
|
+
if (prev === "/" || prev2 === "on") continue;
|
|
541
|
+
if (prev2 === "N." && char === "C") continue;
|
|
542
|
+
a.push(i);
|
|
543
|
+
}
|
|
544
|
+
if (!a.length) continue;
|
|
545
|
+
const divide = 2 ** Math.ceil(Math.log2(a.length));
|
|
546
|
+
const unitTime = secBar / divide;
|
|
547
|
+
for (const [i, v] of a.entries()) {
|
|
548
|
+
const s = str2.slice(v, i === a.length - 1 ? str2.length : a[i + 1]).replace(/\s+/g, "");
|
|
549
|
+
const c = s[0];
|
|
550
|
+
if (c === "_" || c === "N") {
|
|
551
|
+
last = null;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (c === "=") {
|
|
555
|
+
if (last) last.duration += unitTime;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const _when = when + i * unitTime;
|
|
559
|
+
if (c === "%") {
|
|
560
|
+
if (last === null) continue;
|
|
561
|
+
const base = last;
|
|
562
|
+
last = { ...base, when: _when, duration: unitTime };
|
|
563
|
+
} else {
|
|
564
|
+
const key = s.slice(0, s[1] === "#" ? 2 : 1);
|
|
565
|
+
const chord = s.slice(key.length).replace(/[\s・]/g, "");
|
|
566
|
+
last = {
|
|
567
|
+
key,
|
|
568
|
+
chord,
|
|
569
|
+
when: _when,
|
|
570
|
+
duration: unitTime
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
output.push(last);
|
|
574
|
+
}
|
|
575
|
+
if (last !== null && divide > a.length)
|
|
576
|
+
last.duration += unitTime * (divide - a.length);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return output;
|
|
580
|
+
};
|
|
581
|
+
|
|
58
582
|
// src/chords.ts
|
|
59
583
|
var C3 = 48;
|
|
60
584
|
var buildChordPlacements = (options) => {
|
|
61
|
-
const {
|
|
62
|
-
chordStr,
|
|
63
|
-
patternType,
|
|
64
|
-
rootShift,
|
|
65
|
-
bpm,
|
|
66
|
-
stepsPerBar,
|
|
67
|
-
parseChord,
|
|
68
|
-
parseChords
|
|
69
|
-
} = options;
|
|
585
|
+
const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
|
|
70
586
|
const placements = [];
|
|
71
587
|
if (!chordStr.trim()) return placements;
|
|
72
588
|
const offset = rootShift;
|
|
@@ -96,7 +612,7 @@ var buildChordPlacements = (options) => {
|
|
|
96
612
|
for (const chord of group) {
|
|
97
613
|
let notes;
|
|
98
614
|
try {
|
|
99
|
-
notes = [...parseChord(`${chord.key}${chord.chord}`).
|
|
615
|
+
notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
|
|
100
616
|
} catch {
|
|
101
617
|
continue;
|
|
102
618
|
}
|
|
@@ -183,7 +699,7 @@ var buildChordPlacements = (options) => {
|
|
|
183
699
|
chordNames.forEach((chordName, barIndex) => {
|
|
184
700
|
let notes;
|
|
185
701
|
try {
|
|
186
|
-
notes = [...parseChord(chordName).
|
|
702
|
+
notes = [...parseChord(chordName).notes];
|
|
187
703
|
} catch {
|
|
188
704
|
return;
|
|
189
705
|
}
|
|
@@ -257,6 +773,7 @@ var buildUI = (target, options) => {
|
|
|
257
773
|
<button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
|
|
258
774
|
<button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
|
|
259
775
|
<label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
|
|
776
|
+
<span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
|
|
260
777
|
<span class="dtm-grow"></span>
|
|
261
778
|
<span class="dtm-label">BPM</span>
|
|
262
779
|
<input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
|
|
@@ -349,10 +866,10 @@ var buildUI = (target, options) => {
|
|
|
349
866
|
<div class="dtm-row dtm-hidden" data-dtm="midi-track-selection"></div>
|
|
350
867
|
<div class="dtm-row">
|
|
351
868
|
<span class="dtm-label">MML</span>
|
|
352
|
-
<textarea class="dtm-textarea" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
|
|
869
|
+
<textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
|
|
870
|
+
<button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">\u8AAD\u8FBC</button>
|
|
353
871
|
</div>
|
|
354
872
|
<div class="dtm-row">
|
|
355
|
-
<button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">MML\u8AAD\u8FBC</button>
|
|
356
873
|
<span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
|
|
357
874
|
<select class="dtm-select" data-dtm="shift-select">
|
|
358
875
|
<option value="-96">-2\u5206</option>
|
|
@@ -426,6 +943,8 @@ var buildUI = (target, options) => {
|
|
|
426
943
|
const sel = (name) => q(root, `[data-dtm="${name}"]`);
|
|
427
944
|
return {
|
|
428
945
|
root,
|
|
946
|
+
topbar: sel("transport"),
|
|
947
|
+
topbarLoading: sel("topbar-loading"),
|
|
429
948
|
playBtn: sel("play"),
|
|
430
949
|
recBtn: sel("rec"),
|
|
431
950
|
soloCheckbox: sel("solo"),
|
|
@@ -1643,8 +2162,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
1643
2162
|
))().then((v) => {
|
|
1644
2163
|
loaded.set(m, v);
|
|
1645
2164
|
return v;
|
|
1646
|
-
}).catch((
|
|
1647
|
-
console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`,
|
|
2165
|
+
}).catch((err2) => {
|
|
2166
|
+
console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err2);
|
|
1648
2167
|
return null;
|
|
1649
2168
|
});
|
|
1650
2169
|
loading.set(m, p);
|
|
@@ -1685,42 +2204,33 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
1685
2204
|
}
|
|
1686
2205
|
await Promise.all(promises);
|
|
1687
2206
|
};
|
|
1688
|
-
const startStream = (tracks, anchorTime) => {
|
|
2207
|
+
const startStream = (tracks, anchorTime, opts) => {
|
|
1689
2208
|
const session = ++streamSession;
|
|
1690
|
-
const
|
|
1691
|
-
for (const track of tracks) {
|
|
2209
|
+
const runTrack = async (track) => {
|
|
1692
2210
|
const model = loaded.get(track.model.toLowerCase());
|
|
1693
|
-
if (!model)
|
|
2211
|
+
if (!model) return;
|
|
2212
|
+
const items = [];
|
|
1694
2213
|
forEachSungNote(track, (note, prevVowel) => {
|
|
1695
|
-
items.push({
|
|
1696
|
-
model,
|
|
1697
|
-
note,
|
|
1698
|
-
prevVowel,
|
|
1699
|
-
volume: track.volume,
|
|
1700
|
-
pan: track.pan
|
|
1701
|
-
});
|
|
2214
|
+
items.push({ note, prevVowel });
|
|
1702
2215
|
});
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
void (async () => {
|
|
1706
|
-
for (const item of items) {
|
|
2216
|
+
const peak = Math.max(1e-4, track.volume);
|
|
2217
|
+
for (const { note, prevVowel } of items) {
|
|
1707
2218
|
if (session !== streamSession) return;
|
|
1708
|
-
while (
|
|
2219
|
+
while (note.startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
|
|
1709
2220
|
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
|
|
1710
2221
|
if (session !== streamSession) return;
|
|
1711
2222
|
}
|
|
1712
|
-
|
|
1713
|
-
const
|
|
1714
|
-
const { model, note } = item;
|
|
2223
|
+
if (opts?.isAudible && !opts.isAudible(track)) continue;
|
|
2224
|
+
const t0 = anchorTime + note.startSec;
|
|
1715
2225
|
if (model.renderToCache && model.scheduleCached) {
|
|
1716
2226
|
const key = await model.renderToCache(
|
|
1717
2227
|
note.syllable,
|
|
1718
|
-
|
|
2228
|
+
prevVowel,
|
|
1719
2229
|
note.pitch,
|
|
1720
2230
|
note.durationSec * 1e3
|
|
1721
2231
|
);
|
|
1722
2232
|
if (session !== streamSession) return;
|
|
1723
|
-
if (key) model.scheduleCached(key, t0, peak,
|
|
2233
|
+
if (key) model.scheduleCached(key, t0, peak, track.pan);
|
|
1724
2234
|
} else {
|
|
1725
2235
|
const when = t0 - ctx.currentTime;
|
|
1726
2236
|
model(note.syllable, {
|
|
@@ -1730,12 +2240,13 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
1730
2240
|
volume: peak,
|
|
1731
2241
|
when,
|
|
1732
2242
|
duration: note.durationSec,
|
|
1733
|
-
pan:
|
|
2243
|
+
pan: track.pan
|
|
1734
2244
|
});
|
|
2245
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1735
2246
|
}
|
|
1736
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1737
2247
|
}
|
|
1738
|
-
}
|
|
2248
|
+
};
|
|
2249
|
+
for (const track of tracks) void runTrack(track);
|
|
1739
2250
|
};
|
|
1740
2251
|
const stopStream = () => {
|
|
1741
2252
|
streamSession++;
|
|
@@ -3144,8 +3655,10 @@ var parseMML = (mml, options = {}) => {
|
|
|
3144
3655
|
numStr += body[j];
|
|
3145
3656
|
j++;
|
|
3146
3657
|
}
|
|
3147
|
-
if (ch === "t" &&
|
|
3148
|
-
bpm
|
|
3658
|
+
if (ch === "t" && numStr) {
|
|
3659
|
+
if (bpm === null) {
|
|
3660
|
+
bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
|
|
3661
|
+
}
|
|
3149
3662
|
}
|
|
3150
3663
|
pushTok("ctrl", currentStep, 0, tokStart);
|
|
3151
3664
|
} else if (ch === "[") {
|
|
@@ -3275,13 +3788,10 @@ var createSequencer = (options) => {
|
|
|
3275
3788
|
}
|
|
3276
3789
|
while (nowIndex < timeline.length) {
|
|
3277
3790
|
const ev = timeline[nowIndex];
|
|
3278
|
-
if (soloId && ev.trackId !== soloId) {
|
|
3279
|
-
nowIndex++;
|
|
3280
|
-
continue;
|
|
3281
|
-
}
|
|
3282
3791
|
const _when = ev.when - time;
|
|
3283
3792
|
if (_when > PLAN_TIME) break;
|
|
3284
3793
|
nowIndex++;
|
|
3794
|
+
if (soloId && ev.trackId !== soloId) continue;
|
|
3285
3795
|
const velocityVolume = ev.velocity / 127;
|
|
3286
3796
|
const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
|
|
3287
3797
|
options.onPlayNote({
|
|
@@ -3650,6 +4160,7 @@ var DAW_CSS = `
|
|
|
3650
4160
|
}
|
|
3651
4161
|
.dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
|
|
3652
4162
|
.dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
|
|
4163
|
+
.dtm-textarea.dtm-grow { width: 0; }
|
|
3653
4164
|
.dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
|
|
3654
4165
|
|
|
3655
4166
|
/* \u2500\u2500\u2500 \u30C8\u30E9\u30C3\u30AF\u30D4\u30EB\uFF08\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u9078\u629E\u30DC\u30BF\u30F3\uFF09 \u2500\u2500\u2500 */
|
|
@@ -3817,7 +4328,7 @@ var DAW_CSS = `
|
|
|
3817
4328
|
|
|
3818
4329
|
/* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
|
|
3819
4330
|
.dtm-overlay {
|
|
3820
|
-
position: absolute; inset: 0; z-index:
|
|
4331
|
+
position: absolute; inset: 0; z-index: 10;
|
|
3821
4332
|
background: rgba(0,0,0,.92);
|
|
3822
4333
|
display: flex; align-items: center; justify-content: center;
|
|
3823
4334
|
flex-direction: column; gap: 14px;
|
|
@@ -3866,6 +4377,22 @@ var DAW_CSS = `
|
|
|
3866
4377
|
letter-spacing: .15em;
|
|
3867
4378
|
min-height: 1em;
|
|
3868
4379
|
}
|
|
4380
|
+
.dtm-topbar-loading {
|
|
4381
|
+
display: none;
|
|
4382
|
+
font-family: var(--dtm-font);
|
|
4383
|
+
font-size: 11px;
|
|
4384
|
+
color: var(--dtm-primary);
|
|
4385
|
+
margin-left: 12px;
|
|
4386
|
+
letter-spacing: .15em;
|
|
4387
|
+
align-self: center;
|
|
4388
|
+
}
|
|
4389
|
+
.dtm-topbar.is-loading .dtm-topbar-loading {
|
|
4390
|
+
display: inline-block;
|
|
4391
|
+
}
|
|
4392
|
+
.dtm-topbar.is-loading {
|
|
4393
|
+
pointer-events: none;
|
|
4394
|
+
opacity: 0.7;
|
|
4395
|
+
}
|
|
3869
4396
|
|
|
3870
4397
|
@keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
|
3871
4398
|
.dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
|
|
@@ -3941,6 +4468,14 @@ var DAW_CSS = `
|
|
|
3941
4468
|
min-width: 2em;
|
|
3942
4469
|
margin-left: 4px;
|
|
3943
4470
|
}
|
|
4471
|
+
.dtm-player-chord {
|
|
4472
|
+
font-family: 'k8x12', monospace;
|
|
4473
|
+
font-size: 11px;
|
|
4474
|
+
color: var(--dtm-accent);
|
|
4475
|
+
min-width: 4em;
|
|
4476
|
+
margin-left: 8px;
|
|
4477
|
+
font-weight: bold;
|
|
4478
|
+
}
|
|
3944
4479
|
.dtm-player-dots {
|
|
3945
4480
|
margin-left: auto;
|
|
3946
4481
|
display: flex;
|
|
@@ -4291,7 +4826,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
4291
4826
|
const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
|
|
4292
4827
|
const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
|
|
4293
4828
|
const showMidi = !!options.parseMidi;
|
|
4294
|
-
const showChord =
|
|
4829
|
+
const showChord = true;
|
|
4295
4830
|
const refs = buildUI(target, {
|
|
4296
4831
|
tracks: trackConfigs,
|
|
4297
4832
|
drumPatternNames: Object.keys(drumPatterns),
|
|
@@ -4976,6 +5511,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
4976
5511
|
});
|
|
4977
5512
|
}
|
|
4978
5513
|
return {
|
|
5514
|
+
id: trackState?.config.id,
|
|
4979
5515
|
model: lt.model,
|
|
4980
5516
|
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (masterVolume / 100),
|
|
4981
5517
|
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
@@ -4986,13 +5522,15 @@ var mountDAW = (target, options = {}) => {
|
|
|
4986
5522
|
const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
|
|
4987
5523
|
if (streaming && voices) {
|
|
4988
5524
|
const overlay = showLoadingOverlay(refs.rollContainer);
|
|
5525
|
+
setLoading(true);
|
|
4989
5526
|
try {
|
|
4990
5527
|
await voices.loadModels(streamTracks.map((t) => t.model));
|
|
4991
5528
|
await voices.warm(streamTracks);
|
|
4992
|
-
} catch (
|
|
4993
|
-
console.warn("[dtm] voice preload failed",
|
|
5529
|
+
} catch (err2) {
|
|
5530
|
+
console.warn("[dtm] voice preload failed", err2);
|
|
4994
5531
|
} finally {
|
|
4995
5532
|
overlay.remove();
|
|
5533
|
+
setLoading(false);
|
|
4996
5534
|
}
|
|
4997
5535
|
}
|
|
4998
5536
|
if (playbackState !== "paused") {
|
|
@@ -5006,7 +5544,9 @@ var mountDAW = (target, options = {}) => {
|
|
|
5006
5544
|
playbackState = "playing";
|
|
5007
5545
|
sequencer.start(fromStep);
|
|
5008
5546
|
if (streaming && voices) {
|
|
5009
|
-
voices.startStream(streamTracks, sequencer.getStartTime()
|
|
5547
|
+
voices.startStream(streamTracks, sequencer.getStartTime(), {
|
|
5548
|
+
isAudible: (t) => !isSolo || t.id === activeTrackId
|
|
5549
|
+
});
|
|
5010
5550
|
}
|
|
5011
5551
|
updateTransport();
|
|
5012
5552
|
};
|
|
@@ -5339,21 +5879,28 @@ var mountDAW = (target, options = {}) => {
|
|
|
5339
5879
|
barLimit: barLimitBars
|
|
5340
5880
|
};
|
|
5341
5881
|
}
|
|
5342
|
-
const trackLines =
|
|
5343
|
-
|
|
5344
|
-
)
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5882
|
+
const trackLines = [];
|
|
5883
|
+
const trackLinesMini = [];
|
|
5884
|
+
trackStates.forEach((t, i) => {
|
|
5885
|
+
const notes = clipNotes(t.core.getNotes());
|
|
5886
|
+
if (notes.length > 0) {
|
|
5887
|
+
const mml = t.core.getMMLFromNotes(notes, bpm, t.volume).trim();
|
|
5888
|
+
trackLines.push(`@${i} ${mml}`);
|
|
5889
|
+
trackLinesMini.push(`@${i}${mml.replace(/\s+/g, "")}`);
|
|
5890
|
+
}
|
|
5891
|
+
});
|
|
5348
5892
|
const lyricLines = trackStates.map((t, i) => ({
|
|
5349
5893
|
i,
|
|
5350
|
-
|
|
5894
|
+
notes: clipNotes(t.core.getNotes()),
|
|
5895
|
+
text: t.lyrics.replace(/[\r\n]+/g, " ").trim(),
|
|
5351
5896
|
model: t.lyricModel.trim(),
|
|
5352
5897
|
vol: t.vocalVolume,
|
|
5353
5898
|
gate: t.vocalGate,
|
|
5354
5899
|
pan: t.vocalPan,
|
|
5355
5900
|
oct: t.vocalOctave
|
|
5356
|
-
})).filter(
|
|
5901
|
+
})).filter(
|
|
5902
|
+
(x) => x.model.length > 0 && x.text.length > 0 && x.notes.length > 0
|
|
5903
|
+
).map((x) => {
|
|
5357
5904
|
const params = [
|
|
5358
5905
|
x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
|
|
5359
5906
|
x.gate === 100 ? "" : `q${x.gate}`,
|
|
@@ -5369,7 +5916,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5369
5916
|
full,
|
|
5370
5917
|
minified,
|
|
5371
5918
|
ignoredCount: 0,
|
|
5372
|
-
trackCount:
|
|
5919
|
+
trackCount: trackLines.length,
|
|
5373
5920
|
barLimit: barLimitBars
|
|
5374
5921
|
};
|
|
5375
5922
|
};
|
|
@@ -5489,7 +6036,6 @@ var mountDAW = (target, options = {}) => {
|
|
|
5489
6036
|
updateUndoRedo();
|
|
5490
6037
|
};
|
|
5491
6038
|
const applyChord = () => {
|
|
5492
|
-
if (!options.parseChord || !options.parseChords) return;
|
|
5493
6039
|
const active = getActive();
|
|
5494
6040
|
const chordTrack = trackStates.find((t) => t.config.id === "chord");
|
|
5495
6041
|
if (!chordTrack) return;
|
|
@@ -5498,9 +6044,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5498
6044
|
patternType: active.savedChordPattern,
|
|
5499
6045
|
rootShift: active.savedChordRoot,
|
|
5500
6046
|
bpm,
|
|
5501
|
-
stepsPerBar: renderConfig.stepsPerBar
|
|
5502
|
-
parseChord: options.parseChord,
|
|
5503
|
-
parseChords: options.parseChords
|
|
6047
|
+
stepsPerBar: renderConfig.stepsPerBar
|
|
5504
6048
|
});
|
|
5505
6049
|
chordTrack.core.clearNotesWithoutHistory();
|
|
5506
6050
|
chordTrack.core.beginBatch();
|
|
@@ -5585,9 +6129,11 @@ var mountDAW = (target, options = {}) => {
|
|
|
5585
6129
|
};
|
|
5586
6130
|
const overlayDuring = (fn) => {
|
|
5587
6131
|
refs.overlay.hidden = false;
|
|
6132
|
+
setLoading(true);
|
|
5588
6133
|
setTimeout(() => {
|
|
5589
6134
|
fn();
|
|
5590
6135
|
refs.overlay.hidden = true;
|
|
6136
|
+
setLoading(false);
|
|
5591
6137
|
}, 30);
|
|
5592
6138
|
};
|
|
5593
6139
|
const wireEvents = () => {
|
|
@@ -5731,6 +6277,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5731
6277
|
const file = refs.midiInput.files?.[0];
|
|
5732
6278
|
if (!file || !options.parseMidi) return;
|
|
5733
6279
|
refs.overlay.hidden = false;
|
|
6280
|
+
setLoading(true);
|
|
5734
6281
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
5735
6282
|
pendingMidi = options.parseMidi(buffer);
|
|
5736
6283
|
detectedTracks = analyzeMidiTracks(pendingMidi);
|
|
@@ -5751,6 +6298,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5751
6298
|
});
|
|
5752
6299
|
refs.midiTrackSelection.classList.remove("dtm-hidden");
|
|
5753
6300
|
refs.overlay.hidden = true;
|
|
6301
|
+
setLoading(false);
|
|
5754
6302
|
});
|
|
5755
6303
|
refs.midiLoadBtn.addEventListener("click", () => {
|
|
5756
6304
|
if (!pendingMidi) return;
|
|
@@ -5824,6 +6372,9 @@ var mountDAW = (target, options = {}) => {
|
|
|
5824
6372
|
resizeObserver.observe(refs.rollContainer);
|
|
5825
6373
|
document.addEventListener("pointermove", onPointerMove);
|
|
5826
6374
|
document.addEventListener("pointerup", onPointerUp);
|
|
6375
|
+
const setLoading = (loading) => {
|
|
6376
|
+
refs.topbar.classList.toggle("is-loading", loading);
|
|
6377
|
+
};
|
|
5827
6378
|
return {
|
|
5828
6379
|
play,
|
|
5829
6380
|
pause,
|
|
@@ -5861,6 +6412,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5861
6412
|
exportMIDI: exportMIDI2,
|
|
5862
6413
|
setBpm,
|
|
5863
6414
|
getPlaybackState: () => playbackState,
|
|
6415
|
+
setLoading,
|
|
5864
6416
|
destroy: () => {
|
|
5865
6417
|
sequencer.stop();
|
|
5866
6418
|
resizeObserver.disconnect();
|
|
@@ -6005,6 +6557,117 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6005
6557
|
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
6006
6558
|
(a, b) => a - b
|
|
6007
6559
|
);
|
|
6560
|
+
const trackStats = trackIndices.map((index) => {
|
|
6561
|
+
const trackPlacements = placements.filter((p) => p.trackIndex === index);
|
|
6562
|
+
if (trackPlacements.length === 0) {
|
|
6563
|
+
return { index, isChords: false, avgPitch: 0, noteCount: 0 };
|
|
6564
|
+
}
|
|
6565
|
+
const sumPitch = trackPlacements.reduce((sum, p) => sum + p.pitch, 0);
|
|
6566
|
+
const avgPitch = sumPitch / trackPlacements.length;
|
|
6567
|
+
const stepCounts = /* @__PURE__ */ new Map();
|
|
6568
|
+
for (const p of trackPlacements) {
|
|
6569
|
+
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6570
|
+
stepCounts.set(s, (stepCounts.get(s) ?? 0) + 1);
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
const polyphonicSteps = Array.from(stepCounts.values()).filter(
|
|
6574
|
+
(c) => c >= 2
|
|
6575
|
+
).length;
|
|
6576
|
+
const isChords = polyphonicSteps > 0;
|
|
6577
|
+
return {
|
|
6578
|
+
index,
|
|
6579
|
+
isChords,
|
|
6580
|
+
avgPitch,
|
|
6581
|
+
noteCount: trackPlacements.length
|
|
6582
|
+
};
|
|
6583
|
+
});
|
|
6584
|
+
const chordTrackIndices = trackStats.filter((s) => s.isChords).map((s) => s.index);
|
|
6585
|
+
const nonChordTracks = trackStats.filter(
|
|
6586
|
+
(s) => !s.isChords && s.noteCount > 0
|
|
6587
|
+
);
|
|
6588
|
+
let bassTrackIndex = null;
|
|
6589
|
+
if (nonChordTracks.length > 0) {
|
|
6590
|
+
nonChordTracks.sort((a, b) => a.avgPitch - b.avgPitch);
|
|
6591
|
+
bassTrackIndex = nonChordTracks[0].index;
|
|
6592
|
+
}
|
|
6593
|
+
const priorityTrackIndices = /* @__PURE__ */ new Set();
|
|
6594
|
+
for (const idx of chordTrackIndices) {
|
|
6595
|
+
priorityTrackIndices.add(idx);
|
|
6596
|
+
}
|
|
6597
|
+
if (bassTrackIndex !== null) {
|
|
6598
|
+
priorityTrackIndices.add(bassTrackIndex);
|
|
6599
|
+
}
|
|
6600
|
+
const hasPriorityTracksInMml = priorityTrackIndices.size > 0;
|
|
6601
|
+
const maxStep = placements.reduce(
|
|
6602
|
+
(max, p) => Math.max(max, p.startStep + p.durationSteps),
|
|
6603
|
+
0
|
|
6604
|
+
);
|
|
6605
|
+
const priorityPitches = Array.from(
|
|
6606
|
+
{ length: maxStep + 1 },
|
|
6607
|
+
() => /* @__PURE__ */ new Set()
|
|
6608
|
+
);
|
|
6609
|
+
const allPitches = Array.from(
|
|
6610
|
+
{ length: maxStep + 1 },
|
|
6611
|
+
() => /* @__PURE__ */ new Set()
|
|
6612
|
+
);
|
|
6613
|
+
for (const p of placements) {
|
|
6614
|
+
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6615
|
+
if (s >= 0 && s <= maxStep) {
|
|
6616
|
+
allPitches[s].add(p.pitch);
|
|
6617
|
+
if (priorityTrackIndices.has(p.trackIndex)) {
|
|
6618
|
+
priorityPitches[s].add(p.pitch);
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6623
|
+
const stepChords = [];
|
|
6624
|
+
const chordCache = /* @__PURE__ */ new Map();
|
|
6625
|
+
let lastChord = "";
|
|
6626
|
+
let hasPriorityStarted = false;
|
|
6627
|
+
const GRID_SIZE = 48;
|
|
6628
|
+
const MIN_DURATION = 12;
|
|
6629
|
+
for (let g = 0; g <= Math.ceil(maxStep / GRID_SIZE); g++) {
|
|
6630
|
+
const startS = g * GRID_SIZE;
|
|
6631
|
+
const endS = Math.min(maxStep, (g + 1) * GRID_SIZE - 1);
|
|
6632
|
+
if (startS > maxStep) break;
|
|
6633
|
+
for (let s = startS; s <= endS; s++) {
|
|
6634
|
+
const priorityPitchesAtStep = Array.from(priorityPitches[s]);
|
|
6635
|
+
if (priorityPitchesAtStep.length > 0) {
|
|
6636
|
+
hasPriorityStarted = true;
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
const pitchDurations = /* @__PURE__ */ new Map();
|
|
6640
|
+
for (let s = startS; s <= endS; s++) {
|
|
6641
|
+
const usePriority = hasPriorityStarted && hasPriorityTracksInMml;
|
|
6642
|
+
const pitches = usePriority ? Array.from(priorityPitches[s]) : Array.from(allPitches[s]);
|
|
6643
|
+
for (const p of pitches) {
|
|
6644
|
+
pitchDurations.set(p, (pitchDurations.get(p) ?? 0) + 1);
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
let activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur >= MIN_DURATION).map(([p, _]) => p);
|
|
6648
|
+
if (activePitches.length === 0 && pitchDurations.size > 0) {
|
|
6649
|
+
const maxDur = Math.max(...pitchDurations.values());
|
|
6650
|
+
activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur === maxDur).map(([p, _]) => p);
|
|
6651
|
+
}
|
|
6652
|
+
let gridChord = lastChord;
|
|
6653
|
+
if (activePitches.length > 0) {
|
|
6654
|
+
const sortedPitches = activePitches.sort((a, b) => a - b);
|
|
6655
|
+
const cacheKey = sortedPitches.join(",");
|
|
6656
|
+
if (chordCache.has(cacheKey)) {
|
|
6657
|
+
const chordName = chordCache.get(cacheKey);
|
|
6658
|
+
if (chordName) gridChord = chordName;
|
|
6659
|
+
} else {
|
|
6660
|
+
const candidates = detectChord(sortedPitches);
|
|
6661
|
+
const chordName = candidates[0]?.symbol ?? "";
|
|
6662
|
+
if (chordName) gridChord = chordName;
|
|
6663
|
+
chordCache.set(cacheKey, chordName);
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
for (let s = startS; s <= endS; s++) {
|
|
6667
|
+
stepChords[s] = gridChord;
|
|
6668
|
+
}
|
|
6669
|
+
lastChord = gridChord;
|
|
6670
|
+
}
|
|
6008
6671
|
const seqTracks = trackIndices.map((index) => {
|
|
6009
6672
|
let id = 0;
|
|
6010
6673
|
const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
|
|
@@ -6205,6 +6868,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6205
6868
|
barEl.className = "dtm-player-bar";
|
|
6206
6869
|
barEl.textContent = "-";
|
|
6207
6870
|
beatRow.appendChild(barEl);
|
|
6871
|
+
const chordEl = doc.createElement("span");
|
|
6872
|
+
chordEl.className = "dtm-player-chord";
|
|
6873
|
+
chordEl.textContent = "";
|
|
6874
|
+
beatRow.appendChild(chordEl);
|
|
6208
6875
|
const chips = [];
|
|
6209
6876
|
const makeChip = (label) => {
|
|
6210
6877
|
const chip = doc.createElement("span");
|
|
@@ -6348,10 +7015,20 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6348
7015
|
lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
|
|
6349
7016
|
};
|
|
6350
7017
|
const renderPlayhead = (step) => {
|
|
7018
|
+
const intStep = Math.floor(step);
|
|
6351
7019
|
const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
|
|
6352
7020
|
for (let i = 0; i < 4; i++)
|
|
6353
7021
|
beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
|
|
6354
7022
|
barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
|
|
7023
|
+
const chordName = stepChords[intStep] ?? "";
|
|
7024
|
+
if (chordEl.textContent !== chordName) {
|
|
7025
|
+
chordEl.textContent = chordName;
|
|
7026
|
+
if (chordName) {
|
|
7027
|
+
console.log(
|
|
7028
|
+
`[dtm-player-chord] Active Chord: ${chordName} (step: ${intStep})`
|
|
7029
|
+
);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
6355
7032
|
for (const view of laneViews) {
|
|
6356
7033
|
let active = null;
|
|
6357
7034
|
for (const t of view.tokens) {
|
|
@@ -6365,6 +7042,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6365
7042
|
const resetPlayhead = () => {
|
|
6366
7043
|
for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
|
|
6367
7044
|
barEl.textContent = "-";
|
|
7045
|
+
chordEl.textContent = "";
|
|
6368
7046
|
for (const view of laneViews) {
|
|
6369
7047
|
for (const t of view.tokens) t.el.classList.remove("is-active");
|
|
6370
7048
|
view.lane.scrollLeft = 0;
|
|
@@ -6428,6 +7106,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6428
7106
|
});
|
|
6429
7107
|
}
|
|
6430
7108
|
return {
|
|
7109
|
+
id: String(index),
|
|
6431
7110
|
model: lt.model,
|
|
6432
7111
|
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
|
|
6433
7112
|
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
@@ -6443,15 +7122,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6443
7122
|
try {
|
|
6444
7123
|
await v.loadModels(tracks.map((t) => t.model));
|
|
6445
7124
|
await v.warm(tracks);
|
|
6446
|
-
} catch (
|
|
6447
|
-
console.warn("[dtm] voice preload failed",
|
|
7125
|
+
} catch (err2) {
|
|
7126
|
+
console.warn("[dtm] voice preload failed", err2);
|
|
6448
7127
|
} finally {
|
|
6449
7128
|
overlay.remove();
|
|
6450
7129
|
}
|
|
6451
7130
|
if (!playing || activePlayer !== instance) return;
|
|
6452
7131
|
}
|
|
6453
7132
|
seq.start(0);
|
|
6454
|
-
if (streaming)
|
|
7133
|
+
if (streaming) {
|
|
7134
|
+
ensureVoices().startStream(tracks, seq.getStartTime(), {
|
|
7135
|
+
isAudible: (t) => !mutedTracks.has(Number(t.id))
|
|
7136
|
+
});
|
|
7137
|
+
}
|
|
6455
7138
|
};
|
|
6456
7139
|
const play = () => {
|
|
6457
7140
|
if (playing || trackIndices.length === 0) return;
|
|
@@ -6785,8 +7468,6 @@ var DEFAULT_CDN = {
|
|
|
6785
7468
|
soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
|
|
6786
7469
|
soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
|
|
6787
7470
|
soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs",
|
|
6788
|
-
parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs",
|
|
6789
|
-
parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs",
|
|
6790
7471
|
midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
|
|
6791
7472
|
};
|
|
6792
7473
|
var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
|
|
@@ -6830,24 +7511,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
6830
7511
|
eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
|
|
6831
7512
|
eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
|
|
6832
7513
|
]);
|
|
6833
|
-
let parseChord = eng.parseChord;
|
|
6834
|
-
let parseChords = eng.parseChords;
|
|
6835
|
-
if (features.chord && (!parseChord || !parseChords)) {
|
|
6836
|
-
try {
|
|
6837
|
-
[parseChord, parseChords] = await Promise.all([
|
|
6838
|
-
parseChord ?? importFrom(
|
|
6839
|
-
cdn.parseChord,
|
|
6840
|
-
"parseChord"
|
|
6841
|
-
),
|
|
6842
|
-
parseChords ?? importFrom(
|
|
6843
|
-
cdn.parseChords,
|
|
6844
|
-
"parseChords"
|
|
6845
|
-
)
|
|
6846
|
-
]);
|
|
6847
|
-
} catch (e) {
|
|
6848
|
-
console.warn("[dtm] \u30B3\u30FC\u30C9\u89E3\u6790\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F", e);
|
|
6849
|
-
}
|
|
6850
|
-
}
|
|
6851
7514
|
let midiParser = null;
|
|
6852
7515
|
let parseMidi;
|
|
6853
7516
|
if (features.midi) {
|
|
@@ -7033,8 +7696,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
7033
7696
|
onPlayNote: playNote,
|
|
7034
7697
|
onPlayDrum: playDrum,
|
|
7035
7698
|
singingVoices,
|
|
7036
|
-
parseChord,
|
|
7037
|
-
parseChords,
|
|
7038
7699
|
parseMidi,
|
|
7039
7700
|
onToggleRecord,
|
|
7040
7701
|
...dawOverrides
|
|
@@ -7056,15 +7717,32 @@ var createDtmStudio = async (options = {}) => {
|
|
|
7056
7717
|
editorPresetSelects.set(target, select);
|
|
7057
7718
|
select.addEventListener("change", async () => {
|
|
7058
7719
|
if (!select) return;
|
|
7059
|
-
daw.
|
|
7060
|
-
|
|
7720
|
+
const wasPlaying = daw.getPlaybackState() === "playing";
|
|
7721
|
+
if (wasPlaying) {
|
|
7722
|
+
daw.pause();
|
|
7723
|
+
}
|
|
7724
|
+
const overlay = showLoadingOverlay(target);
|
|
7725
|
+
daw.setLoading?.(true);
|
|
7726
|
+
try {
|
|
7727
|
+
daw.setInstrument(select.value);
|
|
7728
|
+
await loadPreset(select.value, trackIds);
|
|
7729
|
+
} finally {
|
|
7730
|
+
overlay.remove();
|
|
7731
|
+
daw.setLoading?.(false);
|
|
7732
|
+
if (wasPlaying) {
|
|
7733
|
+
daw.play();
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7061
7736
|
});
|
|
7062
7737
|
}
|
|
7063
7738
|
const daw = mountDAW(target, base);
|
|
7064
7739
|
mountedEditors.push(daw);
|
|
7065
7740
|
const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
|
|
7066
7741
|
daw.setInstrument(presetKey);
|
|
7067
|
-
|
|
7742
|
+
daw.setLoading?.(true);
|
|
7743
|
+
void loadPreset(presetKey, trackIds).finally(() => {
|
|
7744
|
+
daw.setLoading?.(false);
|
|
7745
|
+
});
|
|
7068
7746
|
const destroy = () => {
|
|
7069
7747
|
daw.destroy();
|
|
7070
7748
|
select?.remove();
|