V24: fix master-chain crackle, 1s lag, solo bypass

- worklet: FILL 48->24 (PDC 7936 dung), flush protocol khi resume sau interruption
  (clear queue + fade-in/crossfade chi voi block moi), drop block stale
- engine fx_realtime: _flush_input_locked NO-DROP (ring day -> giu pending, cap 2048),
  flush_all() dong bo in/out khi worklet resume
- app.jsx: WS flush_done + flushing guard, fallback timer 500ms
- plugins.py: xu ly {cmd:flush} -> flush_done
- index.html: bump v param
This commit is contained in:
2026-08-26 20:15:13 +07:00
parent 28a06134f9
commit f38e500326
6 changed files with 148 additions and 31 deletions
+61 -3
View File
@@ -1848,7 +1848,7 @@ async function fxRtStart() {
fxRt.sessionId = st.session_id;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608312300`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609011200`);
if (token !== fxRt.startToken) { fxRt.starting = false; return; }
fxRt.node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
const u = new URL(API_BASE_URL);
@@ -1861,6 +1861,7 @@ async function fxRtStart() {
ws.onopen = () => {
if (token !== fxRt.startToken) { try { ws.close(); } catch (e) {} fxRt.starting = false; return; }
fxRt.active = true;
fxRt.flushing = false;
fxRt.starting = false;
fxRt.inQueue = []; // bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu
try { fxRtPushAllParams(); } catch (e) {}
@@ -1876,11 +1877,18 @@ async function fxRtStart() {
// Chi delay dry path khi crossfade da xong (dry silent) — ap som
// trong luc dry con la path nghe duoc se tao gap ~80ms.
if (fxRt.fadedIn) fxRtApplyPdc();
} else if (d && d.cmd === 'flush_done') {
// V24: engine da flush ring xong — mo lai nhan output + bao worklet
// (worklet drop block cho ack nay; mat ack -> fallback timer nha).
fxRt.flushing = false;
if (fxRt.flushTimer) { clearTimeout(fxRt.flushTimer); fxRt.flushTimer = null; }
if (fxRt.node) fxRt.node.port.postMessage({ type: 'flush_done' });
}
} catch (e) {}
return;
}
if (!(ev.data instanceof ArrayBuffer) || !fxRt.node) return;
if (fxRt.flushing) return; // V24: output stale trong luc flush — drop
const f = new Float32Array(ev.data);
const half = f.length / 2;
const l = new Float32Array(half);
@@ -1940,6 +1948,22 @@ async function fxRtStart() {
}, 30);
return;
}
if (d.type === 'flush') {
// V24: worklet underrun-resume — yeu cau engine flush ring (xoa backlog
// stale) → het lag ~1s sau stall. Fallback timer phong ack mat (WS/
// engine loi) → tu nha sau 500ms, worklet nhan block moi binh thuong.
if (fxRt.ws && fxRt.ws.readyState === 1) {
try { fxRt.ws.send(JSON.stringify({ cmd: 'flush' })); } catch (e) {}
}
fxRt.flushing = true;
if (fxRt.flushTimer) clearTimeout(fxRt.flushTimer);
fxRt.flushTimer = setTimeout(() => {
fxRt.flushTimer = null;
fxRt.flushing = false;
if (fxRt.node) fxRt.node.port.postMessage({ type: 'flush_done' });
}, 500);
return;
}
if (d.type === 'set_param') {
try { fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot: d.slot, key: d.key, value: d.value })); } catch (e) {}
return;
@@ -1985,7 +2009,9 @@ function fxRtStop(silent) {
fxRt.startToken++;
fxRt.starting = false;
if (fxRt.pumpTimer) { clearInterval(fxRt.pumpTimer); fxRt.pumpTimer = null; }
if (fxRt.flushTimer) { clearTimeout(fxRt.flushTimer); fxRt.flushTimer = null; }
fxRt.inQueue = [];
fxRt.flushing = false;
if (!fxRt.active && !fxRt.ws && !fxRt.node && !fxRt.sessionId) return;
let _fxStopNode = null;
try {
@@ -24466,11 +24492,11 @@ const App = () => {
console.warn('[FX RT track] start fail', trackId, path, e);
return;
}
const entry = { sessionId: st.session_id, ws: null, node: null, ready: false, path, spliced: null, chainKey: fxRtTrackChainKey(chain) + '|vb:' + (track && track.vstFxBypass ? '1' : '0'), paramsBySlot: chain.map(s => fxRtEntryParams(s, null)), inQueue: [], pumpTimer: null, lastInSent: 0 };
const entry = { sessionId: st.session_id, ws: null, node: null, ready: false, path, spliced: null, chainKey: fxRtTrackChainKey(chain) + '|vb:' + (track && track.vstFxBypass ? '1' : '0'), paramsBySlot: chain.map(s => fxRtEntryParams(s, null)), inQueue: [], pumpTimer: null, lastInSent: 0, flushing: false, flushTimer: null };
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608312300`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609011200`);
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
entry.node = node;
const u = new URL(API_BASE_URL);
@@ -24488,7 +24514,19 @@ const App = () => {
} catch (e) {}
};
ws.onmessage = (ev) => {
if (typeof ev.data === 'string') {
try {
const d = JSON.parse(ev.data);
if (d && d.cmd === 'flush_done') {
entry.flushing = false;
if (entry.flushTimer) { clearTimeout(entry.flushTimer); entry.flushTimer = null; }
if (entry.node) entry.node.port.postMessage({ type: 'flush_done' });
}
} catch (e) {}
return;
}
if (!(ev.data instanceof ArrayBuffer) || !entry.node) return;
if (entry.flushing) return; // V24: output stale trong luc flush — drop
const f = new Float32Array(ev.data);
const half = f.length / 2;
const l = new Float32Array(half);
@@ -24501,6 +24539,21 @@ const App = () => {
entry.node.port.onmessage = (ev) => {
const d = ev.data;
if (!d || !entry.ws) return;
if (d.type === 'flush') {
// V24: nhu master — worklet underrun-resume → flush engine ring
// (xoa backlog stale) → het lag ~1s sau stall. Fallback phong ack mat.
if (entry.ws && entry.ws.readyState === 1) {
try { entry.ws.send(JSON.stringify({ cmd: 'flush' })); } catch (e) {}
}
entry.flushing = true;
if (entry.flushTimer) clearTimeout(entry.flushTimer);
entry.flushTimer = setTimeout(() => {
entry.flushTimer = null;
entry.flushing = false;
if (entry.node) entry.node.port.postMessage({ type: 'flush_done' });
}, 500);
return;
}
if (d.type === 'set_param') {
try { entry.ws.send(JSON.stringify({ cmd: 'set_param', slot: d.slot, key: d.key, value: d.value })); } catch (e) {}
return;
@@ -24588,7 +24641,9 @@ const App = () => {
const entry = fxRtTracks[key];
if (!entry) return;
if (entry.pumpTimer) { clearInterval(entry.pumpTimer); entry.pumpTimer = null; }
if (entry.flushTimer) { clearTimeout(entry.flushTimer); entry.flushTimer = null; }
entry.inQueue = [];
entry.flushing = false;
try {
fxRtTrackUnsplice(trackId, path);
if (entry.node) { entry.node.port.onmessage = null; try { entry.node.disconnect(); } catch (e) {} entry.node = null; }
@@ -25388,6 +25443,9 @@ const App = () => {
// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
const startLocalTrackPlayback = (trackId, offsetTime) => {
const context = getAudioContext();
// V24: solo/local path — gọi fxRtStart (idempotent) để âm solo qua master
// chain (trước đây path này bỏ qua → solo không qua master FX).
try { fxRtStart(); } catch (e) { console.warn('[FX RT] start error:', e); }
// V12: parity với startTrackPlayback — loop restart (solo/local) sau
// stopAllPlayback(): hủy STOP#2 + báo PLAY (nếu không C++ transportStopped
// giữ true → mọi note-on bị drop → solo/local loop pass 2+ câm) + set