@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/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
- for (const { note, prevVowel } of items) {
2780
- if (session !== streamSession) return;
2781
- while (note.startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
2782
- await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
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
- if (opts?.isAudible && !opts.isAudible(track)) continue;
2786
- const t0 = anchorTime + note.startSec;
2787
- if (model.renderToCache && model.scheduleCached) {
2788
- const renderToCache = model.renderToCache;
2789
- const scheduleCached = model.scheduleCached;
2790
- void (async () => {
2791
- const key = await renderToCache(
2792
- note.syllable,
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
- if (key) {
2799
- const delay = ctx.currentTime - t0;
2800
- if (delay < 0.05) {
2801
- scheduleCached(key, t0, peak, track.pan);
2802
- opts?.onScheduled?.(track, note, t0);
2803
- } else {
2804
- console.warn(
2805
- `[dtm] Synthesizer late skip: ${note.syllable.kana} at ${note.startSec}s (delayed by ${delay.toFixed(3)}s)`
2806
- );
2807
- opts?.onLateSkip?.(note, delay);
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
- const when = t0 - ctx.currentTime;
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(destination);
3459
+ panner.connect(compressor);
3433
3460
  } else {
3434
- gain.connect(destination);
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(destination);
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(destination);
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 { id: String(index), volume: masterVolume, notes };
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 playSingingMML = (_mml, _options = {}) => {
13132
- return Promise.reject(
13133
- new Error(
13134
- "playSingingMML is not implemented yet. See implementation notes at the top of headless-singing-player.ts."
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,