@onjmin/dtm 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -3
- package/dist/index.d.mts +30 -47
- package/dist/index.d.ts +30 -47
- package/dist/index.js +748 -62
- package/dist/index.mjs +748 -62
- 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);
|
|
@@ -3136,8 +3655,10 @@ var parseMML = (mml, options = {}) => {
|
|
|
3136
3655
|
numStr += body[j];
|
|
3137
3656
|
j++;
|
|
3138
3657
|
}
|
|
3139
|
-
if (ch === "t" &&
|
|
3140
|
-
bpm
|
|
3658
|
+
if (ch === "t" && numStr) {
|
|
3659
|
+
if (bpm === null) {
|
|
3660
|
+
bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
|
|
3661
|
+
}
|
|
3141
3662
|
}
|
|
3142
3663
|
pushTok("ctrl", currentStep, 0, tokStart);
|
|
3143
3664
|
} else if (ch === "[") {
|
|
@@ -3639,6 +4160,7 @@ var DAW_CSS = `
|
|
|
3639
4160
|
}
|
|
3640
4161
|
.dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
|
|
3641
4162
|
.dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
|
|
4163
|
+
.dtm-textarea.dtm-grow { width: 0; }
|
|
3642
4164
|
.dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
|
|
3643
4165
|
|
|
3644
4166
|
/* \u2500\u2500\u2500 \u30C8\u30E9\u30C3\u30AF\u30D4\u30EB\uFF08\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u9078\u629E\u30DC\u30BF\u30F3\uFF09 \u2500\u2500\u2500 */
|
|
@@ -3806,7 +4328,7 @@ var DAW_CSS = `
|
|
|
3806
4328
|
|
|
3807
4329
|
/* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
|
|
3808
4330
|
.dtm-overlay {
|
|
3809
|
-
position: absolute; inset: 0; z-index:
|
|
4331
|
+
position: absolute; inset: 0; z-index: 10;
|
|
3810
4332
|
background: rgba(0,0,0,.92);
|
|
3811
4333
|
display: flex; align-items: center; justify-content: center;
|
|
3812
4334
|
flex-direction: column; gap: 14px;
|
|
@@ -3855,6 +4377,22 @@ var DAW_CSS = `
|
|
|
3855
4377
|
letter-spacing: .15em;
|
|
3856
4378
|
min-height: 1em;
|
|
3857
4379
|
}
|
|
4380
|
+
.dtm-topbar-loading {
|
|
4381
|
+
display: none;
|
|
4382
|
+
font-family: var(--dtm-font);
|
|
4383
|
+
font-size: 11px;
|
|
4384
|
+
color: var(--dtm-primary);
|
|
4385
|
+
margin-left: 12px;
|
|
4386
|
+
letter-spacing: .15em;
|
|
4387
|
+
align-self: center;
|
|
4388
|
+
}
|
|
4389
|
+
.dtm-topbar.is-loading .dtm-topbar-loading {
|
|
4390
|
+
display: inline-block;
|
|
4391
|
+
}
|
|
4392
|
+
.dtm-topbar.is-loading {
|
|
4393
|
+
pointer-events: none;
|
|
4394
|
+
opacity: 0.7;
|
|
4395
|
+
}
|
|
3858
4396
|
|
|
3859
4397
|
@keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
|
3860
4398
|
.dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
|
|
@@ -3930,6 +4468,14 @@ var DAW_CSS = `
|
|
|
3930
4468
|
min-width: 2em;
|
|
3931
4469
|
margin-left: 4px;
|
|
3932
4470
|
}
|
|
4471
|
+
.dtm-player-chord {
|
|
4472
|
+
font-family: 'k8x12', monospace;
|
|
4473
|
+
font-size: 11px;
|
|
4474
|
+
color: var(--dtm-accent);
|
|
4475
|
+
min-width: 4em;
|
|
4476
|
+
margin-left: 8px;
|
|
4477
|
+
font-weight: bold;
|
|
4478
|
+
}
|
|
3933
4479
|
.dtm-player-dots {
|
|
3934
4480
|
margin-left: auto;
|
|
3935
4481
|
display: flex;
|
|
@@ -4280,7 +4826,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
4280
4826
|
const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
|
|
4281
4827
|
const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
|
|
4282
4828
|
const showMidi = !!options.parseMidi;
|
|
4283
|
-
const showChord =
|
|
4829
|
+
const showChord = true;
|
|
4284
4830
|
const refs = buildUI(target, {
|
|
4285
4831
|
tracks: trackConfigs,
|
|
4286
4832
|
drumPatternNames: Object.keys(drumPatterns),
|
|
@@ -4976,13 +5522,15 @@ var mountDAW = (target, options = {}) => {
|
|
|
4976
5522
|
const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
|
|
4977
5523
|
if (streaming && voices) {
|
|
4978
5524
|
const overlay = showLoadingOverlay(refs.rollContainer);
|
|
5525
|
+
setLoading(true);
|
|
4979
5526
|
try {
|
|
4980
5527
|
await voices.loadModels(streamTracks.map((t) => t.model));
|
|
4981
5528
|
await voices.warm(streamTracks);
|
|
4982
|
-
} catch (
|
|
4983
|
-
console.warn("[dtm] voice preload failed",
|
|
5529
|
+
} catch (err2) {
|
|
5530
|
+
console.warn("[dtm] voice preload failed", err2);
|
|
4984
5531
|
} finally {
|
|
4985
5532
|
overlay.remove();
|
|
5533
|
+
setLoading(false);
|
|
4986
5534
|
}
|
|
4987
5535
|
}
|
|
4988
5536
|
if (playbackState !== "paused") {
|
|
@@ -5331,21 +5879,28 @@ var mountDAW = (target, options = {}) => {
|
|
|
5331
5879
|
barLimit: barLimitBars
|
|
5332
5880
|
};
|
|
5333
5881
|
}
|
|
5334
|
-
const trackLines =
|
|
5335
|
-
|
|
5336
|
-
)
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5882
|
+
const trackLines = [];
|
|
5883
|
+
const trackLinesMini = [];
|
|
5884
|
+
trackStates.forEach((t, i) => {
|
|
5885
|
+
const notes = clipNotes(t.core.getNotes());
|
|
5886
|
+
if (notes.length > 0) {
|
|
5887
|
+
const mml = t.core.getMMLFromNotes(notes, bpm, t.volume).trim();
|
|
5888
|
+
trackLines.push(`@${i} ${mml}`);
|
|
5889
|
+
trackLinesMini.push(`@${i}${mml.replace(/\s+/g, "")}`);
|
|
5890
|
+
}
|
|
5891
|
+
});
|
|
5340
5892
|
const lyricLines = trackStates.map((t, i) => ({
|
|
5341
5893
|
i,
|
|
5342
|
-
|
|
5894
|
+
notes: clipNotes(t.core.getNotes()),
|
|
5895
|
+
text: t.lyrics.replace(/[\r\n]+/g, " ").trim(),
|
|
5343
5896
|
model: t.lyricModel.trim(),
|
|
5344
5897
|
vol: t.vocalVolume,
|
|
5345
5898
|
gate: t.vocalGate,
|
|
5346
5899
|
pan: t.vocalPan,
|
|
5347
5900
|
oct: t.vocalOctave
|
|
5348
|
-
})).filter(
|
|
5901
|
+
})).filter(
|
|
5902
|
+
(x) => x.model.length > 0 && x.text.length > 0 && x.notes.length > 0
|
|
5903
|
+
).map((x) => {
|
|
5349
5904
|
const params = [
|
|
5350
5905
|
x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
|
|
5351
5906
|
x.gate === 100 ? "" : `q${x.gate}`,
|
|
@@ -5361,7 +5916,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5361
5916
|
full,
|
|
5362
5917
|
minified,
|
|
5363
5918
|
ignoredCount: 0,
|
|
5364
|
-
trackCount:
|
|
5919
|
+
trackCount: trackLines.length,
|
|
5365
5920
|
barLimit: barLimitBars
|
|
5366
5921
|
};
|
|
5367
5922
|
};
|
|
@@ -5481,7 +6036,6 @@ var mountDAW = (target, options = {}) => {
|
|
|
5481
6036
|
updateUndoRedo();
|
|
5482
6037
|
};
|
|
5483
6038
|
const applyChord = () => {
|
|
5484
|
-
if (!options.parseChord || !options.parseChords) return;
|
|
5485
6039
|
const active = getActive();
|
|
5486
6040
|
const chordTrack = trackStates.find((t) => t.config.id === "chord");
|
|
5487
6041
|
if (!chordTrack) return;
|
|
@@ -5490,9 +6044,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5490
6044
|
patternType: active.savedChordPattern,
|
|
5491
6045
|
rootShift: active.savedChordRoot,
|
|
5492
6046
|
bpm,
|
|
5493
|
-
stepsPerBar: renderConfig.stepsPerBar
|
|
5494
|
-
parseChord: options.parseChord,
|
|
5495
|
-
parseChords: options.parseChords
|
|
6047
|
+
stepsPerBar: renderConfig.stepsPerBar
|
|
5496
6048
|
});
|
|
5497
6049
|
chordTrack.core.clearNotesWithoutHistory();
|
|
5498
6050
|
chordTrack.core.beginBatch();
|
|
@@ -5577,9 +6129,11 @@ var mountDAW = (target, options = {}) => {
|
|
|
5577
6129
|
};
|
|
5578
6130
|
const overlayDuring = (fn) => {
|
|
5579
6131
|
refs.overlay.hidden = false;
|
|
6132
|
+
setLoading(true);
|
|
5580
6133
|
setTimeout(() => {
|
|
5581
6134
|
fn();
|
|
5582
6135
|
refs.overlay.hidden = true;
|
|
6136
|
+
setLoading(false);
|
|
5583
6137
|
}, 30);
|
|
5584
6138
|
};
|
|
5585
6139
|
const wireEvents = () => {
|
|
@@ -5723,6 +6277,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5723
6277
|
const file = refs.midiInput.files?.[0];
|
|
5724
6278
|
if (!file || !options.parseMidi) return;
|
|
5725
6279
|
refs.overlay.hidden = false;
|
|
6280
|
+
setLoading(true);
|
|
5726
6281
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
5727
6282
|
pendingMidi = options.parseMidi(buffer);
|
|
5728
6283
|
detectedTracks = analyzeMidiTracks(pendingMidi);
|
|
@@ -5743,6 +6298,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5743
6298
|
});
|
|
5744
6299
|
refs.midiTrackSelection.classList.remove("dtm-hidden");
|
|
5745
6300
|
refs.overlay.hidden = true;
|
|
6301
|
+
setLoading(false);
|
|
5746
6302
|
});
|
|
5747
6303
|
refs.midiLoadBtn.addEventListener("click", () => {
|
|
5748
6304
|
if (!pendingMidi) return;
|
|
@@ -5816,6 +6372,9 @@ var mountDAW = (target, options = {}) => {
|
|
|
5816
6372
|
resizeObserver.observe(refs.rollContainer);
|
|
5817
6373
|
document.addEventListener("pointermove", onPointerMove);
|
|
5818
6374
|
document.addEventListener("pointerup", onPointerUp);
|
|
6375
|
+
const setLoading = (loading) => {
|
|
6376
|
+
refs.topbar.classList.toggle("is-loading", loading);
|
|
6377
|
+
};
|
|
5819
6378
|
return {
|
|
5820
6379
|
play,
|
|
5821
6380
|
pause,
|
|
@@ -5853,6 +6412,7 @@ var mountDAW = (target, options = {}) => {
|
|
|
5853
6412
|
exportMIDI: exportMIDI2,
|
|
5854
6413
|
setBpm,
|
|
5855
6414
|
getPlaybackState: () => playbackState,
|
|
6415
|
+
setLoading,
|
|
5856
6416
|
destroy: () => {
|
|
5857
6417
|
sequencer.stop();
|
|
5858
6418
|
resizeObserver.disconnect();
|
|
@@ -5997,6 +6557,117 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
5997
6557
|
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
5998
6558
|
(a, b) => a - b
|
|
5999
6559
|
);
|
|
6560
|
+
const trackStats = trackIndices.map((index) => {
|
|
6561
|
+
const trackPlacements = placements.filter((p) => p.trackIndex === index);
|
|
6562
|
+
if (trackPlacements.length === 0) {
|
|
6563
|
+
return { index, isChords: false, avgPitch: 0, noteCount: 0 };
|
|
6564
|
+
}
|
|
6565
|
+
const sumPitch = trackPlacements.reduce((sum, p) => sum + p.pitch, 0);
|
|
6566
|
+
const avgPitch = sumPitch / trackPlacements.length;
|
|
6567
|
+
const stepCounts = /* @__PURE__ */ new Map();
|
|
6568
|
+
for (const p of trackPlacements) {
|
|
6569
|
+
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6570
|
+
stepCounts.set(s, (stepCounts.get(s) ?? 0) + 1);
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
const polyphonicSteps = Array.from(stepCounts.values()).filter(
|
|
6574
|
+
(c) => c >= 2
|
|
6575
|
+
).length;
|
|
6576
|
+
const isChords = polyphonicSteps > 0;
|
|
6577
|
+
return {
|
|
6578
|
+
index,
|
|
6579
|
+
isChords,
|
|
6580
|
+
avgPitch,
|
|
6581
|
+
noteCount: trackPlacements.length
|
|
6582
|
+
};
|
|
6583
|
+
});
|
|
6584
|
+
const chordTrackIndices = trackStats.filter((s) => s.isChords).map((s) => s.index);
|
|
6585
|
+
const nonChordTracks = trackStats.filter(
|
|
6586
|
+
(s) => !s.isChords && s.noteCount > 0
|
|
6587
|
+
);
|
|
6588
|
+
let bassTrackIndex = null;
|
|
6589
|
+
if (nonChordTracks.length > 0) {
|
|
6590
|
+
nonChordTracks.sort((a, b) => a.avgPitch - b.avgPitch);
|
|
6591
|
+
bassTrackIndex = nonChordTracks[0].index;
|
|
6592
|
+
}
|
|
6593
|
+
const priorityTrackIndices = /* @__PURE__ */ new Set();
|
|
6594
|
+
for (const idx of chordTrackIndices) {
|
|
6595
|
+
priorityTrackIndices.add(idx);
|
|
6596
|
+
}
|
|
6597
|
+
if (bassTrackIndex !== null) {
|
|
6598
|
+
priorityTrackIndices.add(bassTrackIndex);
|
|
6599
|
+
}
|
|
6600
|
+
const hasPriorityTracksInMml = priorityTrackIndices.size > 0;
|
|
6601
|
+
const maxStep = placements.reduce(
|
|
6602
|
+
(max, p) => Math.max(max, p.startStep + p.durationSteps),
|
|
6603
|
+
0
|
|
6604
|
+
);
|
|
6605
|
+
const priorityPitches = Array.from(
|
|
6606
|
+
{ length: maxStep + 1 },
|
|
6607
|
+
() => /* @__PURE__ */ new Set()
|
|
6608
|
+
);
|
|
6609
|
+
const allPitches = Array.from(
|
|
6610
|
+
{ length: maxStep + 1 },
|
|
6611
|
+
() => /* @__PURE__ */ new Set()
|
|
6612
|
+
);
|
|
6613
|
+
for (const p of placements) {
|
|
6614
|
+
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6615
|
+
if (s >= 0 && s <= maxStep) {
|
|
6616
|
+
allPitches[s].add(p.pitch);
|
|
6617
|
+
if (priorityTrackIndices.has(p.trackIndex)) {
|
|
6618
|
+
priorityPitches[s].add(p.pitch);
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6623
|
+
const stepChords = [];
|
|
6624
|
+
const chordCache = /* @__PURE__ */ new Map();
|
|
6625
|
+
let lastChord = "";
|
|
6626
|
+
let hasPriorityStarted = false;
|
|
6627
|
+
const GRID_SIZE = 48;
|
|
6628
|
+
const MIN_DURATION = 12;
|
|
6629
|
+
for (let g = 0; g <= Math.ceil(maxStep / GRID_SIZE); g++) {
|
|
6630
|
+
const startS = g * GRID_SIZE;
|
|
6631
|
+
const endS = Math.min(maxStep, (g + 1) * GRID_SIZE - 1);
|
|
6632
|
+
if (startS > maxStep) break;
|
|
6633
|
+
for (let s = startS; s <= endS; s++) {
|
|
6634
|
+
const priorityPitchesAtStep = Array.from(priorityPitches[s]);
|
|
6635
|
+
if (priorityPitchesAtStep.length > 0) {
|
|
6636
|
+
hasPriorityStarted = true;
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
const pitchDurations = /* @__PURE__ */ new Map();
|
|
6640
|
+
for (let s = startS; s <= endS; s++) {
|
|
6641
|
+
const usePriority = hasPriorityStarted && hasPriorityTracksInMml;
|
|
6642
|
+
const pitches = usePriority ? Array.from(priorityPitches[s]) : Array.from(allPitches[s]);
|
|
6643
|
+
for (const p of pitches) {
|
|
6644
|
+
pitchDurations.set(p, (pitchDurations.get(p) ?? 0) + 1);
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
let activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur >= MIN_DURATION).map(([p, _]) => p);
|
|
6648
|
+
if (activePitches.length === 0 && pitchDurations.size > 0) {
|
|
6649
|
+
const maxDur = Math.max(...pitchDurations.values());
|
|
6650
|
+
activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur === maxDur).map(([p, _]) => p);
|
|
6651
|
+
}
|
|
6652
|
+
let gridChord = lastChord;
|
|
6653
|
+
if (activePitches.length > 0) {
|
|
6654
|
+
const sortedPitches = activePitches.sort((a, b) => a - b);
|
|
6655
|
+
const cacheKey = sortedPitches.join(",");
|
|
6656
|
+
if (chordCache.has(cacheKey)) {
|
|
6657
|
+
const chordName = chordCache.get(cacheKey);
|
|
6658
|
+
if (chordName) gridChord = chordName;
|
|
6659
|
+
} else {
|
|
6660
|
+
const candidates = detectChord(sortedPitches);
|
|
6661
|
+
const chordName = candidates[0]?.symbol ?? "";
|
|
6662
|
+
if (chordName) gridChord = chordName;
|
|
6663
|
+
chordCache.set(cacheKey, chordName);
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
for (let s = startS; s <= endS; s++) {
|
|
6667
|
+
stepChords[s] = gridChord;
|
|
6668
|
+
}
|
|
6669
|
+
lastChord = gridChord;
|
|
6670
|
+
}
|
|
6000
6671
|
const seqTracks = trackIndices.map((index) => {
|
|
6001
6672
|
let id = 0;
|
|
6002
6673
|
const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
|
|
@@ -6197,6 +6868,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6197
6868
|
barEl.className = "dtm-player-bar";
|
|
6198
6869
|
barEl.textContent = "-";
|
|
6199
6870
|
beatRow.appendChild(barEl);
|
|
6871
|
+
const chordEl = doc.createElement("span");
|
|
6872
|
+
chordEl.className = "dtm-player-chord";
|
|
6873
|
+
chordEl.textContent = "";
|
|
6874
|
+
beatRow.appendChild(chordEl);
|
|
6200
6875
|
const chips = [];
|
|
6201
6876
|
const makeChip = (label) => {
|
|
6202
6877
|
const chip = doc.createElement("span");
|
|
@@ -6340,10 +7015,20 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6340
7015
|
lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
|
|
6341
7016
|
};
|
|
6342
7017
|
const renderPlayhead = (step) => {
|
|
7018
|
+
const intStep = Math.floor(step);
|
|
6343
7019
|
const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
|
|
6344
7020
|
for (let i = 0; i < 4; i++)
|
|
6345
7021
|
beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
|
|
6346
7022
|
barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
|
|
7023
|
+
const chordName = stepChords[intStep] ?? "";
|
|
7024
|
+
if (chordEl.textContent !== chordName) {
|
|
7025
|
+
chordEl.textContent = chordName;
|
|
7026
|
+
if (chordName) {
|
|
7027
|
+
console.log(
|
|
7028
|
+
`[dtm-player-chord] Active Chord: ${chordName} (step: ${intStep})`
|
|
7029
|
+
);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
6347
7032
|
for (const view of laneViews) {
|
|
6348
7033
|
let active = null;
|
|
6349
7034
|
for (const t of view.tokens) {
|
|
@@ -6357,6 +7042,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6357
7042
|
const resetPlayhead = () => {
|
|
6358
7043
|
for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
|
|
6359
7044
|
barEl.textContent = "-";
|
|
7045
|
+
chordEl.textContent = "";
|
|
6360
7046
|
for (const view of laneViews) {
|
|
6361
7047
|
for (const t of view.tokens) t.el.classList.remove("is-active");
|
|
6362
7048
|
view.lane.scrollLeft = 0;
|
|
@@ -6420,6 +7106,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6420
7106
|
});
|
|
6421
7107
|
}
|
|
6422
7108
|
return {
|
|
7109
|
+
id: String(index),
|
|
6423
7110
|
model: lt.model,
|
|
6424
7111
|
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
|
|
6425
7112
|
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
@@ -6435,15 +7122,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6435
7122
|
try {
|
|
6436
7123
|
await v.loadModels(tracks.map((t) => t.model));
|
|
6437
7124
|
await v.warm(tracks);
|
|
6438
|
-
} catch (
|
|
6439
|
-
console.warn("[dtm] voice preload failed",
|
|
7125
|
+
} catch (err2) {
|
|
7126
|
+
console.warn("[dtm] voice preload failed", err2);
|
|
6440
7127
|
} finally {
|
|
6441
7128
|
overlay.remove();
|
|
6442
7129
|
}
|
|
6443
7130
|
if (!playing || activePlayer !== instance) return;
|
|
6444
7131
|
}
|
|
6445
7132
|
seq.start(0);
|
|
6446
|
-
if (streaming)
|
|
7133
|
+
if (streaming) {
|
|
7134
|
+
ensureVoices().startStream(tracks, seq.getStartTime(), {
|
|
7135
|
+
isAudible: (t) => !mutedTracks.has(Number(t.id))
|
|
7136
|
+
});
|
|
7137
|
+
}
|
|
6447
7138
|
};
|
|
6448
7139
|
const play = () => {
|
|
6449
7140
|
if (playing || trackIndices.length === 0) return;
|
|
@@ -6777,8 +7468,6 @@ var DEFAULT_CDN = {
|
|
|
6777
7468
|
soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
|
|
6778
7469
|
soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
|
|
6779
7470
|
soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs",
|
|
6780
|
-
parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs",
|
|
6781
|
-
parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs",
|
|
6782
7471
|
midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
|
|
6783
7472
|
};
|
|
6784
7473
|
var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
|
|
@@ -6822,24 +7511,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
6822
7511
|
eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
|
|
6823
7512
|
eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
|
|
6824
7513
|
]);
|
|
6825
|
-
let parseChord = eng.parseChord;
|
|
6826
|
-
let parseChords = eng.parseChords;
|
|
6827
|
-
if (features.chord && (!parseChord || !parseChords)) {
|
|
6828
|
-
try {
|
|
6829
|
-
[parseChord, parseChords] = await Promise.all([
|
|
6830
|
-
parseChord ?? importFrom(
|
|
6831
|
-
cdn.parseChord,
|
|
6832
|
-
"parseChord"
|
|
6833
|
-
),
|
|
6834
|
-
parseChords ?? importFrom(
|
|
6835
|
-
cdn.parseChords,
|
|
6836
|
-
"parseChords"
|
|
6837
|
-
)
|
|
6838
|
-
]);
|
|
6839
|
-
} catch (e) {
|
|
6840
|
-
console.warn("[dtm] \u30B3\u30FC\u30C9\u89E3\u6790\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F", e);
|
|
6841
|
-
}
|
|
6842
|
-
}
|
|
6843
7514
|
let midiParser = null;
|
|
6844
7515
|
let parseMidi;
|
|
6845
7516
|
if (features.midi) {
|
|
@@ -7025,8 +7696,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
7025
7696
|
onPlayNote: playNote,
|
|
7026
7697
|
onPlayDrum: playDrum,
|
|
7027
7698
|
singingVoices,
|
|
7028
|
-
parseChord,
|
|
7029
|
-
parseChords,
|
|
7030
7699
|
parseMidi,
|
|
7031
7700
|
onToggleRecord,
|
|
7032
7701
|
...dawOverrides
|
|
@@ -7048,15 +7717,32 @@ var createDtmStudio = async (options = {}) => {
|
|
|
7048
7717
|
editorPresetSelects.set(target, select);
|
|
7049
7718
|
select.addEventListener("change", async () => {
|
|
7050
7719
|
if (!select) return;
|
|
7051
|
-
daw.
|
|
7052
|
-
|
|
7720
|
+
const wasPlaying = daw.getPlaybackState() === "playing";
|
|
7721
|
+
if (wasPlaying) {
|
|
7722
|
+
daw.pause();
|
|
7723
|
+
}
|
|
7724
|
+
const overlay = showLoadingOverlay(target);
|
|
7725
|
+
daw.setLoading?.(true);
|
|
7726
|
+
try {
|
|
7727
|
+
daw.setInstrument(select.value);
|
|
7728
|
+
await loadPreset(select.value, trackIds);
|
|
7729
|
+
} finally {
|
|
7730
|
+
overlay.remove();
|
|
7731
|
+
daw.setLoading?.(false);
|
|
7732
|
+
if (wasPlaying) {
|
|
7733
|
+
daw.play();
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7053
7736
|
});
|
|
7054
7737
|
}
|
|
7055
7738
|
const daw = mountDAW(target, base);
|
|
7056
7739
|
mountedEditors.push(daw);
|
|
7057
7740
|
const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
|
|
7058
7741
|
daw.setInstrument(presetKey);
|
|
7059
|
-
|
|
7742
|
+
daw.setLoading?.(true);
|
|
7743
|
+
void loadPreset(presetKey, trackIds).finally(() => {
|
|
7744
|
+
daw.setLoading?.(false);
|
|
7745
|
+
});
|
|
7060
7746
|
const destroy = () => {
|
|
7061
7747
|
daw.destroy();
|
|
7062
7748
|
select?.remove();
|