fix(fxrt): giu session realtime khi loop/seek/reschedule - het bypass+reload moi pass loop

- fxRtKeepForRestart flag: nhánh restart/resume (loop selection/project-end,
  sub-tab loop, reschedule items, seek, solo toggle, piano-roll seek, export
  rewind, unmute, playhead, tab-switch resume, ghost play) set flag truoc
  stopAllPlayback -> giu session FX (fxRtStart early-return) -> khong reload
  VST ~4s moi pass loop.
- User stop thật (fade=true), bridge-down, new project, recovery rebuild,
  export end, offline export: khong set flag -> teardown nhu cu.
- Rebuild app.precompiled.js tu source (babel) + giu lai IIFE overlay telemetry.
- Bump cache-buster 202608242200 -> 202608242300 (5 ban).
This commit is contained in:
2026-08-24 15:54:13 +07:00
parent 348079ceae
commit 9137e7e276
3 changed files with 108 additions and 39 deletions
+36 -4
View File
@@ -1648,6 +1648,12 @@ function encodeFloatWavBlob(pcm, sampleRate, channels) {
// tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường
// chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out.
const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, latencyTotal: 0, fadedIn: false };
// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không
// teardown) → fxRtStart early-return → KHÔNG reload VST ~4s mỗi pass loop
// (hết bypass rồi mới load lại). Chỉ set TRƯỚC nhánh restart/resume; user
// stop thật (fade=true) / bridge-down / new-project / export-end KHÔNG set →
// teardown bình thường.
let fxRtKeepForRestart = false;
// Pace input realtime: worklet sản xuất đúng realtime, nhưng sau khi main
// thread bị block (solo restart, GUI decode, rAF) các frame 'in' dồn lại —
// gửi 1 loạt làm SHM ring 4-slot tràn → mất block → ngắt quãng. Gửi tối đa 1
@@ -17649,6 +17655,7 @@ const App = () => {
tNode.gainNode.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.04);
}
setTimeout(() => {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
if (tNode && tNode.gainNode) {
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === trackId) : null;
@@ -18235,6 +18242,7 @@ const App = () => {
// already uses for solo.
if (becameAudible && isPlaying && recordingStateRef.current !== 'RECORDING' && activeTracksRef.current && activeTracksRef.current.length) {
try {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
const ctx = getAudioContext();
const resumeAt = currentTimeRef.current || 0;
@@ -18543,6 +18551,7 @@ const App = () => {
localSelectionAnchorRef.current = time;
if (isPlaying) {
setCurrentTime(time);
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
setTimeout(() => {
startOffsetTimeRef.current = time;
@@ -18967,6 +18976,7 @@ const App = () => {
}));
}
const _pt = Math.max(0, startOffsetTimeRef.current + (_ctx.currentTime - startAudioTimeRef.current) - _secStart);
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = _pt;
startAudioTimeRef.current = _ctx.currentTime;
@@ -18987,6 +18997,7 @@ const App = () => {
const _mainPos = startOffsetTimeRef.current + (_ctx.currentTime - startAudioTimeRef.current);
const _itemInfo = getPianoRollItemInfo(_prTabNew);
const _prPos = _itemInfo ? Math.max(0, _mainPos - (_itemInfo.startTime || 0)) : (_prTabNew.currentTime || 0);
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = _prPos;
startAudioTimeRef.current = _ctx.currentTime;
@@ -19040,6 +19051,7 @@ const App = () => {
if (_resumeAt !== null) {
try {
console.log('[Resume] play tại', _resumeAt.toFixed(2), 's (item.startTime=' + (_itemInfo ? _itemInfo.startTime.toFixed(2) : '?') + ' + st.currentTime=' + (prevSub.currentTime || 0).toFixed(2) + ')');
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = _resumeAt;
startAudioTimeRef.current = getAudioContext().currentTime;
@@ -23266,6 +23278,7 @@ const App = () => {
lastRescheduleTimeRef.current = nowT;
scheduledItemsSigRef.current = sig;
const pt = currentTimeRef.current;
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
setIsPlaying(true);
// Re-schedule theo ĐÚNG chế độ play hiện tại: loop local / solo chỉ
@@ -23511,6 +23524,7 @@ const App = () => {
const start = Math.min(st.selectionStart, st.selectionEnd);
const end = Math.max(st.selectionStart, st.selectionEnd);
if (bufferPos >= end) {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
...s,
@@ -23537,6 +23551,7 @@ const App = () => {
return Math.max(maxEnd * beatSec, 16 * beatSec * 4) + 1.0;
})() : st.buffer.duration;
if (bufferPos >= effectiveDuration) {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
if (st.isLooping) {
setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
@@ -23576,6 +23591,7 @@ const App = () => {
if (!selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null) {
if (selRight > selLeft && updatedTime >= selRight) {
if (hasAnySolo || selectionMode === 'local') {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = selLeft;
startAudioTimeRef.current = context.currentTime;
@@ -23588,6 +23604,7 @@ const App = () => {
setCurrentTime(selLeft);
setIsPlaying(true);
} else {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = selLeft;
startAudioTimeRef.current = context.currentTime;
@@ -23609,6 +23626,7 @@ const App = () => {
return;
}
if (isLoopingSelection) {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = 0;
startAudioTimeRef.current = context.currentTime;
@@ -24998,6 +25016,7 @@ const App = () => {
} : s));
} else {
try {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
const startOffset = st.currentTime || 0;
if (st.type === 'PIANO_ROLL') {
@@ -25049,6 +25068,7 @@ const App = () => {
synthEngine: ltrk ? ltrk.synth_engine : undefined
});
});
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
const prCtx = getAudioContext();
const prTNode = activeTrackNodesRef.current[prSt.trackId];
@@ -25083,10 +25103,17 @@ const App = () => {
isPlayingRef.current = false;
// Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng).
try { if (masterBus && masterBus.output && masterBus.output.gain) { const _actx = getAudioContext(); masterBus.output.gain.cancelScheduledValues(_actx.currentTime); masterBus.output.gain.setValueAtTime(1, _actx.currentTime); } } catch (e) { }
// Realtime VST FX: dừng session + khôi phục output→analyser.
try { fxRtStop(); } catch (e) { console.warn('[FX RT] stop error:', e); }
// Realtime VST FX per-track: dừng mọi session track + restore chain trực tiếp.
try { fxRtTrackStopAll(); } catch (e) { console.warn('[FX RT track] stop error:', e); }
// Realtime VST FX: dừng session + khôi phục output→analyser. Nhánh restart
// (loop/seek/reschedule/solo...) set fxRtKeepForRestart=true → GIỮ session
// (fxRtStart early-return khi active + chain khớp) → hết bypass+reload ~4s
// mỗi pass loop. User stop thật (fade=true) → teardown bình thường.
const _keepFx = fxRtKeepForRestart;
fxRtKeepForRestart = false;
if (!_keepFx) {
try { fxRtStop(); } catch (e) { console.warn('[FX RT] stop error:', e); }
// Realtime VST FX per-track: dừng mọi session track + restore chain trực tiếp.
try { fxRtTrackStopAll(); } catch (e) { console.warn('[FX RT track] stop error:', e); }
}
_nativePlayEpoch++; // hủy mọi render/decode pending của lần play cũ
// V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer
// cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref).
@@ -25210,6 +25237,7 @@ const App = () => {
const st = subTabs.find(s => s.id === activeTab);
if (!st) return;
if (st.isPlaying) {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
window.SonicSF.stopAll();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
@@ -25234,6 +25262,7 @@ const App = () => {
}
} else {
if (isPlaying) {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
setCurrentTime(time);
startOffsetTimeRef.current = time;
@@ -26919,6 +26948,7 @@ const App = () => {
const toggleTrackSoloEvaluate = trackId => {
const wasPlaying = isPlaying;
const playingSubTab = subTabs.find(s => s.isPlaying && s.type === 'PIANO_ROLL');
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
updateActiveTracks(prev => prev.map(t =>
t.id === trackId ? { ...t, solo: !t.solo, muted: false } : t
@@ -28086,6 +28116,7 @@ const App = () => {
// Rewind + play the FULL project from the start (FluidSynth + FX racks +
// mastering chain — exactly what you hear).
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
try { if (window.SonicSF && window.SonicSF.stopAllNotes) window.SonicSF.stopAllNotes(); } catch (e) { }
startTrackPlayback(0);
@@ -33193,6 +33224,7 @@ STRICT CONSTRAINTS:
const seekSt = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL');
if (!seekSt) return;
if (seekSt.isPlaying) {
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
window.SonicSF.stopAll();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608242200" defer></script>
<script src="/static/js/app.precompiled.js?v=202608242300" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {