@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/README.md +40 -3
- package/dist/index.d.mts +14 -54
- package/dist/index.d.ts +14 -54
- package/dist/index.js +420 -59
- package/dist/index.mjs +419 -59
- 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
|
};
|
|
@@ -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 ? "
|
|
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(
|
|
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(
|
|
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)
|
|
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
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
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