@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.mjs
CHANGED
|
@@ -2653,54 +2653,73 @@ var createSingingVoices = (ctx, destination, options = {}) => {
|
|
|
2653
2653
|
forEachSungNote(track, (note, prevVowel) => {
|
|
2654
2654
|
items.push({ note, prevVowel });
|
|
2655
2655
|
});
|
|
2656
|
+
if (items.length === 0) return;
|
|
2656
2657
|
const peak = Math.max(1e-4, track.volume);
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2658
|
+
const loopStartSec = opts?.loopStartSec ?? 0;
|
|
2659
|
+
let loopOffsetSec = 0;
|
|
2660
|
+
let pass = 0;
|
|
2661
|
+
do {
|
|
2662
|
+
for (const { note, prevVowel } of items) {
|
|
2661
2663
|
if (session !== streamSession) return;
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
prevVowel,
|
|
2672
|
-
note.pitch,
|
|
2673
|
-
note.durationSec * 1e3
|
|
2674
|
-
);
|
|
2664
|
+
if (pass > 0 && note.startSec < loopStartSec - 1e-4) {
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
if (opts?.loopLengthSec && opts.loopLengthSec > 0 && note.startSec >= loopStartSec + opts.loopLengthSec - 1e-4) {
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
const startSec = note.startSec + loopOffsetSec;
|
|
2671
|
+
while (startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
|
|
2672
|
+
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
|
|
2675
2673
|
if (session !== streamSession) return;
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2674
|
+
}
|
|
2675
|
+
if (opts?.isAudible && !opts.isAudible(track)) continue;
|
|
2676
|
+
const t0 = anchorTime + startSec;
|
|
2677
|
+
if (model.renderToCache && model.scheduleCached) {
|
|
2678
|
+
const renderToCache = model.renderToCache;
|
|
2679
|
+
const scheduleCached = model.scheduleCached;
|
|
2680
|
+
void (async () => {
|
|
2681
|
+
const key = await renderToCache(
|
|
2682
|
+
note.syllable,
|
|
2683
|
+
prevVowel,
|
|
2684
|
+
note.pitch,
|
|
2685
|
+
note.durationSec * 1e3
|
|
2686
|
+
);
|
|
2687
|
+
if (session !== streamSession) return;
|
|
2688
|
+
if (key) {
|
|
2689
|
+
const delay = ctx.currentTime - t0;
|
|
2690
|
+
if (delay < 0.05) {
|
|
2691
|
+
scheduleCached(key, t0, peak, track.pan);
|
|
2692
|
+
opts?.onScheduled?.(track, note, t0);
|
|
2693
|
+
} else {
|
|
2694
|
+
console.warn(
|
|
2695
|
+
`[dtm] Synthesizer late skip: ${note.syllable.kana} at ${startSec}s (delayed by ${delay.toFixed(3)}s)`
|
|
2696
|
+
);
|
|
2697
|
+
opts?.onLateSkip?.(note, delay);
|
|
2698
|
+
}
|
|
2686
2699
|
}
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2700
|
+
})();
|
|
2701
|
+
} else {
|
|
2702
|
+
const when = t0 - ctx.currentTime;
|
|
2703
|
+
model(note.syllable, {
|
|
2704
|
+
trackId: "",
|
|
2705
|
+
pitch: note.pitch,
|
|
2706
|
+
velocity: 100,
|
|
2707
|
+
volume: peak,
|
|
2708
|
+
when,
|
|
2709
|
+
duration: note.durationSec,
|
|
2710
|
+
pan: track.pan
|
|
2711
|
+
});
|
|
2712
|
+
opts?.onScheduled?.(track, note, t0);
|
|
2713
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
if (opts?.loopLengthSec && opts.loopLengthSec > 0) {
|
|
2717
|
+
loopOffsetSec += opts.loopLengthSec;
|
|
2718
|
+
pass++;
|
|
2689
2719
|
} else {
|
|
2690
|
-
|
|
2691
|
-
model(note.syllable, {
|
|
2692
|
-
trackId: "",
|
|
2693
|
-
pitch: note.pitch,
|
|
2694
|
-
velocity: 100,
|
|
2695
|
-
volume: peak,
|
|
2696
|
-
when,
|
|
2697
|
-
duration: note.durationSec,
|
|
2698
|
-
pan: track.pan
|
|
2699
|
-
});
|
|
2700
|
-
opts?.onScheduled?.(track, note, t0);
|
|
2701
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2720
|
+
break;
|
|
2702
2721
|
}
|
|
2703
|
-
}
|
|
2722
|
+
} while (session === streamSession);
|
|
2704
2723
|
};
|
|
2705
2724
|
for (const track of tracks) void runTrack(track);
|
|
2706
2725
|
};
|
|
@@ -3275,6 +3294,13 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3275
3294
|
const wave = tone.wave ?? "square";
|
|
3276
3295
|
const attack = tone.attack ?? 0;
|
|
3277
3296
|
const gainScale = tone.gain ?? 1;
|
|
3297
|
+
const compressor = ctx.createDynamicsCompressor();
|
|
3298
|
+
compressor.threshold.value = -12;
|
|
3299
|
+
compressor.knee.value = 6;
|
|
3300
|
+
compressor.ratio.value = 8;
|
|
3301
|
+
compressor.attack.value = 3e-3;
|
|
3302
|
+
compressor.release.value = 0.15;
|
|
3303
|
+
compressor.connect(destination);
|
|
3278
3304
|
const playNote3 = (e) => {
|
|
3279
3305
|
const osc = ctx.createOscillator();
|
|
3280
3306
|
const gain = ctx.createGain();
|
|
@@ -3307,9 +3333,9 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3307
3333
|
panner = ctx.createStereoPanner();
|
|
3308
3334
|
panner.pan.value = Math.max(-1, Math.min(1, e.pan));
|
|
3309
3335
|
gain.connect(panner);
|
|
3310
|
-
panner.connect(
|
|
3336
|
+
panner.connect(compressor);
|
|
3311
3337
|
} else {
|
|
3312
|
-
gain.connect(
|
|
3338
|
+
gain.connect(compressor);
|
|
3313
3339
|
}
|
|
3314
3340
|
osc.start(t0);
|
|
3315
3341
|
osc.stop(t0 + e.duration + 0.02);
|
|
@@ -3331,7 +3357,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3331
3357
|
osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
|
|
3332
3358
|
g2.gain.setValueAtTime(vol * 0.135, t0);
|
|
3333
3359
|
g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
|
|
3334
|
-
osc.connect(g2).connect(
|
|
3360
|
+
osc.connect(g2).connect(compressor);
|
|
3335
3361
|
osc.start(t0);
|
|
3336
3362
|
osc.stop(t0 + 0.2);
|
|
3337
3363
|
osc.onended = () => osc.disconnect();
|
|
@@ -3350,7 +3376,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3350
3376
|
const g = ctx.createGain();
|
|
3351
3377
|
g.gain.setValueAtTime(vol * (isSnareLike ? 0.105 : 0.06), t0);
|
|
3352
3378
|
g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
|
|
3353
|
-
src.connect(filter).connect(g).connect(
|
|
3379
|
+
src.connect(filter).connect(g).connect(compressor);
|
|
3354
3380
|
src.start(t0);
|
|
3355
3381
|
src.stop(t0 + dur);
|
|
3356
3382
|
src.onended = () => {
|
|
@@ -3364,6 +3390,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
|
|
|
3364
3390
|
|
|
3365
3391
|
// src/headless-player.ts
|
|
3366
3392
|
var STEPS_PER_BAR = 192;
|
|
3393
|
+
var TRACK_ID_BY_INDEX = ["melody", "submelody", "bass", "chord"];
|
|
3367
3394
|
var playPlacements = (placements, options) => {
|
|
3368
3395
|
const bpm = options.bpm;
|
|
3369
3396
|
const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
|
|
@@ -3382,7 +3409,11 @@ var playPlacements = (placements, options) => {
|
|
|
3382
3409
|
pitch: p.pitch,
|
|
3383
3410
|
velocity: p.velocity
|
|
3384
3411
|
}));
|
|
3385
|
-
return {
|
|
3412
|
+
return {
|
|
3413
|
+
id: TRACK_ID_BY_INDEX[index] ?? `t${index}`,
|
|
3414
|
+
volume: masterVolume,
|
|
3415
|
+
notes
|
|
3416
|
+
};
|
|
3386
3417
|
});
|
|
3387
3418
|
const ownsCtx = !options.audioContext;
|
|
3388
3419
|
const ctx = options.audioContext ?? new AudioContext();
|
|
@@ -13006,12 +13037,240 @@ var mountDAW = (target, options = {}) => {
|
|
|
13006
13037
|
};
|
|
13007
13038
|
|
|
13008
13039
|
// src/headless-singing-player.ts
|
|
13009
|
-
var
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
|
|
13040
|
+
var STEPS_PER_BEAT4 = 48;
|
|
13041
|
+
var STEPS_PER_BAR3 = 192;
|
|
13042
|
+
var TRACK_ID_BY_INDEX2 = ["melody", "submelody", "bass", "chord"];
|
|
13043
|
+
var playSingingMML = async (mml, options = {}) => {
|
|
13044
|
+
const {
|
|
13045
|
+
placements,
|
|
13046
|
+
bpm: parsedBpm,
|
|
13047
|
+
meta,
|
|
13048
|
+
lyrics
|
|
13049
|
+
} = parseMML(mml, {
|
|
13050
|
+
collectLyrics: true
|
|
13051
|
+
});
|
|
13052
|
+
const lyricTracks = lyrics ?? /* @__PURE__ */ new Map();
|
|
13053
|
+
const customVocalByKey = new Map(
|
|
13054
|
+
parseCustomVocals(mml).map((d) => [d.key, d])
|
|
13055
|
+
);
|
|
13056
|
+
const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
|
|
13057
|
+
const secondsPerStep = 60 / bpm / STEPS_PER_BEAT4;
|
|
13058
|
+
const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
|
|
13059
|
+
const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
|
|
13060
|
+
const drumVolume = meta.drumVolume ?? 80;
|
|
13061
|
+
const trackVolume = meta.volume ?? 100;
|
|
13062
|
+
let masterVolume = options.volume ?? 100;
|
|
13063
|
+
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
13064
|
+
(a, b) => a - b
|
|
13014
13065
|
);
|
|
13066
|
+
const seqTracks = trackIndices.map((index) => {
|
|
13067
|
+
let id = 0;
|
|
13068
|
+
const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
|
|
13069
|
+
id: id++,
|
|
13070
|
+
startStep: p.startStep,
|
|
13071
|
+
durationSteps: p.durationSteps,
|
|
13072
|
+
pitch: p.pitch,
|
|
13073
|
+
velocity: p.velocity
|
|
13074
|
+
}));
|
|
13075
|
+
return {
|
|
13076
|
+
id: TRACK_ID_BY_INDEX2[index] ?? `t${index}`,
|
|
13077
|
+
volume: trackVolume / 100 * masterVolume,
|
|
13078
|
+
notes
|
|
13079
|
+
};
|
|
13080
|
+
});
|
|
13081
|
+
const ownsCtx = !options.audioContext;
|
|
13082
|
+
const ctx = options.audioContext ?? new AudioContext();
|
|
13083
|
+
const destination = options.destination ?? ctx.destination;
|
|
13084
|
+
const useSynth = options.synth ?? !options.onPlayNote;
|
|
13085
|
+
const synth = useSynth ? createSynth(ctx, destination) : null;
|
|
13086
|
+
const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
|
|
13087
|
+
let playing = false;
|
|
13088
|
+
let destroyed = false;
|
|
13089
|
+
let voices = options.singingVoices ?? null;
|
|
13090
|
+
const buildStreamTracks = (fromStep) => [...lyricTracks.entries()].map(([index, lt]) => {
|
|
13091
|
+
const seqTrack = seqTracks.find(
|
|
13092
|
+
(t) => t.id === (TRACK_ID_BY_INDEX2[index] ?? `t${index}`)
|
|
13093
|
+
);
|
|
13094
|
+
const sorted = [...seqTrack?.notes ?? []].sort(
|
|
13095
|
+
(a, b) => a.startStep - b.startStep
|
|
13096
|
+
);
|
|
13097
|
+
const gate = (lt.gate ?? DEFAULT_GATE) / 100;
|
|
13098
|
+
const semis = (lt.octave ?? 0) * 12;
|
|
13099
|
+
const count = Math.min(sorted.length, lt.syllables.length);
|
|
13100
|
+
const notes = [];
|
|
13101
|
+
for (let i = 0; i < count; i++) {
|
|
13102
|
+
const n = sorted[i];
|
|
13103
|
+
if (n.startStep < fromStep) continue;
|
|
13104
|
+
notes.push({
|
|
13105
|
+
syllable: lt.syllables[i],
|
|
13106
|
+
pitch: n.pitch + semis,
|
|
13107
|
+
startSec: (n.startStep - fromStep) * secondsPerStep,
|
|
13108
|
+
durationSec: n.durationSteps * secondsPerStep * gate
|
|
13109
|
+
});
|
|
13110
|
+
}
|
|
13111
|
+
return {
|
|
13112
|
+
id: TRACK_ID_BY_INDEX2[index] ?? `t${index}`,
|
|
13113
|
+
model: lt.model,
|
|
13114
|
+
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME),
|
|
13115
|
+
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
13116
|
+
notes
|
|
13117
|
+
};
|
|
13118
|
+
});
|
|
13119
|
+
const seq = createSequencer({
|
|
13120
|
+
getTracks: () => seqTracks,
|
|
13121
|
+
getBpm: () => bpm,
|
|
13122
|
+
getPlayStartStep: () => 0,
|
|
13123
|
+
getDrumPattern: () => drumPattern,
|
|
13124
|
+
getSoloTrackId: () => null,
|
|
13125
|
+
getLoop: () => options.loop ?? false,
|
|
13126
|
+
cues: options.cues,
|
|
13127
|
+
onCue: options.onCue,
|
|
13128
|
+
getAudioTime: () => ctx.currentTime,
|
|
13129
|
+
onPlayNote: (e) => {
|
|
13130
|
+
const namedIdx = TRACK_ID_BY_INDEX2.indexOf(
|
|
13131
|
+
e.trackId
|
|
13132
|
+
);
|
|
13133
|
+
const trackIdx = namedIdx >= 0 ? namedIdx : Number(e.trackId);
|
|
13134
|
+
if (lyricTracks.has(trackIdx)) return;
|
|
13135
|
+
options.onPlayNote?.(e);
|
|
13136
|
+
synth?.playNote(e);
|
|
13137
|
+
},
|
|
13138
|
+
onPlayDrum: (e) => {
|
|
13139
|
+
const velocity = e.velocity * (drumVolume / 100) * (trackVolume / 100) * (masterVolume / 100);
|
|
13140
|
+
options.onPlayDrum?.({ ...e, velocity });
|
|
13141
|
+
synth?.playDrum({ ...e, velocity });
|
|
13142
|
+
},
|
|
13143
|
+
onTick: (step) => {
|
|
13144
|
+
options.onTick?.(step);
|
|
13145
|
+
},
|
|
13146
|
+
onEnd: (_interrupted) => finish(),
|
|
13147
|
+
stepsPerBar: STEPS_PER_BAR3
|
|
13148
|
+
});
|
|
13149
|
+
const finish = () => {
|
|
13150
|
+
if (!playing) return;
|
|
13151
|
+
playing = false;
|
|
13152
|
+
voices?.stopStream();
|
|
13153
|
+
options.onStop?.();
|
|
13154
|
+
};
|
|
13155
|
+
const onVisibilityChange = () => {
|
|
13156
|
+
if (!playing) return;
|
|
13157
|
+
if (document.hidden) {
|
|
13158
|
+
void ctx.suspend();
|
|
13159
|
+
} else if (ctx.state === "suspended") {
|
|
13160
|
+
void ctx.resume();
|
|
13161
|
+
}
|
|
13162
|
+
};
|
|
13163
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
13164
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
13165
|
+
}
|
|
13166
|
+
const stop = () => {
|
|
13167
|
+
if (!playing) return;
|
|
13168
|
+
seq.stop();
|
|
13169
|
+
finish();
|
|
13170
|
+
};
|
|
13171
|
+
const setVolume = (volume) => {
|
|
13172
|
+
masterVolume = volume;
|
|
13173
|
+
const effectiveTrackVolume = trackVolume / 100 * masterVolume;
|
|
13174
|
+
for (const t of seqTracks) t.volume = effectiveTrackVolume;
|
|
13175
|
+
voices?.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
13176
|
+
};
|
|
13177
|
+
const suspend = () => ctx.suspend();
|
|
13178
|
+
const resume = () => ctx.resume();
|
|
13179
|
+
const destroy = () => {
|
|
13180
|
+
seq.stop();
|
|
13181
|
+
playing = false;
|
|
13182
|
+
destroyed = true;
|
|
13183
|
+
voices?.reset();
|
|
13184
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
13185
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
13186
|
+
}
|
|
13187
|
+
if (ownsCtx && ctx.state !== "closed") {
|
|
13188
|
+
void ctx.close();
|
|
13189
|
+
}
|
|
13190
|
+
};
|
|
13191
|
+
const playback = {
|
|
13192
|
+
stop,
|
|
13193
|
+
isPlaying: () => playing,
|
|
13194
|
+
setVolume,
|
|
13195
|
+
suspend,
|
|
13196
|
+
resume,
|
|
13197
|
+
destroy
|
|
13198
|
+
};
|
|
13199
|
+
playing = true;
|
|
13200
|
+
try {
|
|
13201
|
+
const resumes = [];
|
|
13202
|
+
const r = options.onResumeAudio?.();
|
|
13203
|
+
if (r) resumes.push(Promise.resolve(r));
|
|
13204
|
+
if (ctx.state === "suspended") resumes.push(ctx.resume());
|
|
13205
|
+
if (resumes.length > 0) await Promise.all(resumes);
|
|
13206
|
+
if (!playing || destroyed) {
|
|
13207
|
+
return playback;
|
|
13208
|
+
}
|
|
13209
|
+
if (lyricTracks.size > 0) {
|
|
13210
|
+
if (!voices) {
|
|
13211
|
+
voices = createSingingVoices(ctx, destination, {
|
|
13212
|
+
voiceWorkerUrl: options.voiceWorkerUrl
|
|
13213
|
+
});
|
|
13214
|
+
}
|
|
13215
|
+
if (customVocalByKey.size > 0 && voices.registerVoicebanks) {
|
|
13216
|
+
voices.registerVoicebanks(
|
|
13217
|
+
Object.fromEntries([...customVocalByKey].map(([k, d]) => [k, d.url]))
|
|
13218
|
+
);
|
|
13219
|
+
}
|
|
13220
|
+
const streamTracks = buildStreamTracks(0);
|
|
13221
|
+
await voices.loadModels(streamTracks.map((t) => t.model));
|
|
13222
|
+
if (!playing || destroyed) {
|
|
13223
|
+
return playback;
|
|
13224
|
+
}
|
|
13225
|
+
await voices.warm(streamTracks, PREWARM_NOTES);
|
|
13226
|
+
if (!playing || destroyed) {
|
|
13227
|
+
return playback;
|
|
13228
|
+
}
|
|
13229
|
+
let loopLengthSec;
|
|
13230
|
+
let loopStartSec;
|
|
13231
|
+
const loopOption = options.loop ?? false;
|
|
13232
|
+
if (loopOption) {
|
|
13233
|
+
let loopStartStep = 0;
|
|
13234
|
+
let loopEndStep = -1;
|
|
13235
|
+
if (typeof loopOption === "object") {
|
|
13236
|
+
loopStartStep = loopOption.start ? resolveLoopPoint(
|
|
13237
|
+
loopOption.start,
|
|
13238
|
+
bpm,
|
|
13239
|
+
STEPS_PER_BAR3,
|
|
13240
|
+
secondsPerStep
|
|
13241
|
+
) : 0;
|
|
13242
|
+
const endVal = loopOption.end ? resolveLoopPoint(
|
|
13243
|
+
loopOption.end,
|
|
13244
|
+
bpm,
|
|
13245
|
+
STEPS_PER_BAR3,
|
|
13246
|
+
secondsPerStep
|
|
13247
|
+
) : null;
|
|
13248
|
+
loopEndStep = endVal !== null ? endVal : -1;
|
|
13249
|
+
}
|
|
13250
|
+
if (loopEndStep === -1) {
|
|
13251
|
+
let maxEndStep = 0;
|
|
13252
|
+
for (const p of placements) {
|
|
13253
|
+
maxEndStep = Math.max(maxEndStep, p.startStep + p.durationSteps);
|
|
13254
|
+
}
|
|
13255
|
+
loopEndStep = maxEndStep;
|
|
13256
|
+
}
|
|
13257
|
+
loopStartSec = loopStartStep * secondsPerStep;
|
|
13258
|
+
loopLengthSec = (loopEndStep - loopStartStep) * secondsPerStep;
|
|
13259
|
+
}
|
|
13260
|
+
seq.start(0);
|
|
13261
|
+
voices.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
13262
|
+
voices.startStream(streamTracks, seq.getStartTime(), {
|
|
13263
|
+
loopLengthSec,
|
|
13264
|
+
loopStartSec
|
|
13265
|
+
});
|
|
13266
|
+
} else {
|
|
13267
|
+
seq.start(0);
|
|
13268
|
+
}
|
|
13269
|
+
} catch (err2) {
|
|
13270
|
+
stop();
|
|
13271
|
+
throw err2;
|
|
13272
|
+
}
|
|
13273
|
+
return playback;
|
|
13015
13274
|
};
|
|
13016
13275
|
|
|
13017
13276
|
// src/piano-roll.ts
|
|
@@ -14165,6 +14424,80 @@ var createDtmStudio = async (options = {}) => {
|
|
|
14165
14424
|
onResumeAudio: resumeAudio
|
|
14166
14425
|
});
|
|
14167
14426
|
};
|
|
14427
|
+
const playSingingMMLInstance = (mml, opts = {}) => {
|
|
14428
|
+
const { placements } = parseMML(mml);
|
|
14429
|
+
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
14430
|
+
(a, b) => a - b
|
|
14431
|
+
);
|
|
14432
|
+
const isAdvancedMode = trackIndices.some((idx) => idx >= 4);
|
|
14433
|
+
const playerPreset = defaultPreset;
|
|
14434
|
+
void loadPreset(
|
|
14435
|
+
playerPreset,
|
|
14436
|
+
trackIndices.map(String),
|
|
14437
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14438
|
+
);
|
|
14439
|
+
const playPlayerNote = (e) => {
|
|
14440
|
+
const trackIdx = Number(e.trackId);
|
|
14441
|
+
const role = TRACK_ROLES[trackIdx] ?? "melody";
|
|
14442
|
+
let sfInst = resolveSoundFont(
|
|
14443
|
+
playerPreset,
|
|
14444
|
+
role,
|
|
14445
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14446
|
+
);
|
|
14447
|
+
if (!sfInst) {
|
|
14448
|
+
void loadPreset(
|
|
14449
|
+
playerPreset,
|
|
14450
|
+
[role],
|
|
14451
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14452
|
+
);
|
|
14453
|
+
sfInst = resolveSoundFont(
|
|
14454
|
+
playerPreset,
|
|
14455
|
+
role,
|
|
14456
|
+
isAdvancedMode ? "advanced" : "simple"
|
|
14457
|
+
);
|
|
14458
|
+
}
|
|
14459
|
+
if (!sfInst) return;
|
|
14460
|
+
sfInst.play({
|
|
14461
|
+
ctx: audioCtx,
|
|
14462
|
+
destination: masterGain,
|
|
14463
|
+
pitch: e.pitch,
|
|
14464
|
+
volume: e.volume,
|
|
14465
|
+
when: e.when,
|
|
14466
|
+
duration: e.duration
|
|
14467
|
+
});
|
|
14468
|
+
};
|
|
14469
|
+
return playSingingMML(mml, {
|
|
14470
|
+
...opts,
|
|
14471
|
+
audioContext: audioCtx,
|
|
14472
|
+
destination: masterGain,
|
|
14473
|
+
synth: false,
|
|
14474
|
+
singingVoices,
|
|
14475
|
+
onPlayNote: playPlayerNote,
|
|
14476
|
+
onPlayDrum: playDrum,
|
|
14477
|
+
onResumeAudio: resumeAudio
|
|
14478
|
+
});
|
|
14479
|
+
};
|
|
14480
|
+
const playNoteEvent = (e) => {
|
|
14481
|
+
const idx = Number(e.trackId);
|
|
14482
|
+
const role = TRACK_ROLES[idx] ?? "melody";
|
|
14483
|
+
let sfInst = resolveSoundFont(defaultPreset, role, "simple");
|
|
14484
|
+
if (!sfInst) {
|
|
14485
|
+
void loadPreset(defaultPreset, [role], "simple");
|
|
14486
|
+
sfInst = resolveSoundFont(defaultPreset, role, "simple");
|
|
14487
|
+
}
|
|
14488
|
+
if (!sfInst) return;
|
|
14489
|
+
sfInst.play({
|
|
14490
|
+
ctx: audioCtx,
|
|
14491
|
+
destination: masterGain,
|
|
14492
|
+
pitch: e.pitch,
|
|
14493
|
+
volume: e.volume,
|
|
14494
|
+
when: e.when,
|
|
14495
|
+
duration: e.duration
|
|
14496
|
+
});
|
|
14497
|
+
};
|
|
14498
|
+
const playDrumEvent = (e) => {
|
|
14499
|
+
playDrum(e);
|
|
14500
|
+
};
|
|
14168
14501
|
const playNote3 = async (options2) => {
|
|
14169
14502
|
await listReady;
|
|
14170
14503
|
const key = options2.instrument ? resolveNameToKey(options2.instrument) : void 0;
|
|
@@ -14270,6 +14603,9 @@ var createDtmStudio = async (options = {}) => {
|
|
|
14270
14603
|
mountPlayer,
|
|
14271
14604
|
mountChordPlayer: mountChordPlayerInstance,
|
|
14272
14605
|
play,
|
|
14606
|
+
playSingingMML: playSingingMMLInstance,
|
|
14607
|
+
playNoteEvent,
|
|
14608
|
+
playDrumEvent,
|
|
14273
14609
|
playNote: playNote3,
|
|
14274
14610
|
playChords: playChords3,
|
|
14275
14611
|
loadPreset,
|
|
@@ -14369,6 +14705,7 @@ export {
|
|
|
14369
14705
|
playNote,
|
|
14370
14706
|
playPlacements,
|
|
14371
14707
|
playSingingMML,
|
|
14708
|
+
resolveLoopPoint,
|
|
14372
14709
|
setBackgroundActive,
|
|
14373
14710
|
setDrawOffset,
|
|
14374
14711
|
shiftNotes,
|
package/package.json
CHANGED