@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.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
- for (const { note, prevVowel } of items) {
2658
- if (session !== streamSession) return;
2659
- while (note.startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
2660
- await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
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
- if (opts?.isAudible && !opts.isAudible(track)) continue;
2664
- const t0 = anchorTime + note.startSec;
2665
- if (model.renderToCache && model.scheduleCached) {
2666
- const renderToCache = model.renderToCache;
2667
- const scheduleCached = model.scheduleCached;
2668
- void (async () => {
2669
- const key = await renderToCache(
2670
- note.syllable,
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
- if (key) {
2677
- const delay = ctx.currentTime - t0;
2678
- if (delay < 0.05) {
2679
- scheduleCached(key, t0, peak, track.pan);
2680
- opts?.onScheduled?.(track, note, t0);
2681
- } else {
2682
- console.warn(
2683
- `[dtm] Synthesizer late skip: ${note.syllable.kana} at ${note.startSec}s (delayed by ${delay.toFixed(3)}s)`
2684
- );
2685
- opts?.onLateSkip?.(note, delay);
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
- const when = t0 - ctx.currentTime;
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(destination);
3336
+ panner.connect(compressor);
3311
3337
  } else {
3312
- gain.connect(destination);
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(destination);
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(destination);
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 { id: String(index), volume: masterVolume, notes };
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 playSingingMML = (_mml, _options = {}) => {
13010
- return Promise.reject(
13011
- new Error(
13012
- "playSingingMML is not implemented yet. See implementation notes at the top of headless-singing-player.ts."
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
@@ -2,7 +2,7 @@
2
2
  "author": "onjmin",
3
3
  "license": "MIT",
4
4
  "name": "@onjmin/dtm",
5
- "version": "0.1.67",
5
+ "version": "0.1.69",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {