@onjmin/dtm 0.1.67 → 0.1.69
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 +40 -3
- package/dist/index.d.mts +19 -53
- package/dist/index.d.ts +19 -53
- package/dist/index.js +390 -52
- package/dist/index.mjs +389 -52
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -109,6 +109,7 @@ __export(index_exports, {
|
|
|
109
109
|
playNote: () => playNote,
|
|
110
110
|
playPlacements: () => playPlacements,
|
|
111
111
|
playSingingMML: () => playSingingMML,
|
|
112
|
+
resolveLoopPoint: () => resolveLoopPoint,
|
|
112
113
|
setBackgroundActive: () => setBackgroundActive,
|
|
113
114
|
setDrawOffset: () => setDrawOffset,
|
|
114
115
|
shiftNotes: () => shiftNotes,
|
|
@@ -2775,54 +2776,73 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
2775
2776
|
forEachSungNote(track, (note, prevVowel) => {
|
|
2776
2777
|
items.push({ note, prevVowel });
|
|
2777
2778
|
});
|
|
2779
|
+
if (items.length === 0) return;
|
|
2778
2780
|
const peak = Math.max(1e-4, track.volume);
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2781
|
+
const loopStartSec = opts?.loopStartSec ?? 0;
|
|
2782
|
+
let loopOffsetSec = 0;
|
|
2783
|
+
let pass = 0;
|
|
2784
|
+
do {
|
|
2785
|
+
for (const { note, prevVowel } of items) {
|
|
2783
2786
|
if (session !== streamSession) return;
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
prevVowel,
|
|
2794
|
-
note.pitch,
|
|
2795
|
-
note.durationSec * 1e3
|
|
2796
|
-
);
|
|
2787
|
+
if (pass > 0 && note.startSec < loopStartSec - 1e-4) {
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
if (opts?.loopLengthSec && opts.loopLengthSec > 0 && note.startSec >= loopStartSec + opts.loopLengthSec - 1e-4) {
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
const startSec = note.startSec + loopOffsetSec;
|
|
2794
|
+
while (startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
|
|
2795
|
+
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
|
|
2797
2796
|
if (session !== streamSession) return;
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2797
|
+
}
|
|
2798
|
+
if (opts?.isAudible && !opts.isAudible(track)) continue;
|
|
2799
|
+
const t0 = anchorTime + startSec;
|
|
2800
|
+
if (model.renderToCache && model.scheduleCached) {
|
|
2801
|
+
const renderToCache = model.renderToCache;
|
|
2802
|
+
const scheduleCached = model.scheduleCached;
|
|
2803
|
+
void (async () => {
|
|
2804
|
+
const key = await renderToCache(
|
|
2805
|
+
note.syllable,
|
|
2806
|
+
prevVowel,
|
|
2807
|
+
note.pitch,
|
|
2808
|
+
note.durationSec * 1e3
|
|
2809
|
+
);
|
|
2810
|
+
if (session !== streamSession) return;
|
|
2811
|
+
if (key) {
|
|
2812
|
+
const delay = ctx.currentTime - t0;
|
|
2813
|
+
if (delay < 0.05) {
|
|
2814
|
+
scheduleCached(key, t0, peak, track.pan);
|
|
2815
|
+
opts?.onScheduled?.(track, note, t0);
|
|
2816
|
+
} else {
|
|
2817
|
+
console.warn(
|
|
2818
|
+
`[dtm] Synthesizer late skip: ${note.syllable.kana} at ${startSec}s (delayed by ${delay.toFixed(3)}s)`
|
|
2819
|
+
);
|
|
2820
|
+
opts?.onLateSkip?.(note, delay);
|
|
2821
|
+
}
|
|
2808
2822
|
}
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2823
|
+
})();
|
|
2824
|
+
} else {
|
|
2825
|
+
const when = t0 - ctx.currentTime;
|
|
2826
|
+
model(note.syllable, {
|
|
2827
|
+
trackId: "",
|
|
2828
|
+
pitch: note.pitch,
|
|
2829
|
+
velocity: 100,
|
|
2830
|
+
volume: peak,
|
|
2831
|
+
when,
|
|
2832
|
+
duration: note.durationSec,
|
|
2833
|
+
pan: track.pan
|
|
2834
|
+
});
|
|
2835
|
+
opts?.onScheduled?.(track, note, t0);
|
|
2836
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (opts?.loopLengthSec && opts.loopLengthSec > 0) {
|
|
2840
|
+
loopOffsetSec += opts.loopLengthSec;
|
|
2841
|
+
pass++;
|
|
2811
2842
|
} else {
|
|
2812
|
-
|
|
2813
|
-
model(note.syllable, {
|
|
2814
|
-
trackId: "",
|
|
2815
|
-
pitch: note.pitch,
|
|
2816
|
-
velocity: 100,
|
|
2817
|
-
volume: peak,
|
|
2818
|
-
when,
|
|
2819
|
-
duration: note.durationSec,
|
|
2820
|
-
pan: track.pan
|
|
2821
|
-
});
|
|
2822
|
-
opts?.onScheduled?.(track, note, t0);
|
|
2823
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2843
|
+
break;
|
|
2824
2844
|
}
|
|
2825
|
-
}
|
|
2845
|
+
} while (session === streamSession);
|
|
2826
2846
|
};
|
|
2827
2847
|
for (const track of tracks) void runTrack(track);
|
|
2828
2848
|
};
|
|
@@ -3397,6 +3417,13 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3397
3417
|
const wave = tone.wave ?? "square";
|
|
3398
3418
|
const attack = tone.attack ?? 0;
|
|
3399
3419
|
const gainScale = tone.gain ?? 1;
|
|
3420
|
+
const compressor = ctx.createDynamicsCompressor();
|
|
3421
|
+
compressor.threshold.value = -12;
|
|
3422
|
+
compressor.knee.value = 6;
|
|
3423
|
+
compressor.ratio.value = 8;
|
|
3424
|
+
compressor.attack.value = 3e-3;
|
|
3425
|
+
compressor.release.value = 0.15;
|
|
3426
|
+
compressor.connect(destination);
|
|
3400
3427
|
const playNote3 = (e) => {
|
|
3401
3428
|
const osc = ctx.createOscillator();
|
|
3402
3429
|
const gain = ctx.createGain();
|
|
@@ -3429,9 +3456,9 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3429
3456
|
panner = ctx.createStereoPanner();
|
|
3430
3457
|
panner.pan.value = Math.max(-1, Math.min(1, e.pan));
|
|
3431
3458
|
gain.connect(panner);
|
|
3432
|
-
panner.connect(
|
|
3459
|
+
panner.connect(compressor);
|
|
3433
3460
|
} else {
|
|
3434
|
-
gain.connect(
|
|
3461
|
+
gain.connect(compressor);
|
|
3435
3462
|
}
|
|
3436
3463
|
osc.start(t0);
|
|
3437
3464
|
osc.stop(t0 + e.duration + 0.02);
|
|
@@ -3453,7 +3480,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3453
3480
|
osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
|
|
3454
3481
|
g2.gain.setValueAtTime(vol * 0.135, t0);
|
|
3455
3482
|
g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
|
|
3456
|
-
osc.connect(g2).connect(
|
|
3483
|
+
osc.connect(g2).connect(compressor);
|
|
3457
3484
|
osc.start(t0);
|
|
3458
3485
|
osc.stop(t0 + 0.2);
|
|
3459
3486
|
osc.onended = () => osc.disconnect();
|
|
@@ -3472,7 +3499,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3472
3499
|
const g = ctx.createGain();
|
|
3473
3500
|
g.gain.setValueAtTime(vol * (isSnareLike ? 0.105 : 0.06), t0);
|
|
3474
3501
|
g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
|
|
3475
|
-
src.connect(filter).connect(g).connect(
|
|
3502
|
+
src.connect(filter).connect(g).connect(compressor);
|
|
3476
3503
|
src.start(t0);
|
|
3477
3504
|
src.stop(t0 + dur);
|
|
3478
3505
|
src.onended = () => {
|
|
@@ -3486,6 +3513,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3486
3513
|
|
|
3487
3514
|
// src/headless-player.ts
|
|
3488
3515
|
var STEPS_PER_BAR = 192;
|
|
3516
|
+
var TRACK_ID_BY_INDEX = ["melody", "submelody", "bass", "chord"];
|
|
3489
3517
|
var playPlacements = (placements, options) => {
|
|
3490
3518
|
const bpm = options.bpm;
|
|
3491
3519
|
const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
|
|
@@ -3504,7 +3532,11 @@ var playPlacements = (placements, options) => {
|
|
|
3504
3532
|
pitch: p.pitch,
|
|
3505
3533
|
velocity: p.velocity
|
|
3506
3534
|
}));
|
|
3507
|
-
return {
|
|
3535
|
+
return {
|
|
3536
|
+
id: TRACK_ID_BY_INDEX[index] ?? `t${index}`,
|
|
3537
|
+
volume: masterVolume,
|
|
3538
|
+
notes
|
|
3539
|
+
};
|
|
3508
3540
|
});
|
|
3509
3541
|
const ownsCtx = !options.audioContext;
|
|
3510
3542
|
const ctx = options.audioContext ?? new AudioContext();
|
|
@@ -13128,12 +13160,240 @@ var mountDAW = (target, options = {}) => {
|
|
|
13128
13160
|
};
|
|
13129
13161
|
|
|
13130
13162
|
// src/headless-singing-player.ts
|
|
13131
|
-
var
|
|
13132
|
-
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13163
|
+
var STEPS_PER_BEAT4 = 48;
|
|
13164
|
+
var STEPS_PER_BAR3 = 192;
|
|
13165
|
+
var TRACK_ID_BY_INDEX2 = ["melody", "submelody", "bass", "chord"];
|
|
13166
|
+
var playSingingMML = async (mml, options = {}) => {
|
|
13167
|
+
const {
|
|
13168
|
+
placements,
|
|
13169
|
+
bpm: parsedBpm,
|
|
13170
|
+
meta,
|
|
13171
|
+
lyrics
|
|
13172
|
+
} = parseMML(mml, {
|
|
13173
|
+
collectLyrics: true
|
|
13174
|
+
});
|
|
13175
|
+
const lyricTracks = lyrics ?? /* @__PURE__ */ new Map();
|
|
13176
|
+
const customVocalByKey = new Map(
|
|
13177
|
+
parseCustomVocals(mml).map((d) => [d.key, d])
|
|
13178
|
+
);
|
|
13179
|
+
const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
|
|
13180
|
+
const secondsPerStep = 60 / bpm / STEPS_PER_BEAT4;
|
|
13181
|
+
const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
|
|
13182
|
+
const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
|
|
13183
|
+
const drumVolume = meta.drumVolume ?? 80;
|
|
13184
|
+
const trackVolume = meta.volume ?? 100;
|
|
13185
|
+
let masterVolume = options.volume ?? 100;
|
|
13186
|
+
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
13187
|
+
(a, b) => a - b
|
|
13136
13188
|
);
|
|
13189
|
+
const seqTracks = trackIndices.map((index) => {
|
|
13190
|
+
let id = 0;
|
|
13191
|
+
const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
|
|
13192
|
+
id: id++,
|
|
13193
|
+
startStep: p.startStep,
|
|
13194
|
+
durationSteps: p.durationSteps,
|
|
13195
|
+
pitch: p.pitch,
|
|
13196
|
+
velocity: p.velocity
|
|
13197
|
+
}));
|
|
13198
|
+
return {
|
|
13199
|
+
id: TRACK_ID_BY_INDEX2[index] ?? `t${index}`,
|
|
13200
|
+
volume: trackVolume / 100 * masterVolume,
|
|
13201
|
+
notes
|
|
13202
|
+
};
|
|
13203
|
+
});
|
|
13204
|
+
const ownsCtx = !options.audioContext;
|
|
13205
|
+
const ctx = options.audioContext ?? new AudioContext();
|
|
13206
|
+
const destination = options.destination ?? ctx.destination;
|
|
13207
|
+
const useSynth = options.synth ?? !options.onPlayNote;
|
|
13208
|
+
const synth = useSynth ? createSynth(ctx, destination) : null;
|
|
13209
|
+
const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
|
|
13210
|
+
let playing = false;
|
|
13211
|
+
let destroyed = false;
|
|
13212
|
+
let voices = options.singingVoices ?? null;
|
|
13213
|
+
const buildStreamTracks = (fromStep) => [...lyricTracks.entries()].map(([index, lt]) => {
|
|
13214
|
+
const seqTrack = seqTracks.find(
|
|
13215
|
+
(t) => t.id === (TRACK_ID_BY_INDEX2[index] ?? `t${index}`)
|
|
13216
|
+
);
|
|
13217
|
+
const sorted = [...seqTrack?.notes ?? []].sort(
|
|
13218
|
+
(a, b) => a.startStep - b.startStep
|
|
13219
|
+
);
|
|
13220
|
+
const gate = (lt.gate ?? DEFAULT_GATE) / 100;
|
|
13221
|
+
const semis = (lt.octave ?? 0) * 12;
|
|
13222
|
+
const count = Math.min(sorted.length, lt.syllables.length);
|
|
13223
|
+
const notes = [];
|
|
13224
|
+
for (let i = 0; i < count; i++) {
|
|
13225
|
+
const n = sorted[i];
|
|
13226
|
+
if (n.startStep < fromStep) continue;
|
|
13227
|
+
notes.push({
|
|
13228
|
+
syllable: lt.syllables[i],
|
|
13229
|
+
pitch: n.pitch + semis,
|
|
13230
|
+
startSec: (n.startStep - fromStep) * secondsPerStep,
|
|
13231
|
+
durationSec: n.durationSteps * secondsPerStep * gate
|
|
13232
|
+
});
|
|
13233
|
+
}
|
|
13234
|
+
return {
|
|
13235
|
+
id: TRACK_ID_BY_INDEX2[index] ?? `t${index}`,
|
|
13236
|
+
model: lt.model,
|
|
13237
|
+
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME),
|
|
13238
|
+
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
13239
|
+
notes
|
|
13240
|
+
};
|
|
13241
|
+
});
|
|
13242
|
+
const seq = createSequencer({
|
|
13243
|
+
getTracks: () => seqTracks,
|
|
13244
|
+
getBpm: () => bpm,
|
|
13245
|
+
getPlayStartStep: () => 0,
|
|
13246
|
+
getDrumPattern: () => drumPattern,
|
|
13247
|
+
getSoloTrackId: () => null,
|
|
13248
|
+
getLoop: () => options.loop ?? false,
|
|
13249
|
+
cues: options.cues,
|
|
13250
|
+
onCue: options.onCue,
|
|
13251
|
+
getAudioTime: () => ctx.currentTime,
|
|
13252
|
+
onPlayNote: (e) => {
|
|
13253
|
+
const namedIdx = TRACK_ID_BY_INDEX2.indexOf(
|
|
13254
|
+
e.trackId
|
|
13255
|
+
);
|
|
13256
|
+
const trackIdx = namedIdx >= 0 ? namedIdx : Number(e.trackId);
|
|
13257
|
+
if (lyricTracks.has(trackIdx)) return;
|
|
13258
|
+
options.onPlayNote?.(e);
|
|
13259
|
+
synth?.playNote(e);
|
|
13260
|
+
},
|
|
13261
|
+
onPlayDrum: (e) => {
|
|
13262
|
+
const velocity = e.velocity * (drumVolume / 100) * (trackVolume / 100) * (masterVolume / 100);
|
|
13263
|
+
options.onPlayDrum?.({ ...e, velocity });
|
|
13264
|
+
synth?.playDrum({ ...e, velocity });
|
|
13265
|
+
},
|
|
13266
|
+
onTick: (step) => {
|
|
13267
|
+
options.onTick?.(step);
|
|
13268
|
+
},
|
|
13269
|
+
onEnd: (_interrupted) => finish(),
|
|
13270
|
+
stepsPerBar: STEPS_PER_BAR3
|
|
13271
|
+
});
|
|
13272
|
+
const finish = () => {
|
|
13273
|
+
if (!playing) return;
|
|
13274
|
+
playing = false;
|
|
13275
|
+
voices?.stopStream();
|
|
13276
|
+
options.onStop?.();
|
|
13277
|
+
};
|
|
13278
|
+
const onVisibilityChange = () => {
|
|
13279
|
+
if (!playing) return;
|
|
13280
|
+
if (document.hidden) {
|
|
13281
|
+
void ctx.suspend();
|
|
13282
|
+
} else if (ctx.state === "suspended") {
|
|
13283
|
+
void ctx.resume();
|
|
13284
|
+
}
|
|
13285
|
+
};
|
|
13286
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
13287
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
13288
|
+
}
|
|
13289
|
+
const stop = () => {
|
|
13290
|
+
if (!playing) return;
|
|
13291
|
+
seq.stop();
|
|
13292
|
+
finish();
|
|
13293
|
+
};
|
|
13294
|
+
const setVolume = (volume) => {
|
|
13295
|
+
masterVolume = volume;
|
|
13296
|
+
const effectiveTrackVolume = trackVolume / 100 * masterVolume;
|
|
13297
|
+
for (const t of seqTracks) t.volume = effectiveTrackVolume;
|
|
13298
|
+
voices?.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
13299
|
+
};
|
|
13300
|
+
const suspend = () => ctx.suspend();
|
|
13301
|
+
const resume = () => ctx.resume();
|
|
13302
|
+
const destroy = () => {
|
|
13303
|
+
seq.stop();
|
|
13304
|
+
playing = false;
|
|
13305
|
+
destroyed = true;
|
|
13306
|
+
voices?.reset();
|
|
13307
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
13308
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
13309
|
+
}
|
|
13310
|
+
if (ownsCtx && ctx.state !== "closed") {
|
|
13311
|
+
void ctx.close();
|
|
13312
|
+
}
|
|
13313
|
+
};
|
|
13314
|
+
const playback = {
|
|
13315
|
+
stop,
|
|
13316
|
+
isPlaying: () => playing,
|
|
13317
|
+
setVolume,
|
|
13318
|
+
suspend,
|
|
13319
|
+
resume,
|
|
13320
|
+
destroy
|
|
13321
|
+
};
|
|
13322
|
+
playing = true;
|
|
13323
|
+
try {
|
|
13324
|
+
const resumes = [];
|
|
13325
|
+
const r = options.onResumeAudio?.();
|
|
13326
|
+
if (r) resumes.push(Promise.resolve(r));
|
|
13327
|
+
if (ctx.state === "suspended") resumes.push(ctx.resume());
|
|
13328
|
+
if (resumes.length > 0) await Promise.all(resumes);
|
|
13329
|
+
if (!playing || destroyed) {
|
|
13330
|
+
return playback;
|
|
13331
|
+
}
|
|
13332
|
+
if (lyricTracks.size > 0) {
|
|
13333
|
+
if (!voices) {
|
|
13334
|
+
voices = createSingingVoices(ctx, destination, {
|
|
13335
|
+
voiceWorkerUrl: options.voiceWorkerUrl
|
|
13336
|
+
});
|
|
13337
|
+
}
|
|
13338
|
+
if (customVocalByKey.size > 0 && voices.registerVoicebanks) {
|
|
13339
|
+
voices.registerVoicebanks(
|
|
13340
|
+
Object.fromEntries([...customVocalByKey].map(([k, d]) => [k, d.url]))
|
|
13341
|
+
);
|
|
13342
|
+
}
|
|
13343
|
+
const streamTracks = buildStreamTracks(0);
|
|
13344
|
+
await voices.loadModels(streamTracks.map((t) => t.model));
|
|
13345
|
+
if (!playing || destroyed) {
|
|
13346
|
+
return playback;
|
|
13347
|
+
}
|
|
13348
|
+
await voices.warm(streamTracks, PREWARM_NOTES);
|
|
13349
|
+
if (!playing || destroyed) {
|
|
13350
|
+
return playback;
|
|
13351
|
+
}
|
|
13352
|
+
let loopLengthSec;
|
|
13353
|
+
let loopStartSec;
|
|
13354
|
+
const loopOption = options.loop ?? false;
|
|
13355
|
+
if (loopOption) {
|
|
13356
|
+
let loopStartStep = 0;
|
|
13357
|
+
let loopEndStep = -1;
|
|
13358
|
+
if (typeof loopOption === "object") {
|
|
13359
|
+
loopStartStep = loopOption.start ? resolveLoopPoint(
|
|
13360
|
+
loopOption.start,
|
|
13361
|
+
bpm,
|
|
13362
|
+
STEPS_PER_BAR3,
|
|
13363
|
+
secondsPerStep
|
|
13364
|
+
) : 0;
|
|
13365
|
+
const endVal = loopOption.end ? resolveLoopPoint(
|
|
13366
|
+
loopOption.end,
|
|
13367
|
+
bpm,
|
|
13368
|
+
STEPS_PER_BAR3,
|
|
13369
|
+
secondsPerStep
|
|
13370
|
+
) : null;
|
|
13371
|
+
loopEndStep = endVal !== null ? endVal : -1;
|
|
13372
|
+
}
|
|
13373
|
+
if (loopEndStep === -1) {
|
|
13374
|
+
let maxEndStep = 0;
|
|
13375
|
+
for (const p of placements) {
|
|
13376
|
+
maxEndStep = Math.max(maxEndStep, p.startStep + p.durationSteps);
|
|
13377
|
+
}
|
|
13378
|
+
loopEndStep = maxEndStep;
|
|
13379
|
+
}
|
|
13380
|
+
loopStartSec = loopStartStep * secondsPerStep;
|
|
13381
|
+
loopLengthSec = (loopEndStep - loopStartStep) * secondsPerStep;
|
|
13382
|
+
}
|
|
13383
|
+
seq.start(0);
|
|
13384
|
+
voices.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
13385
|
+
voices.startStream(streamTracks, seq.getStartTime(), {
|
|
13386
|
+
loopLengthSec,
|
|
13387
|
+
loopStartSec
|
|
13388
|
+
});
|
|
13389
|
+
} else {
|
|
13390
|
+
seq.start(0);
|
|
13391
|
+
}
|
|
13392
|
+
} catch (err2) {
|
|
13393
|
+
stop();
|
|
13394
|
+
throw err2;
|
|
13395
|
+
}
|
|
13396
|
+
return playback;
|
|
13137
13397
|
};
|
|
13138
13398
|
|
|
13139
13399
|
// src/piano-roll.ts
|
|
@@ -14288,6 +14548,80 @@ var createDtmStudio = async (options = {}) => {
|
|
|
14288
14548
|
onResumeAudio: resumeAudio
|
|
14289
14549
|
});
|
|
14290
14550
|
};
|
|
14551
|
+
const playSingingMMLInstance = (mml, opts = {}) => {
|
|
14552
|
+
const { placements } = parseMML(mml);
|
|
14553
|
+
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
14554
|
+
(a, b) => a - b
|
|
14555
|
+
);
|
|
14556
|
+
const isAdvancedMode = trackIndices.some((idx) => idx >= 4);
|
|
14557
|
+
const playerPreset = defaultPreset;
|
|
14558
|
+
void loadPreset(
|
|
14559
|
+
playerPreset,
|
|
14560
|
+
trackIndices.map(String),
|
|
14561
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14562
|
+
);
|
|
14563
|
+
const playPlayerNote = (e) => {
|
|
14564
|
+
const trackIdx = Number(e.trackId);
|
|
14565
|
+
const role = TRACK_ROLES[trackIdx] ?? "melody";
|
|
14566
|
+
let sfInst = resolveSoundFont(
|
|
14567
|
+
playerPreset,
|
|
14568
|
+
role,
|
|
14569
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14570
|
+
);
|
|
14571
|
+
if (!sfInst) {
|
|
14572
|
+
void loadPreset(
|
|
14573
|
+
playerPreset,
|
|
14574
|
+
[role],
|
|
14575
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14576
|
+
);
|
|
14577
|
+
sfInst = resolveSoundFont(
|
|
14578
|
+
playerPreset,
|
|
14579
|
+
role,
|
|
14580
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14581
|
+
);
|
|
14582
|
+
}
|
|
14583
|
+
if (!sfInst) return;
|
|
14584
|
+
sfInst.play({
|
|
14585
|
+
ctx: audioCtx,
|
|
14586
|
+
destination: masterGain,
|
|
14587
|
+
pitch: e.pitch,
|
|
14588
|
+
volume: e.volume,
|
|
14589
|
+
when: e.when,
|
|
14590
|
+
duration: e.duration
|
|
14591
|
+
});
|
|
14592
|
+
};
|
|
14593
|
+
return playSingingMML(mml, {
|
|
14594
|
+
...opts,
|
|
14595
|
+
audioContext: audioCtx,
|
|
14596
|
+
destination: masterGain,
|
|
14597
|
+
synth: false,
|
|
14598
|
+
singingVoices,
|
|
14599
|
+
onPlayNote: playPlayerNote,
|
|
14600
|
+
onPlayDrum: playDrum,
|
|
14601
|
+
onResumeAudio: resumeAudio
|
|
14602
|
+
});
|
|
14603
|
+
};
|
|
14604
|
+
const playNoteEvent = (e) => {
|
|
14605
|
+
const idx = Number(e.trackId);
|
|
14606
|
+
const role = TRACK_ROLES[idx] ?? "melody";
|
|
14607
|
+
let sfInst = resolveSoundFont(defaultPreset, role, "simple");
|
|
14608
|
+
if (!sfInst) {
|
|
14609
|
+
void loadPreset(defaultPreset, [role], "simple");
|
|
14610
|
+
sfInst = resolveSoundFont(defaultPreset, role, "simple");
|
|
14611
|
+
}
|
|
14612
|
+
if (!sfInst) return;
|
|
14613
|
+
sfInst.play({
|
|
14614
|
+
ctx: audioCtx,
|
|
14615
|
+
destination: masterGain,
|
|
14616
|
+
pitch: e.pitch,
|
|
14617
|
+
volume: e.volume,
|
|
14618
|
+
when: e.when,
|
|
14619
|
+
duration: e.duration
|
|
14620
|
+
});
|
|
14621
|
+
};
|
|
14622
|
+
const playDrumEvent = (e) => {
|
|
14623
|
+
playDrum(e);
|
|
14624
|
+
};
|
|
14291
14625
|
const playNote3 = async (options2) => {
|
|
14292
14626
|
await listReady;
|
|
14293
14627
|
const key = options2.instrument ? resolveNameToKey(options2.instrument) : void 0;
|
|
@@ -14393,6 +14727,9 @@ var createDtmStudio = async (options = {}) => {
|
|
|
14393
14727
|
mountPlayer,
|
|
14394
14728
|
mountChordPlayer: mountChordPlayerInstance,
|
|
14395
14729
|
play,
|
|
14730
|
+
playSingingMML: playSingingMMLInstance,
|
|
14731
|
+
playNoteEvent,
|
|
14732
|
+
playDrumEvent,
|
|
14396
14733
|
playNote: playNote3,
|
|
14397
14734
|
playChords: playChords3,
|
|
14398
14735
|
loadPreset,
|
|
@@ -14493,6 +14830,7 @@ var createDtmStudio = async (options = {}) => {
|
|
|
14493
14830
|
playNote,
|
|
14494
14831
|
playPlacements,
|
|
14495
14832
|
playSingingMML,
|
|
14833
|
+
resolveLoopPoint,
|
|
14496
14834
|
setBackgroundActive,
|
|
14497
14835
|
setDrawOffset,
|
|
14498
14836
|
shiftNotes,
|