@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.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
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
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
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -4795,6 +4815,58 @@ var DAW_CSS = `
|
|
|
4795
4815
|
margin-left: 8px;
|
|
4796
4816
|
font-weight: bold;
|
|
4797
4817
|
}
|
|
4818
|
+
.dtm-player-seek-row {
|
|
4819
|
+
display: flex;
|
|
4820
|
+
align-items: center;
|
|
4821
|
+
gap: 8px;
|
|
4822
|
+
}
|
|
4823
|
+
.dtm-player-seek {
|
|
4824
|
+
flex: 1 1 auto;
|
|
4825
|
+
min-width: 0;
|
|
4826
|
+
height: 12px;
|
|
4827
|
+
margin: 0;
|
|
4828
|
+
background: transparent;
|
|
4829
|
+
cursor: pointer;
|
|
4830
|
+
-webkit-appearance: none;
|
|
4831
|
+
appearance: none;
|
|
4832
|
+
}
|
|
4833
|
+
.dtm-player-seek:disabled { cursor: default; opacity: 0.4; }
|
|
4834
|
+
.dtm-player-seek::-webkit-slider-runnable-track {
|
|
4835
|
+
height: 6px;
|
|
4836
|
+
background: var(--dtm-border2);
|
|
4837
|
+
border: 2px solid var(--c-black);
|
|
4838
|
+
}
|
|
4839
|
+
.dtm-player-seek::-moz-range-track {
|
|
4840
|
+
height: 6px;
|
|
4841
|
+
background: var(--dtm-border2);
|
|
4842
|
+
border: 2px solid var(--c-black);
|
|
4843
|
+
}
|
|
4844
|
+
.dtm-player-seek::-webkit-slider-thumb {
|
|
4845
|
+
-webkit-appearance: none;
|
|
4846
|
+
width: 12px;
|
|
4847
|
+
height: 16px;
|
|
4848
|
+
margin-top: -7px;
|
|
4849
|
+
background: var(--dtm-primary);
|
|
4850
|
+
border: 2px solid var(--c-black);
|
|
4851
|
+
box-shadow: 2px 2px 0 var(--c-black);
|
|
4852
|
+
cursor: pointer;
|
|
4853
|
+
}
|
|
4854
|
+
.dtm-player-seek::-moz-range-thumb {
|
|
4855
|
+
width: 12px;
|
|
4856
|
+
height: 16px;
|
|
4857
|
+
background: var(--dtm-primary);
|
|
4858
|
+
border: 2px solid var(--c-black);
|
|
4859
|
+
box-shadow: 2px 2px 0 var(--c-black);
|
|
4860
|
+
cursor: pointer;
|
|
4861
|
+
border-radius: 0;
|
|
4862
|
+
}
|
|
4863
|
+
.dtm-player-time {
|
|
4864
|
+
flex: 0 0 auto;
|
|
4865
|
+
font-family: 'k8x12', monospace;
|
|
4866
|
+
font-size: 11px;
|
|
4867
|
+
color: var(--dtm-text);
|
|
4868
|
+
white-space: nowrap;
|
|
4869
|
+
}
|
|
4798
4870
|
.dtm-player-dots {
|
|
4799
4871
|
margin-left: auto;
|
|
4800
4872
|
display: flex;
|
|
@@ -6071,6 +6143,31 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6071
6143
|
beatRow.appendChild(chordEl);
|
|
6072
6144
|
head.append(playBtn, beatRow, dots, mmlHeader);
|
|
6073
6145
|
root.appendChild(head);
|
|
6146
|
+
const seekRow = doc.createElement("div");
|
|
6147
|
+
seekRow.className = "dtm-player-seek-row";
|
|
6148
|
+
const seekInput = doc.createElement("input");
|
|
6149
|
+
seekInput.type = "range";
|
|
6150
|
+
seekInput.className = "dtm-player-seek";
|
|
6151
|
+
seekInput.min = "0";
|
|
6152
|
+
seekInput.max = String(Math.max(1, maxStep));
|
|
6153
|
+
seekInput.step = "1";
|
|
6154
|
+
seekInput.value = "0";
|
|
6155
|
+
seekInput.disabled = trackIndices.length === 0;
|
|
6156
|
+
const timeEl = doc.createElement("span");
|
|
6157
|
+
timeEl.className = "dtm-player-time";
|
|
6158
|
+
timeEl.textContent = "0:00 / 0:00";
|
|
6159
|
+
seekRow.append(seekInput, timeEl);
|
|
6160
|
+
root.appendChild(seekRow);
|
|
6161
|
+
const formatTime = (step) => {
|
|
6162
|
+
const totalSec = Math.max(0, step * secondsPerStep);
|
|
6163
|
+
const m = Math.floor(totalSec / 60);
|
|
6164
|
+
const s = Math.floor(totalSec % 60);
|
|
6165
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
6166
|
+
};
|
|
6167
|
+
const updateTimeLabel = (step) => {
|
|
6168
|
+
timeEl.textContent = `${formatTime(step)} / ${formatTime(maxStep)}`;
|
|
6169
|
+
};
|
|
6170
|
+
updateTimeLabel(0);
|
|
6074
6171
|
const msgArea = doc.createElement("div");
|
|
6075
6172
|
msgArea.className = "dtm-player-message";
|
|
6076
6173
|
msgArea.style.display = "none";
|
|
@@ -6289,10 +6386,15 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6289
6386
|
};
|
|
6290
6387
|
const renderPlayhead = (step) => {
|
|
6291
6388
|
const intStep = Math.floor(step);
|
|
6389
|
+
playStep = intStep;
|
|
6292
6390
|
const beatIndex = Math.floor(step / STEPS_PER_BEAT2) % 4;
|
|
6293
6391
|
for (let i = 0; i < 4; i++)
|
|
6294
6392
|
beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
|
|
6295
6393
|
barEl.textContent = String(Math.floor(step / STEPS_PER_BAR2) + 1);
|
|
6394
|
+
if (!isSeeking) {
|
|
6395
|
+
seekInput.value = String(Math.min(maxStep, Math.max(0, intStep)));
|
|
6396
|
+
updateTimeLabel(intStep);
|
|
6397
|
+
}
|
|
6296
6398
|
const chordName = stepChords[intStep] ?? "";
|
|
6297
6399
|
if (chordEl.textContent !== chordName) {
|
|
6298
6400
|
chordEl.textContent = chordName;
|
|
@@ -6313,9 +6415,12 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6313
6415
|
}
|
|
6314
6416
|
};
|
|
6315
6417
|
const resetPlayhead = () => {
|
|
6418
|
+
playStep = 0;
|
|
6316
6419
|
for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
|
|
6317
6420
|
barEl.textContent = "-";
|
|
6318
6421
|
chordEl.textContent = "";
|
|
6422
|
+
seekInput.value = "0";
|
|
6423
|
+
updateTimeLabel(0);
|
|
6319
6424
|
for (const view of laneViews) {
|
|
6320
6425
|
for (const t of view.tokens) t.el.classList.remove("is-active");
|
|
6321
6426
|
view.lane.scrollLeft = 0;
|
|
@@ -6351,9 +6456,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6351
6456
|
});
|
|
6352
6457
|
let playing = false;
|
|
6353
6458
|
let skipSinging = false;
|
|
6459
|
+
let playStep = 0;
|
|
6354
6460
|
const setPlayingUI = (on) => {
|
|
6355
6461
|
playing = on;
|
|
6356
|
-
playBtn.innerHTML = icon(on ? "
|
|
6462
|
+
playBtn.innerHTML = icon(on ? "pause" : "play", 12);
|
|
6357
6463
|
playBtn.classList.toggle("dtm-player-play--stop", on);
|
|
6358
6464
|
};
|
|
6359
6465
|
const finish = () => {
|
|
@@ -6367,7 +6473,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6367
6473
|
masterVolume = volume;
|
|
6368
6474
|
peekVoices()?.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
6369
6475
|
};
|
|
6370
|
-
const buildStreamTracks = () => [...lyricTracks.entries()].map(([index, lt]) => {
|
|
6476
|
+
const buildStreamTracks = (fromStep) => [...lyricTracks.entries()].map(([index, lt]) => {
|
|
6371
6477
|
const seqTrack = seqTracks.find((t) => Number(t.id) === index);
|
|
6372
6478
|
const sorted = [...seqTrack?.notes ?? []].sort(
|
|
6373
6479
|
(a, b) => a.startStep - b.startStep
|
|
@@ -6378,10 +6484,11 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6378
6484
|
const notes = [];
|
|
6379
6485
|
for (let i = 0; i < count; i++) {
|
|
6380
6486
|
const n = sorted[i];
|
|
6487
|
+
if (n.startStep < fromStep) continue;
|
|
6381
6488
|
notes.push({
|
|
6382
6489
|
syllable: lt.syllables[i],
|
|
6383
6490
|
pitch: n.pitch + semis,
|
|
6384
|
-
startSec: n.startStep * secondsPerStep,
|
|
6491
|
+
startSec: (n.startStep - fromStep) * secondsPerStep,
|
|
6385
6492
|
durationSec: n.durationSteps * secondsPerStep * gate
|
|
6386
6493
|
});
|
|
6387
6494
|
}
|
|
@@ -6393,9 +6500,9 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6393
6500
|
notes
|
|
6394
6501
|
};
|
|
6395
6502
|
});
|
|
6396
|
-
const startWhenReady = async () => {
|
|
6503
|
+
const startWhenReady = async (fromStep) => {
|
|
6397
6504
|
const streaming = voicesAvailable && lyricTracks.size > 0;
|
|
6398
|
-
const tracks = streaming ? buildStreamTracks() : [];
|
|
6505
|
+
const tracks = streaming ? buildStreamTracks(fromStep) : [];
|
|
6399
6506
|
if (streaming) {
|
|
6400
6507
|
const v = ensureVoices();
|
|
6401
6508
|
const overlay = showLoadingOverlay(body, {
|
|
@@ -6404,7 +6511,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6404
6511
|
if (!playing || activePlayer !== instance) return;
|
|
6405
6512
|
skipSinging = true;
|
|
6406
6513
|
overlay.remove();
|
|
6407
|
-
seq.start(
|
|
6514
|
+
seq.start(fromStep);
|
|
6408
6515
|
}
|
|
6409
6516
|
});
|
|
6410
6517
|
try {
|
|
@@ -6437,7 +6544,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6437
6544
|
}
|
|
6438
6545
|
if (!playing || activePlayer !== instance || skipSinging) return;
|
|
6439
6546
|
}
|
|
6440
|
-
seq.start(
|
|
6547
|
+
seq.start(fromStep);
|
|
6441
6548
|
if (streaming && !skipSinging) {
|
|
6442
6549
|
const v = ensureVoices();
|
|
6443
6550
|
v.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
@@ -6451,9 +6558,9 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6451
6558
|
});
|
|
6452
6559
|
}
|
|
6453
6560
|
};
|
|
6454
|
-
const play = () => {
|
|
6561
|
+
const play = (fromStep = 0) => {
|
|
6455
6562
|
if (playing || trackIndices.length === 0) return;
|
|
6456
|
-
if (checkConsentAndShow(() => play())) return;
|
|
6563
|
+
if (checkConsentAndShow(() => play(fromStep))) return;
|
|
6457
6564
|
if (activePlayer && activePlayer !== instance) activePlayer.stop();
|
|
6458
6565
|
activePlayer = instance;
|
|
6459
6566
|
skipSinging = false;
|
|
@@ -6469,7 +6576,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6469
6576
|
if (resumes.length > 0) await Promise.all(resumes);
|
|
6470
6577
|
if (!playing || activePlayer !== instance) return;
|
|
6471
6578
|
if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
|
|
6472
|
-
await startWhenReady();
|
|
6579
|
+
await startWhenReady(fromStep);
|
|
6473
6580
|
})();
|
|
6474
6581
|
};
|
|
6475
6582
|
const stop = () => {
|
|
@@ -6478,9 +6585,40 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6478
6585
|
peekVoices()?.stopStream();
|
|
6479
6586
|
finish();
|
|
6480
6587
|
};
|
|
6588
|
+
const pause = () => {
|
|
6589
|
+
if (!playing) return;
|
|
6590
|
+
seq.stop();
|
|
6591
|
+
peekVoices()?.stopStream();
|
|
6592
|
+
clearJumpTimers();
|
|
6593
|
+
setPlayingUI(false);
|
|
6594
|
+
if (activePlayer === instance) activePlayer = null;
|
|
6595
|
+
options.onStop?.();
|
|
6596
|
+
};
|
|
6597
|
+
let isSeeking = false;
|
|
6598
|
+
const seekTo = (step) => {
|
|
6599
|
+
const clamped = Math.min(maxStep, Math.max(0, Math.round(step)));
|
|
6600
|
+
renderPlayhead(clamped);
|
|
6601
|
+
if (playing) {
|
|
6602
|
+
seq.stop();
|
|
6603
|
+
peekVoices()?.stopStream();
|
|
6604
|
+
clearJumpTimers();
|
|
6605
|
+
setPlayingUI(false);
|
|
6606
|
+
play(clamped);
|
|
6607
|
+
}
|
|
6608
|
+
};
|
|
6609
|
+
seekInput.addEventListener("pointerdown", () => {
|
|
6610
|
+
isSeeking = true;
|
|
6611
|
+
});
|
|
6612
|
+
seekInput.addEventListener("input", () => {
|
|
6613
|
+
renderPlayhead(Number(seekInput.value));
|
|
6614
|
+
});
|
|
6615
|
+
seekInput.addEventListener("change", () => {
|
|
6616
|
+
isSeeking = false;
|
|
6617
|
+
seekTo(Number(seekInput.value));
|
|
6618
|
+
});
|
|
6481
6619
|
playBtn.addEventListener("click", () => {
|
|
6482
|
-
if (playing)
|
|
6483
|
-
else play();
|
|
6620
|
+
if (playing) pause();
|
|
6621
|
+
else play(Number(seekInput.value));
|
|
6484
6622
|
});
|
|
6485
6623
|
const destroy = () => {
|
|
6486
6624
|
doc.removeEventListener("click", handleOutsideClick);
|
|
@@ -13010,12 +13148,234 @@ var mountDAW = (target, options = {}) => {
|
|
|
13010
13148
|
};
|
|
13011
13149
|
|
|
13012
13150
|
// src/headless-singing-player.ts
|
|
13013
|
-
var
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
13151
|
+
var STEPS_PER_BEAT4 = 48;
|
|
13152
|
+
var STEPS_PER_BAR3 = 192;
|
|
13153
|
+
var playSingingMML = async (mml, options = {}) => {
|
|
13154
|
+
const {
|
|
13155
|
+
placements,
|
|
13156
|
+
bpm: parsedBpm,
|
|
13157
|
+
meta,
|
|
13158
|
+
lyrics
|
|
13159
|
+
} = parseMML(mml, {
|
|
13160
|
+
collectLyrics: true
|
|
13161
|
+
});
|
|
13162
|
+
const lyricTracks = lyrics ?? /* @__PURE__ */ new Map();
|
|
13163
|
+
const customVocalByKey = new Map(
|
|
13164
|
+
parseCustomVocals(mml).map((d) => [d.key, d])
|
|
13165
|
+
);
|
|
13166
|
+
const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
|
|
13167
|
+
const secondsPerStep = 60 / bpm / STEPS_PER_BEAT4;
|
|
13168
|
+
const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
|
|
13169
|
+
const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
|
|
13170
|
+
const drumVolume = meta.drumVolume ?? 80;
|
|
13171
|
+
const trackVolume = meta.volume ?? 100;
|
|
13172
|
+
let masterVolume = options.volume ?? 100;
|
|
13173
|
+
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
13174
|
+
(a, b) => a - b
|
|
13018
13175
|
);
|
|
13176
|
+
const seqTracks = trackIndices.map((index) => {
|
|
13177
|
+
let id = 0;
|
|
13178
|
+
const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
|
|
13179
|
+
id: id++,
|
|
13180
|
+
startStep: p.startStep,
|
|
13181
|
+
durationSteps: p.durationSteps,
|
|
13182
|
+
pitch: p.pitch,
|
|
13183
|
+
velocity: p.velocity
|
|
13184
|
+
}));
|
|
13185
|
+
return {
|
|
13186
|
+
id: String(index),
|
|
13187
|
+
volume: trackVolume / 100 * masterVolume,
|
|
13188
|
+
notes
|
|
13189
|
+
};
|
|
13190
|
+
});
|
|
13191
|
+
const ownsCtx = !options.audioContext;
|
|
13192
|
+
const ctx = options.audioContext ?? new AudioContext();
|
|
13193
|
+
const destination = options.destination ?? ctx.destination;
|
|
13194
|
+
const useSynth = options.synth ?? !options.onPlayNote;
|
|
13195
|
+
const synth = useSynth ? createSynth(ctx, destination) : null;
|
|
13196
|
+
const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
|
|
13197
|
+
let playing = false;
|
|
13198
|
+
let destroyed = false;
|
|
13199
|
+
let voices = options.singingVoices ?? null;
|
|
13200
|
+
const buildStreamTracks = (fromStep) => [...lyricTracks.entries()].map(([index, lt]) => {
|
|
13201
|
+
const seqTrack = seqTracks.find((t) => Number(t.id) === index);
|
|
13202
|
+
const sorted = [...seqTrack?.notes ?? []].sort(
|
|
13203
|
+
(a, b) => a.startStep - b.startStep
|
|
13204
|
+
);
|
|
13205
|
+
const gate = (lt.gate ?? DEFAULT_GATE) / 100;
|
|
13206
|
+
const semis = (lt.octave ?? 0) * 12;
|
|
13207
|
+
const count = Math.min(sorted.length, lt.syllables.length);
|
|
13208
|
+
const notes = [];
|
|
13209
|
+
for (let i = 0; i < count; i++) {
|
|
13210
|
+
const n = sorted[i];
|
|
13211
|
+
if (n.startStep < fromStep) continue;
|
|
13212
|
+
notes.push({
|
|
13213
|
+
syllable: lt.syllables[i],
|
|
13214
|
+
pitch: n.pitch + semis,
|
|
13215
|
+
startSec: (n.startStep - fromStep) * secondsPerStep,
|
|
13216
|
+
durationSec: n.durationSteps * secondsPerStep * gate
|
|
13217
|
+
});
|
|
13218
|
+
}
|
|
13219
|
+
return {
|
|
13220
|
+
id: String(index),
|
|
13221
|
+
model: lt.model,
|
|
13222
|
+
volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME),
|
|
13223
|
+
pan: panToStereo(lt.pan ?? DEFAULT_PAN),
|
|
13224
|
+
notes
|
|
13225
|
+
};
|
|
13226
|
+
});
|
|
13227
|
+
const seq = createSequencer({
|
|
13228
|
+
getTracks: () => seqTracks,
|
|
13229
|
+
getBpm: () => bpm,
|
|
13230
|
+
getPlayStartStep: () => 0,
|
|
13231
|
+
getDrumPattern: () => drumPattern,
|
|
13232
|
+
getSoloTrackId: () => null,
|
|
13233
|
+
getLoop: () => options.loop ?? false,
|
|
13234
|
+
cues: options.cues,
|
|
13235
|
+
onCue: options.onCue,
|
|
13236
|
+
getAudioTime: () => ctx.currentTime,
|
|
13237
|
+
onPlayNote: (e) => {
|
|
13238
|
+
const trackIdx = Number(e.trackId);
|
|
13239
|
+
if (lyricTracks.has(trackIdx)) return;
|
|
13240
|
+
options.onPlayNote?.(e);
|
|
13241
|
+
synth?.playNote(e);
|
|
13242
|
+
},
|
|
13243
|
+
onPlayDrum: (e) => {
|
|
13244
|
+
const velocity = e.velocity * (drumVolume / 100) * (trackVolume / 100) * (masterVolume / 100);
|
|
13245
|
+
options.onPlayDrum?.({ ...e, velocity });
|
|
13246
|
+
synth?.playDrum({ ...e, velocity });
|
|
13247
|
+
},
|
|
13248
|
+
onTick: (step) => {
|
|
13249
|
+
options.onTick?.(step);
|
|
13250
|
+
},
|
|
13251
|
+
onEnd: (_interrupted) => finish(),
|
|
13252
|
+
stepsPerBar: STEPS_PER_BAR3
|
|
13253
|
+
});
|
|
13254
|
+
const finish = () => {
|
|
13255
|
+
if (!playing) return;
|
|
13256
|
+
playing = false;
|
|
13257
|
+
voices?.stopStream();
|
|
13258
|
+
options.onStop?.();
|
|
13259
|
+
};
|
|
13260
|
+
const onVisibilityChange = () => {
|
|
13261
|
+
if (!playing) return;
|
|
13262
|
+
if (document.hidden) {
|
|
13263
|
+
void ctx.suspend();
|
|
13264
|
+
} else if (ctx.state === "suspended") {
|
|
13265
|
+
void ctx.resume();
|
|
13266
|
+
}
|
|
13267
|
+
};
|
|
13268
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
13269
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
13270
|
+
}
|
|
13271
|
+
const stop = () => {
|
|
13272
|
+
if (!playing) return;
|
|
13273
|
+
seq.stop();
|
|
13274
|
+
finish();
|
|
13275
|
+
};
|
|
13276
|
+
const setVolume = (volume) => {
|
|
13277
|
+
masterVolume = volume;
|
|
13278
|
+
const effectiveTrackVolume = trackVolume / 100 * masterVolume;
|
|
13279
|
+
for (const t of seqTracks) t.volume = effectiveTrackVolume;
|
|
13280
|
+
voices?.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
13281
|
+
};
|
|
13282
|
+
const suspend = () => ctx.suspend();
|
|
13283
|
+
const resume = () => ctx.resume();
|
|
13284
|
+
const destroy = () => {
|
|
13285
|
+
seq.stop();
|
|
13286
|
+
playing = false;
|
|
13287
|
+
destroyed = true;
|
|
13288
|
+
voices?.reset();
|
|
13289
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
13290
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
13291
|
+
}
|
|
13292
|
+
if (ownsCtx && ctx.state !== "closed") {
|
|
13293
|
+
void ctx.close();
|
|
13294
|
+
}
|
|
13295
|
+
};
|
|
13296
|
+
const playback = {
|
|
13297
|
+
stop,
|
|
13298
|
+
isPlaying: () => playing,
|
|
13299
|
+
setVolume,
|
|
13300
|
+
suspend,
|
|
13301
|
+
resume,
|
|
13302
|
+
destroy
|
|
13303
|
+
};
|
|
13304
|
+
playing = true;
|
|
13305
|
+
try {
|
|
13306
|
+
const resumes = [];
|
|
13307
|
+
const r = options.onResumeAudio?.();
|
|
13308
|
+
if (r) resumes.push(Promise.resolve(r));
|
|
13309
|
+
if (ctx.state === "suspended") resumes.push(ctx.resume());
|
|
13310
|
+
if (resumes.length > 0) await Promise.all(resumes);
|
|
13311
|
+
if (!playing || destroyed) {
|
|
13312
|
+
return playback;
|
|
13313
|
+
}
|
|
13314
|
+
if (lyricTracks.size > 0) {
|
|
13315
|
+
if (!voices) {
|
|
13316
|
+
voices = createSingingVoices(ctx, destination, {
|
|
13317
|
+
voiceWorkerUrl: options.voiceWorkerUrl
|
|
13318
|
+
});
|
|
13319
|
+
}
|
|
13320
|
+
if (customVocalByKey.size > 0 && voices.registerVoicebanks) {
|
|
13321
|
+
voices.registerVoicebanks(
|
|
13322
|
+
Object.fromEntries([...customVocalByKey].map(([k, d]) => [k, d.url]))
|
|
13323
|
+
);
|
|
13324
|
+
}
|
|
13325
|
+
const streamTracks = buildStreamTracks(0);
|
|
13326
|
+
await voices.loadModels(streamTracks.map((t) => t.model));
|
|
13327
|
+
if (!playing || destroyed) {
|
|
13328
|
+
return playback;
|
|
13329
|
+
}
|
|
13330
|
+
await voices.warm(streamTracks, PREWARM_NOTES);
|
|
13331
|
+
if (!playing || destroyed) {
|
|
13332
|
+
return playback;
|
|
13333
|
+
}
|
|
13334
|
+
let loopLengthSec;
|
|
13335
|
+
let loopStartSec;
|
|
13336
|
+
const loopOption = options.loop ?? false;
|
|
13337
|
+
if (loopOption) {
|
|
13338
|
+
let loopStartStep = 0;
|
|
13339
|
+
let loopEndStep = -1;
|
|
13340
|
+
if (typeof loopOption === "object") {
|
|
13341
|
+
loopStartStep = loopOption.start ? resolveLoopPoint(
|
|
13342
|
+
loopOption.start,
|
|
13343
|
+
bpm,
|
|
13344
|
+
STEPS_PER_BAR3,
|
|
13345
|
+
secondsPerStep
|
|
13346
|
+
) : 0;
|
|
13347
|
+
const endVal = loopOption.end ? resolveLoopPoint(
|
|
13348
|
+
loopOption.end,
|
|
13349
|
+
bpm,
|
|
13350
|
+
STEPS_PER_BAR3,
|
|
13351
|
+
secondsPerStep
|
|
13352
|
+
) : null;
|
|
13353
|
+
loopEndStep = endVal !== null ? endVal : -1;
|
|
13354
|
+
}
|
|
13355
|
+
if (loopEndStep === -1) {
|
|
13356
|
+
let maxEndStep = 0;
|
|
13357
|
+
for (const p of placements) {
|
|
13358
|
+
maxEndStep = Math.max(maxEndStep, p.startStep + p.durationSteps);
|
|
13359
|
+
}
|
|
13360
|
+
loopEndStep = maxEndStep;
|
|
13361
|
+
}
|
|
13362
|
+
loopStartSec = loopStartStep * secondsPerStep;
|
|
13363
|
+
loopLengthSec = (loopEndStep - loopStartStep) * secondsPerStep;
|
|
13364
|
+
}
|
|
13365
|
+
seq.start(0);
|
|
13366
|
+
voices.setVolume(trackVolume / 100 * (masterVolume / 100));
|
|
13367
|
+
voices.startStream(streamTracks, seq.getStartTime(), {
|
|
13368
|
+
loopLengthSec,
|
|
13369
|
+
loopStartSec
|
|
13370
|
+
});
|
|
13371
|
+
} else {
|
|
13372
|
+
seq.start(0);
|
|
13373
|
+
}
|
|
13374
|
+
} catch (err2) {
|
|
13375
|
+
stop();
|
|
13376
|
+
throw err2;
|
|
13377
|
+
}
|
|
13378
|
+
return playback;
|
|
13019
13379
|
};
|
|
13020
13380
|
|
|
13021
13381
|
// src/piano-roll.ts
|
|
@@ -14375,6 +14735,7 @@ var createDtmStudio = async (options = {}) => {
|
|
|
14375
14735
|
playNote,
|
|
14376
14736
|
playPlacements,
|
|
14377
14737
|
playSingingMML,
|
|
14738
|
+
resolveLoopPoint,
|
|
14378
14739
|
setBackgroundActive,
|
|
14379
14740
|
setDrawOffset,
|
|
14380
14741
|
shiftNotes,
|