@onjmin/dtm 0.1.66 → 0.1.68

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
  };
@@ -4673,6 +4692,58 @@ var DAW_CSS = `
4673
4692
  margin-left: 8px;
4674
4693
  font-weight: bold;
4675
4694
  }
4695
+ .dtm-player-seek-row {
4696
+ display: flex;
4697
+ align-items: center;
4698
+ gap: 8px;
4699
+ }
4700
+ .dtm-player-seek {
4701
+ flex: 1 1 auto;
4702
+ min-width: 0;
4703
+ height: 12px;
4704
+ margin: 0;
4705
+ background: transparent;
4706
+ cursor: pointer;
4707
+ -webkit-appearance: none;
4708
+ appearance: none;
4709
+ }
4710
+ .dtm-player-seek:disabled { cursor: default; opacity: 0.4; }
4711
+ .dtm-player-seek::-webkit-slider-runnable-track {
4712
+ height: 6px;
4713
+ background: var(--dtm-border2);
4714
+ border: 2px solid var(--c-black);
4715
+ }
4716
+ .dtm-player-seek::-moz-range-track {
4717
+ height: 6px;
4718
+ background: var(--dtm-border2);
4719
+ border: 2px solid var(--c-black);
4720
+ }
4721
+ .dtm-player-seek::-webkit-slider-thumb {
4722
+ -webkit-appearance: none;
4723
+ width: 12px;
4724
+ height: 16px;
4725
+ margin-top: -7px;
4726
+ background: var(--dtm-primary);
4727
+ border: 2px solid var(--c-black);
4728
+ box-shadow: 2px 2px 0 var(--c-black);
4729
+ cursor: pointer;
4730
+ }
4731
+ .dtm-player-seek::-moz-range-thumb {
4732
+ width: 12px;
4733
+ height: 16px;
4734
+ background: var(--dtm-primary);
4735
+ border: 2px solid var(--c-black);
4736
+ box-shadow: 2px 2px 0 var(--c-black);
4737
+ cursor: pointer;
4738
+ border-radius: 0;
4739
+ }
4740
+ .dtm-player-time {
4741
+ flex: 0 0 auto;
4742
+ font-family: 'k8x12', monospace;
4743
+ font-size: 11px;
4744
+ color: var(--dtm-text);
4745
+ white-space: nowrap;
4746
+ }
4676
4747
  .dtm-player-dots {
4677
4748
  margin-left: auto;
4678
4749
  display: flex;
@@ -5949,6 +6020,31 @@ var mountMmlPlayer = (target, mml, options = {}) => {
5949
6020
  beatRow.appendChild(chordEl);
5950
6021
  head.append(playBtn, beatRow, dots, mmlHeader);
5951
6022
  root.appendChild(head);
6023
+ const seekRow = doc.createElement("div");
6024
+ seekRow.className = "dtm-player-seek-row";
6025
+ const seekInput = doc.createElement("input");
6026
+ seekInput.type = "range";
6027
+ seekInput.className = "dtm-player-seek";
6028
+ seekInput.min = "0";
6029
+ seekInput.max = String(Math.max(1, maxStep));
6030
+ seekInput.step = "1";
6031
+ seekInput.value = "0";
6032
+ seekInput.disabled = trackIndices.length === 0;
6033
+ const timeEl = doc.createElement("span");
6034
+ timeEl.className = "dtm-player-time";
6035
+ timeEl.textContent = "0:00 / 0:00";
6036
+ seekRow.append(seekInput, timeEl);
6037
+ root.appendChild(seekRow);
6038
+ const formatTime = (step) => {
6039
+ const totalSec = Math.max(0, step * secondsPerStep);
6040
+ const m = Math.floor(totalSec / 60);
6041
+ const s = Math.floor(totalSec % 60);
6042
+ return `${m}:${String(s).padStart(2, "0")}`;
6043
+ };
6044
+ const updateTimeLabel = (step) => {
6045
+ timeEl.textContent = `${formatTime(step)} / ${formatTime(maxStep)}`;
6046
+ };
6047
+ updateTimeLabel(0);
5952
6048
  const msgArea = doc.createElement("div");
5953
6049
  msgArea.className = "dtm-player-message";
5954
6050
  msgArea.style.display = "none";
@@ -6167,10 +6263,15 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6167
6263
  };
6168
6264
  const renderPlayhead = (step) => {
6169
6265
  const intStep = Math.floor(step);
6266
+ playStep = intStep;
6170
6267
  const beatIndex = Math.floor(step / STEPS_PER_BEAT2) % 4;
6171
6268
  for (let i = 0; i < 4; i++)
6172
6269
  beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
6173
6270
  barEl.textContent = String(Math.floor(step / STEPS_PER_BAR2) + 1);
6271
+ if (!isSeeking) {
6272
+ seekInput.value = String(Math.min(maxStep, Math.max(0, intStep)));
6273
+ updateTimeLabel(intStep);
6274
+ }
6174
6275
  const chordName = stepChords[intStep] ?? "";
6175
6276
  if (chordEl.textContent !== chordName) {
6176
6277
  chordEl.textContent = chordName;
@@ -6191,9 +6292,12 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6191
6292
  }
6192
6293
  };
6193
6294
  const resetPlayhead = () => {
6295
+ playStep = 0;
6194
6296
  for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
6195
6297
  barEl.textContent = "-";
6196
6298
  chordEl.textContent = "";
6299
+ seekInput.value = "0";
6300
+ updateTimeLabel(0);
6197
6301
  for (const view of laneViews) {
6198
6302
  for (const t of view.tokens) t.el.classList.remove("is-active");
6199
6303
  view.lane.scrollLeft = 0;
@@ -6229,9 +6333,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6229
6333
  });
6230
6334
  let playing = false;
6231
6335
  let skipSinging = false;
6336
+ let playStep = 0;
6232
6337
  const setPlayingUI = (on) => {
6233
6338
  playing = on;
6234
- playBtn.innerHTML = icon(on ? "stop" : "play", 12);
6339
+ playBtn.innerHTML = icon(on ? "pause" : "play", 12);
6235
6340
  playBtn.classList.toggle("dtm-player-play--stop", on);
6236
6341
  };
6237
6342
  const finish = () => {
@@ -6245,7 +6350,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6245
6350
  masterVolume = volume;
6246
6351
  peekVoices()?.setVolume(trackVolume / 100 * (masterVolume / 100));
6247
6352
  };
6248
- const buildStreamTracks = () => [...lyricTracks.entries()].map(([index, lt]) => {
6353
+ const buildStreamTracks = (fromStep) => [...lyricTracks.entries()].map(([index, lt]) => {
6249
6354
  const seqTrack = seqTracks.find((t) => Number(t.id) === index);
6250
6355
  const sorted = [...seqTrack?.notes ?? []].sort(
6251
6356
  (a, b) => a.startStep - b.startStep
@@ -6256,10 +6361,11 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6256
6361
  const notes = [];
6257
6362
  for (let i = 0; i < count; i++) {
6258
6363
  const n = sorted[i];
6364
+ if (n.startStep < fromStep) continue;
6259
6365
  notes.push({
6260
6366
  syllable: lt.syllables[i],
6261
6367
  pitch: n.pitch + semis,
6262
- startSec: n.startStep * secondsPerStep,
6368
+ startSec: (n.startStep - fromStep) * secondsPerStep,
6263
6369
  durationSec: n.durationSteps * secondsPerStep * gate
6264
6370
  });
6265
6371
  }
@@ -6271,9 +6377,9 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6271
6377
  notes
6272
6378
  };
6273
6379
  });
6274
- const startWhenReady = async () => {
6380
+ const startWhenReady = async (fromStep) => {
6275
6381
  const streaming = voicesAvailable && lyricTracks.size > 0;
6276
- const tracks = streaming ? buildStreamTracks() : [];
6382
+ const tracks = streaming ? buildStreamTracks(fromStep) : [];
6277
6383
  if (streaming) {
6278
6384
  const v = ensureVoices();
6279
6385
  const overlay = showLoadingOverlay(body, {
@@ -6282,7 +6388,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6282
6388
  if (!playing || activePlayer !== instance) return;
6283
6389
  skipSinging = true;
6284
6390
  overlay.remove();
6285
- seq.start(0);
6391
+ seq.start(fromStep);
6286
6392
  }
6287
6393
  });
6288
6394
  try {
@@ -6315,7 +6421,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6315
6421
  }
6316
6422
  if (!playing || activePlayer !== instance || skipSinging) return;
6317
6423
  }
6318
- seq.start(0);
6424
+ seq.start(fromStep);
6319
6425
  if (streaming && !skipSinging) {
6320
6426
  const v = ensureVoices();
6321
6427
  v.setVolume(trackVolume / 100 * (masterVolume / 100));
@@ -6329,9 +6435,9 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6329
6435
  });
6330
6436
  }
6331
6437
  };
6332
- const play = () => {
6438
+ const play = (fromStep = 0) => {
6333
6439
  if (playing || trackIndices.length === 0) return;
6334
- if (checkConsentAndShow(() => play())) return;
6440
+ if (checkConsentAndShow(() => play(fromStep))) return;
6335
6441
  if (activePlayer && activePlayer !== instance) activePlayer.stop();
6336
6442
  activePlayer = instance;
6337
6443
  skipSinging = false;
@@ -6347,7 +6453,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6347
6453
  if (resumes.length > 0) await Promise.all(resumes);
6348
6454
  if (!playing || activePlayer !== instance) return;
6349
6455
  if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6350
- await startWhenReady();
6456
+ await startWhenReady(fromStep);
6351
6457
  })();
6352
6458
  };
6353
6459
  const stop = () => {
@@ -6356,9 +6462,40 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6356
6462
  peekVoices()?.stopStream();
6357
6463
  finish();
6358
6464
  };
6465
+ const pause = () => {
6466
+ if (!playing) return;
6467
+ seq.stop();
6468
+ peekVoices()?.stopStream();
6469
+ clearJumpTimers();
6470
+ setPlayingUI(false);
6471
+ if (activePlayer === instance) activePlayer = null;
6472
+ options.onStop?.();
6473
+ };
6474
+ let isSeeking = false;
6475
+ const seekTo = (step) => {
6476
+ const clamped = Math.min(maxStep, Math.max(0, Math.round(step)));
6477
+ renderPlayhead(clamped);
6478
+ if (playing) {
6479
+ seq.stop();
6480
+ peekVoices()?.stopStream();
6481
+ clearJumpTimers();
6482
+ setPlayingUI(false);
6483
+ play(clamped);
6484
+ }
6485
+ };
6486
+ seekInput.addEventListener("pointerdown", () => {
6487
+ isSeeking = true;
6488
+ });
6489
+ seekInput.addEventListener("input", () => {
6490
+ renderPlayhead(Number(seekInput.value));
6491
+ });
6492
+ seekInput.addEventListener("change", () => {
6493
+ isSeeking = false;
6494
+ seekTo(Number(seekInput.value));
6495
+ });
6359
6496
  playBtn.addEventListener("click", () => {
6360
- if (playing) stop();
6361
- else play();
6497
+ if (playing) pause();
6498
+ else play(Number(seekInput.value));
6362
6499
  });
6363
6500
  const destroy = () => {
6364
6501
  doc.removeEventListener("click", handleOutsideClick);
@@ -12888,12 +13025,234 @@ var mountDAW = (target, options = {}) => {
12888
13025
  };
12889
13026
 
12890
13027
  // src/headless-singing-player.ts
12891
- var playSingingMML = (_mml, _options = {}) => {
12892
- return Promise.reject(
12893
- new Error(
12894
- "playSingingMML is not implemented yet. See implementation notes at the top of headless-singing-player.ts."
12895
- )
13028
+ var STEPS_PER_BEAT4 = 48;
13029
+ var STEPS_PER_BAR3 = 192;
13030
+ var playSingingMML = async (mml, options = {}) => {
13031
+ const {
13032
+ placements,
13033
+ bpm: parsedBpm,
13034
+ meta,
13035
+ lyrics
13036
+ } = parseMML(mml, {
13037
+ collectLyrics: true
13038
+ });
13039
+ const lyricTracks = lyrics ?? /* @__PURE__ */ new Map();
13040
+ const customVocalByKey = new Map(
13041
+ parseCustomVocals(mml).map((d) => [d.key, d])
13042
+ );
13043
+ const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
13044
+ const secondsPerStep = 60 / bpm / STEPS_PER_BEAT4;
13045
+ const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
13046
+ const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
13047
+ const drumVolume = meta.drumVolume ?? 80;
13048
+ const trackVolume = meta.volume ?? 100;
13049
+ let masterVolume = options.volume ?? 100;
13050
+ const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
13051
+ (a, b) => a - b
12896
13052
  );
13053
+ const seqTracks = trackIndices.map((index) => {
13054
+ let id = 0;
13055
+ const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
13056
+ id: id++,
13057
+ startStep: p.startStep,
13058
+ durationSteps: p.durationSteps,
13059
+ pitch: p.pitch,
13060
+ velocity: p.velocity
13061
+ }));
13062
+ return {
13063
+ id: String(index),
13064
+ volume: trackVolume / 100 * masterVolume,
13065
+ notes
13066
+ };
13067
+ });
13068
+ const ownsCtx = !options.audioContext;
13069
+ const ctx = options.audioContext ?? new AudioContext();
13070
+ const destination = options.destination ?? ctx.destination;
13071
+ const useSynth = options.synth ?? !options.onPlayNote;
13072
+ const synth = useSynth ? createSynth(ctx, destination) : null;
13073
+ const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
13074
+ let playing = false;
13075
+ let destroyed = false;
13076
+ let voices = options.singingVoices ?? null;
13077
+ const buildStreamTracks = (fromStep) => [...lyricTracks.entries()].map(([index, lt]) => {
13078
+ const seqTrack = seqTracks.find((t) => Number(t.id) === index);
13079
+ const sorted = [...seqTrack?.notes ?? []].sort(
13080
+ (a, b) => a.startStep - b.startStep
13081
+ );
13082
+ const gate = (lt.gate ?? DEFAULT_GATE) / 100;
13083
+ const semis = (lt.octave ?? 0) * 12;
13084
+ const count = Math.min(sorted.length, lt.syllables.length);
13085
+ const notes = [];
13086
+ for (let i = 0; i < count; i++) {
13087
+ const n = sorted[i];
13088
+ if (n.startStep < fromStep) continue;
13089
+ notes.push({
13090
+ syllable: lt.syllables[i],
13091
+ pitch: n.pitch + semis,
13092
+ startSec: (n.startStep - fromStep) * secondsPerStep,
13093
+ durationSec: n.durationSteps * secondsPerStep * gate
13094
+ });
13095
+ }
13096
+ return {
13097
+ id: String(index),
13098
+ model: lt.model,
13099
+ volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME),
13100
+ pan: panToStereo(lt.pan ?? DEFAULT_PAN),
13101
+ notes
13102
+ };
13103
+ });
13104
+ const seq = createSequencer({
13105
+ getTracks: () => seqTracks,
13106
+ getBpm: () => bpm,
13107
+ getPlayStartStep: () => 0,
13108
+ getDrumPattern: () => drumPattern,
13109
+ getSoloTrackId: () => null,
13110
+ getLoop: () => options.loop ?? false,
13111
+ cues: options.cues,
13112
+ onCue: options.onCue,
13113
+ getAudioTime: () => ctx.currentTime,
13114
+ onPlayNote: (e) => {
13115
+ const trackIdx = Number(e.trackId);
13116
+ if (lyricTracks.has(trackIdx)) return;
13117
+ options.onPlayNote?.(e);
13118
+ synth?.playNote(e);
13119
+ },
13120
+ onPlayDrum: (e) => {
13121
+ const velocity = e.velocity * (drumVolume / 100) * (trackVolume / 100) * (masterVolume / 100);
13122
+ options.onPlayDrum?.({ ...e, velocity });
13123
+ synth?.playDrum({ ...e, velocity });
13124
+ },
13125
+ onTick: (step) => {
13126
+ options.onTick?.(step);
13127
+ },
13128
+ onEnd: (_interrupted) => finish(),
13129
+ stepsPerBar: STEPS_PER_BAR3
13130
+ });
13131
+ const finish = () => {
13132
+ if (!playing) return;
13133
+ playing = false;
13134
+ voices?.stopStream();
13135
+ options.onStop?.();
13136
+ };
13137
+ const onVisibilityChange = () => {
13138
+ if (!playing) return;
13139
+ if (document.hidden) {
13140
+ void ctx.suspend();
13141
+ } else if (ctx.state === "suspended") {
13142
+ void ctx.resume();
13143
+ }
13144
+ };
13145
+ if (pauseWhenHidden && typeof document !== "undefined") {
13146
+ document.addEventListener("visibilitychange", onVisibilityChange);
13147
+ }
13148
+ const stop = () => {
13149
+ if (!playing) return;
13150
+ seq.stop();
13151
+ finish();
13152
+ };
13153
+ const setVolume = (volume) => {
13154
+ masterVolume = volume;
13155
+ const effectiveTrackVolume = trackVolume / 100 * masterVolume;
13156
+ for (const t of seqTracks) t.volume = effectiveTrackVolume;
13157
+ voices?.setVolume(trackVolume / 100 * (masterVolume / 100));
13158
+ };
13159
+ const suspend = () => ctx.suspend();
13160
+ const resume = () => ctx.resume();
13161
+ const destroy = () => {
13162
+ seq.stop();
13163
+ playing = false;
13164
+ destroyed = true;
13165
+ voices?.reset();
13166
+ if (pauseWhenHidden && typeof document !== "undefined") {
13167
+ document.removeEventListener("visibilitychange", onVisibilityChange);
13168
+ }
13169
+ if (ownsCtx && ctx.state !== "closed") {
13170
+ void ctx.close();
13171
+ }
13172
+ };
13173
+ const playback = {
13174
+ stop,
13175
+ isPlaying: () => playing,
13176
+ setVolume,
13177
+ suspend,
13178
+ resume,
13179
+ destroy
13180
+ };
13181
+ playing = true;
13182
+ try {
13183
+ const resumes = [];
13184
+ const r = options.onResumeAudio?.();
13185
+ if (r) resumes.push(Promise.resolve(r));
13186
+ if (ctx.state === "suspended") resumes.push(ctx.resume());
13187
+ if (resumes.length > 0) await Promise.all(resumes);
13188
+ if (!playing || destroyed) {
13189
+ return playback;
13190
+ }
13191
+ if (lyricTracks.size > 0) {
13192
+ if (!voices) {
13193
+ voices = createSingingVoices(ctx, destination, {
13194
+ voiceWorkerUrl: options.voiceWorkerUrl
13195
+ });
13196
+ }
13197
+ if (customVocalByKey.size > 0 && voices.registerVoicebanks) {
13198
+ voices.registerVoicebanks(
13199
+ Object.fromEntries([...customVocalByKey].map(([k, d]) => [k, d.url]))
13200
+ );
13201
+ }
13202
+ const streamTracks = buildStreamTracks(0);
13203
+ await voices.loadModels(streamTracks.map((t) => t.model));
13204
+ if (!playing || destroyed) {
13205
+ return playback;
13206
+ }
13207
+ await voices.warm(streamTracks, PREWARM_NOTES);
13208
+ if (!playing || destroyed) {
13209
+ return playback;
13210
+ }
13211
+ let loopLengthSec;
13212
+ let loopStartSec;
13213
+ const loopOption = options.loop ?? false;
13214
+ if (loopOption) {
13215
+ let loopStartStep = 0;
13216
+ let loopEndStep = -1;
13217
+ if (typeof loopOption === "object") {
13218
+ loopStartStep = loopOption.start ? resolveLoopPoint(
13219
+ loopOption.start,
13220
+ bpm,
13221
+ STEPS_PER_BAR3,
13222
+ secondsPerStep
13223
+ ) : 0;
13224
+ const endVal = loopOption.end ? resolveLoopPoint(
13225
+ loopOption.end,
13226
+ bpm,
13227
+ STEPS_PER_BAR3,
13228
+ secondsPerStep
13229
+ ) : null;
13230
+ loopEndStep = endVal !== null ? endVal : -1;
13231
+ }
13232
+ if (loopEndStep === -1) {
13233
+ let maxEndStep = 0;
13234
+ for (const p of placements) {
13235
+ maxEndStep = Math.max(maxEndStep, p.startStep + p.durationSteps);
13236
+ }
13237
+ loopEndStep = maxEndStep;
13238
+ }
13239
+ loopStartSec = loopStartStep * secondsPerStep;
13240
+ loopLengthSec = (loopEndStep - loopStartStep) * secondsPerStep;
13241
+ }
13242
+ seq.start(0);
13243
+ voices.setVolume(trackVolume / 100 * (masterVolume / 100));
13244
+ voices.startStream(streamTracks, seq.getStartTime(), {
13245
+ loopLengthSec,
13246
+ loopStartSec
13247
+ });
13248
+ } else {
13249
+ seq.start(0);
13250
+ }
13251
+ } catch (err2) {
13252
+ stop();
13253
+ throw err2;
13254
+ }
13255
+ return playback;
12897
13256
  };
12898
13257
 
12899
13258
  // src/piano-roll.ts
@@ -14251,6 +14610,7 @@ export {
14251
14610
  playNote,
14252
14611
  playPlacements,
14253
14612
  playSingingMML,
14613
+ resolveLoopPoint,
14254
14614
  setBackgroundActive,
14255
14615
  setDrawOffset,
14256
14616
  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.66",
5
+ "version": "0.1.68",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {