fix(fx-rt): het crackle master FX - limiter attack smooth + crossfade khi bat/tat chain

- NativeInstrumentEngine: kAttack 0.9->0.1 (het zipper khi sum nhieu track)
- app.jsx: crossfade masterFxDryGain/masterFxGain thay hard-rewire graph luc
  activation/deactivation master chain FX; worklet bao 'started' -> ramp dung
  luc; PDC tre sau khi dry silent (tranh gap 80ms)
- sf-fx-realtime.js: fade-in 512 mau luc FILL xong / resume sau underrun
- index.html + addModule: cache-buster v=202608241200
- rebuild app.precompiled.js, copy bundle vao dist/ + src-tauri/resources/
- keo dai fix cac round truoc: FILL 16, FXRT_BURST_MS, REBUFFER, pacing,
  VST GUI click/focus, solo crackle
- them harness + plans/patch scripts
This commit is contained in:
2026-08-24 07:46:09 +07:00
parent 373a96cf7b
commit 10997a65b9
21 changed files with 1181 additions and 177 deletions
+263 -72
View File
@@ -1054,6 +1054,16 @@ function initMasterBus(ctx) {
const output = ctx.createGain();
output.gain.value = 1.0;
// Crossfade master-FX activation (het click khi bat/tat master chain FX):
// path mac dinh outputAnalyser -> masterFxDryGain(1) -> pdcDelay -> output;
// nhanh FX noi vao masterFxGain(0) -> output. fxRtStart/fxRtStop ramp 2 gain
// nay thay vi hard-rewire graph (dry path luon noi san — no click, no gap
// trong luc worklet FILL ~86ms).
const masterFxDryGain = ctx.createGain();
masterFxDryGain.gain.value = 1.0;
const masterFxGain = ctx.createGain();
masterFxGain.gain.value = 0.0;
// Per-track mastering-bypass dry bus: tracks with bypass ON feed into
// dryInput -> dryOutput -> output, skipping the mastering modules
// (EQ / Imager / Maximizer) while still passing the master volume fader
@@ -1104,6 +1114,8 @@ function initMasterBus(ctx) {
compressor: ctx.createDynamicsCompressor(),
analyser,
output,
masterFxDryGain,
masterFxGain,
masteringActive: false,
dryInput,
dryOutput,
@@ -1151,7 +1163,10 @@ function initMasterBus(ctx) {
// "state is bad" → CÂM + stuck. Mastering chain có comp/lim module riêng khi bật.
masterBus.input.connect(masterBus.inputAnalyser);
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
masterBus.outputAnalyser.connect(masterBus.output);
masterBus.outputAnalyser.connect(masterBus.masterFxDryGain);
masterBus.masterFxDryGain.connect(masterBus.pdcDelay);
masterBus.outputAnalyser.connect(masterBus.masterFxGain);
masterBus.masterFxGain.connect(masterBus.output);
masterBus.muteGain.connect(masterBus.analyser);
masterBus.muteGain.connect(masterBus.splitter);
masterBus.analyser.connect(ctx.destination);
@@ -1631,7 +1646,13 @@ function encodeFloatWavBlob(pcm, sampleRate, channels) {
// worklet → analyser (sau master fader, trước metering/destination — khớp thứ
// 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, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, chainKey: null, latencyTotal: 0 };
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 };
// 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
// burst (4 block ~21ms) mỗi 21ms → ring luôn kịp tiêu thụ, audio hồi phục
// trơn sau stall (FILL hấp thụ phần đầu).
const FXRT_BURST_MS = 21;
// Signature của chain VST realtime — phát hiện chain ĐỔI (insert/remove/bypass/
// reorder VST) → restart session với chain mới (fxRtStart early-return khi
// active, nếu không so chainKey thì VST mới insert không bao giờ được áp).
@@ -1688,20 +1709,41 @@ function fxRtPushAllParams() {
});
});
}
async function fxRtSetChain(chain) {
const sid = fxRt.sessionId;
if (!sid || !fxRt.ws || fxRt.ws.readyState !== 1) return;
const chainPayload = chain.map(s => s.type === 'vst3'
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: false });
try {
await window.SonicAPI.fxRealtimeSetChain({ session_id: sid, fx_chain: chainPayload });
try { fxRtPushAllParams(); } catch (e) {}
} catch (e) {
console.warn('[FX RT] set-chain fail:', e);
}
}
async function fxRtStart() {
const chain = fxRtChain();
const ck = fxRtChainKey(chain);
if (fxRt.active) {
// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart.
if (!fxRt.node || fxRt.node.context !== getAudioContext()) fxRtStop(true);
// Chain VST thay đổi (insert/remove/bypass/reorder trong MASTER FX CHAIN)
// → stop + start lại để bridge load đúng chuỗi FX mới.
else if (fxRt.chainKey !== ck) fxRtStop(true);
// Chain thay đổi (insert/remove/bypass/reorder/preset GUI) — KHÔNG restart
// (ngắt âm + crackle): set_chain lên session ĐANG CHẠY → bridge worker
// load chain mới (audio giữ chain cũ tới khi swap) → swap atomic mượt.
else if (fxRt.chainKey !== ck) { fxRtSetChain(chain); fxRt.chainKey = ck; return; }
else return;
}
if (!chain.length) return;
// Token chống race bypass: user bấm PWR/Bypass khi fxRtStart đang chờ
// fxRealtimeStart (~4s load VST). Lúc đó fxRt.active vẫn false → effect
// `else if (fxRt.active) fxRtStop()` KHÔNG chạy → start in-flight vẫn nối
// node → bypass vô hiệu. Mỗi fxRtStop() bump startToken → mọi checkpoint
// dưới đây thấy mismatch → hủy session vừa tạo, không nối node.
const token = ++fxRt.startToken;
fxRt.starting = true;
const ctx = getAudioContext();
if (!ctx || !ctx.audioWorklet || !masterBus || !masterBus.output || !masterBus.analyser) return;
if (!ctx || !ctx.audioWorklet || !masterBus || !masterBus.output || !masterBus.analyser || !masterBus.masterFxGain || !masterBus.masterFxDryGain) return;
const chainPayload = chain.map(s => s.type === 'vst3'
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: false });
@@ -1711,12 +1753,21 @@ async function fxRtStart() {
} catch (e) {
console.warn('[FX RT] start fail:', e);
showToast('Không khởi động VST FX realtime: ' + (e.message || e), 'warning');
fxRt.starting = false;
return;
}
if (token !== fxRt.startToken) {
// Bypass/stop giữa lúc engine đang load chain — hủy session vừa tạo.
console.warn('[FX RT] start cancelled mid-flight — session stopped');
try { window.SonicAPI.fxRealtimeStop({ session_id: st.session_id }).catch(() => {}); } catch (e2) {}
fxRt.starting = false;
return;
}
fxRt.sessionId = st.session_id;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212401`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241200`);
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);
const wsProto = u.protocol === 'https:' ? 'wss://' : 'ws://';
@@ -1726,7 +1777,9 @@ async function fxRtStart() {
fxRt.ws = ws;
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
if (token !== fxRt.startToken) { try { ws.close(); } catch (e) {} fxRt.starting = false; return; }
fxRt.active = true;
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) {}
};
@@ -1738,7 +1791,9 @@ async function fxRtStart() {
const d = JSON.parse(ev.data);
if (d && d.cmd === 'latency' && typeof d.total === 'number') {
fxRt.latencyTotal = d.total;
fxRtApplyPdc();
// 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();
}
} catch (e) {}
return;
@@ -1753,29 +1808,61 @@ async function fxRtStart() {
};
ws.onclose = () => fxRtStop(true);
ws.onerror = () => { try { ws.close(); } catch (e) {} };
// Worklet gom 4 block (1024 mẫu) vào 1 frame → tách 4 WS frame 512 floats
// ở đây. Bridge VST3 nhận input dồn → take=4 → batch 1 call process 1024
// mẫu (~3.5-4.75ms/block) → đủ 187.5/s (take=1: 5.5ms > budget 5.33ms →
// drop → crackle). KHÔNG dùng f.subarray().buffer — nó gửi cả backing
// buffer 8192B; slice() copy đúng 2048B.
const fxRtSendFrames = (d) => {
if (!fxRt.ws || fxRt.ws.readyState !== 1) return;
try {
const f = new Float32Array(d.data);
if (f.length > 512) {
for (let i = 0; i < f.length; i += 512) fxRt.ws.send(f.slice(i, i + 512).buffer);
} else {
fxRt.ws.send(d.data);
}
} catch (e) {}
};
const fxRtInPump = () => {
if (!fxRt.inQueue.length) return;
const now = Date.now();
while (fxRt.inQueue.length && now - fxRt.lastInSent >= FXRT_BURST_MS) {
fxRt.lastInSent = now;
fxRtSendFrames(fxRt.inQueue.shift());
}
if (fxRt.inQueue.length) {
if (fxRt.pumpTimer) clearTimeout(fxRt.pumpTimer);
fxRt.pumpTimer = setTimeout(fxRtInPump, Math.max(1, FXRT_BURST_MS - (now - fxRt.lastInSent)));
} else if (fxRt.pumpTimer) {
clearTimeout(fxRt.pumpTimer);
fxRt.pumpTimer = null;
}
};
fxRt.node.port.onmessage = (ev) => {
const d = ev.data;
if (!d || !fxRt.ws) return;
if (d.type === 'started') {
// Worklet bat dau phat (sau FILL) -> crossfade dry->fx muot. Token guard
// chong race: neu stop/restart xay ra trong luc cho, bo qua.
if (fxRt.fadedIn) return;
setTimeout(() => {
if (token !== fxRt.startToken) return;
fxRt.fadedIn = true;
fxRtCrossfade(true, token);
// Dry gan silent (tc 0.025) roi moi tre pdcDelay — tranh gap khi delay
// dang lon dan.
setTimeout(() => fxRtApplyPdc(), 250);
}, 30);
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;
}
if (d.type !== 'in') return;
// Worklet gom 4 block (1024 mẫu) vào 1 frame → tách 4 WS frame 512 floats
// ở đây. Bridge VST3 nhận input dồn → take=4 → batch 1 call process 1024
// mẫu (~3.5-4.75ms/block) → đủ 187.5/s (take=1: 5.5ms > budget 5.33ms →
// drop → crackle). KHÔNG dùng f.subarray().buffer — nó gửi cả backing
// buffer 8192B; slice() copy đúng 2048B.
if (ws.readyState === 1) {
try {
const f = new Float32Array(d.data);
if (f.length > 512) {
for (let i = 0; i < f.length; i += 512) ws.send(f.slice(i, i + 512).buffer);
} else {
ws.send(d.data);
}
} catch (e) {}
}
fxRt.inQueue.push(d);
fxRtInPump();
};
// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn theo burst (main thread bị
// rAF renderFrame block khi mở MASTERING PANEL) → SHM ring 4-slot tràn → mất
@@ -1783,9 +1870,13 @@ async function fxRtStart() {
// Phase 4.2: fader SAU worklet — worklet chèn giữa outputAnalyser (chain
// output) và output (fader/destination). output → analyser/splitter giữ
// nguyên (meter đo âm SAU fader).
// Crossfade activation: KHONG disconnect dry path — outputAnalyser van noi
// masterFxDryGain -> pdcDelay -> output (gain 1); nhanh fx noi masterFxGain
// (gain 0) -> output. Tin hieu 'started' tu worklet -> ramp 2 gain -> switch
// muot, het click hard-rewire.
try { masterBus.outputAnalyser.disconnect(masterBus.output); } catch (e) {}
masterBus.outputAnalyser.connect(fxRt.node);
fxRt.node.connect(masterBus.output);
fxRt.node.connect(masterBus.masterFxGain);
fxRt.chainKey = ck;
} catch (e) {
console.warn('[FX RT] setup fail:', e);
@@ -1793,18 +1884,40 @@ async function fxRtStart() {
}
}
function fxRtStop(silent) {
// Phải bump token TRƯỚC guard early-return: lúc fxRtStart đang in-flight thì
// active/ws/node/sessionId đều chưa có → guard sẽ return, nhưng start vẫn
// phải bị hủy (token mismatch ở checkpoint).
fxRt.startToken++;
fxRt.starting = false;
if (fxRt.pumpTimer) { clearInterval(fxRt.pumpTimer); fxRt.pumpTimer = null; }
fxRt.inQueue = [];
if (!fxRt.active && !fxRt.ws && !fxRt.node && !fxRt.sessionId) return;
let _fxStopNode = null;
try {
if (fxRt.node) {
fxRt.node.disconnect();
if (masterBus && masterBus.outputAnalyser && masterBus.output) {
try { masterBus.outputAnalyser.disconnect(masterBus.output); } catch (e) {}
try { masterBus.outputAnalyser.connect(masterBus.output); } catch (e) {}
_fxStopNode = fxRt.node;
if (masterBus && masterBus.masterFxGain && masterBus.masterFxDryGain) {
// Crossfade deactivation: ramp dry 0->1 / fx 1->0 roi MOI disconnect
// (tranh click hard-switch nguoc). Dry path da noi san qua
// masterFxDryGain — khong can reconnect outputAnalyser->output. PDC giam
// ve 0 sau khi dry chiem cho (fx silent).
try { fxRtCrossfade(false); } catch (e) {}
setTimeout(() => {
try { _fxStopNode.disconnect(); } catch (e) {}
_fxStopNode.port.onmessage = null;
if (fxRt.node === _fxStopNode) fxRt.node = null;
fxRt.fadedIn = false;
fxRtApplyPdc();
}, 180);
} else {
// Fallback graph cu (khong co crossfade gains) — hard switch nhu cu.
try { _fxStopNode.disconnect(); } catch (e) {}
if (masterBus && masterBus.outputAnalyser && masterBus.output) {
try { masterBus.outputAnalyser.connect(masterBus.output); } catch (e) {}
}
_fxStopNode.port.onmessage = null;
if (fxRt.node === _fxStopNode) fxRt.node = null;
}
fxRt.node.port.onmessage = null;
fxRt.node = null;
}
} catch (e) {}
try { if (fxRt.ws) { fxRt.ws.onclose = null; fxRt.ws.onmessage = null; try { fxRt.ws.close(); } catch (e) {} fxRt.ws = null; } } catch (e) {}
@@ -1814,7 +1927,9 @@ function fxRtStop(silent) {
fxRt.active = false;
fxRt.chainKey = null;
fxRt.latencyTotal = 0;
fxRtApplyPdc();
// PDC giam ve 0 sau khi dry chiem cho (trong setTimeout cua nhanh crossfade)
// hoac ngay neu khong co node — khong goi tai day de tranh gap 80ms.
if (!_fxStopNode) fxRtApplyPdc();
if (sid) {
try { window.SonicAPI.fxRealtimeStop({ session_id: sid }).catch(() => {}); } catch (e) {}
}
@@ -1837,6 +1952,20 @@ function fxRtApplyPdc() {
try { bus.pdcDelay.delayTime.setTargetAtTime(l / (ctx.sampleRate || 44100), ctx.currentTime, 0.05); } catch (e) {}
}
// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi
// hard-rewire (click). Hai gain nay duoc tao trong initMasterBus.
function fxRtCrossfade(toFx, token) {
const ctx = getAudioContext();
const bus = masterBus;
if (!ctx || !bus || !bus.masterFxGain || !bus.masterFxDryGain) return;
if (token !== undefined && token !== fxRt.startToken) return;
const tc = 0.025;
try {
bus.masterFxGain.gain.setTargetAtTime(toFx ? 1 : 0, ctx.currentTime, tc);
bus.masterFxDryGain.gain.setTargetAtTime(toFx ? 0 : 1, ctx.currentTime, tc);
} catch (e) {}
}
// ── Realtime VST FX per-track (SHM bridge) — mọi luồng track qua bridge ─────
// Mỗi track: session riêng (1 WS/SHM) cho TỪNG đường chain có âm — audio-clips
// (path 'audio', splice fxEntry→mods→worklet→fxLegacyIn) và MIDI/SF (path 'sf',
@@ -2743,22 +2872,6 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMast
React.createElement("div", {
className: "flex flex-col items-stretch w-[300px] shrink-0 strip-bg rounded-lg p-2 text-slate-300 select-none shadow-2xl relative overflow-hidden"
},
React.createElement("div", { className: "space-y-1.5 mb-2" },
React.createElement("button", {
onClick: () => setShowMasteringModal(true),
className: "w-full bg-slate-800 hover:bg-slate-700 text-[10px] font-semibold py-1 rounded border border-slate-700 text-slate-300 tracking-tight"
}, "FX CHAIN"),
React.createElement("button", {
onClick: () => setRefOpen(true),
className: "w-full bg-slate-800 hover:bg-slate-700 text-[10px] font-semibold py-1 rounded border border-slate-700 text-cyan-300 tracking-tight"
}, "A/B REF"),
React.createElement("div", {
className: "flex items-center justify-between bg-slate-950 border border-slate-800 rounded px-1.5 py-0.5 text-[10px] font-mono"
},
React.createElement("span", { className: "text-slate-400 truncate" }, "Output 1 / Output 2"),
React.createElement("i", { className: "fa-solid fa-circle-notch text-[9px] text-slate-500" })
)
),
React.createElement("div", { className: "flex flex-col items-center my-0.5" },
React.createElement("span", { className: "text-[9px] text-slate-400 font-mono" }, panText || 'center'),
React.createElement("div", { className: "flex items-center gap-1" },
@@ -12443,7 +12556,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
React.useEffect(() => {
deadRef.current = false;
setDead(false);
const tick = setInterval(() => { const img = imgRef.current; if (img) img.src = url + '/frame?t=' + Date.now(); }, 100);
const tick = setInterval(() => { const img = imgRef.current; if (img && img.complete) img.src = url + '/frame?t=' + Date.now(); }, 100);
const ping = setInterval(() => {
fetch(url + '/ping').then(r => { if (!r.ok) throw new Error('bad'); }).catch(() => {
if (!deadRef.current) { deadRef.current = true; setDead(true); onDead && onDead(); }
@@ -12501,7 +12614,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
} catch (e) {}
};
refresh();
const iv = setInterval(refresh, 500);
const iv = setInterval(refresh, 250);
return () => { alive = false; clearInterval(iv); };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
@@ -12611,6 +12724,25 @@ let _persistFxGuiTrackId = null;
// Latest preset_b64 captured from each plugin's embedded GUI (poll /preset)
// - merged into the project on save so knob tweaks survive reload.
let _capturedPresetByPath = {};
// Baseline of each plugin's GUI state at embed-open time. /preset round-trips
// through getState/applyPreset and the blob can differ from the value the
// session started with; restarting the chain on that first difference reloads
// the VST and crackles. First capture after open = baseline (no restart); only
// a CHANGE vs baseline schedules the ~3s restart that syncs realtime to GUI.
let _fxGuiBaselineByPath = {};
// Debounced restart khi preset GUI đổi (preset poll) — _capturedPresetByPath
// đổi làm chainKey đổi nhưng không ai gọi fxRtStart/reconcile → session
// realtime GIỮ preset cũ (xoay knob trong GUI không đổi âm realtime). Restart
// ~3s sau khi knob DỪNG xoay (không restart mỗi poll 1.5s giữa lúc kéo — load
// VST ~4s ngắt âm). fn tự early-return khi chainKey không đổi.
let _presetRestartTimers = {};
function _schedulePresetRestart(key, fn, ms) {
if (_presetRestartTimers[key]) clearTimeout(_presetRestartTimers[key]);
_presetRestartTimers[key] = setTimeout(() => {
delete _presetRestartTimers[key];
try { fn(); } catch (e) {}
}, ms);
}
const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const [activeType, setActiveType] = React.useState('eq');
const [addModuleOpen, setAddModuleOpen] = React.useState(false);
@@ -12661,16 +12793,25 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
fxGuiOpenRef.current = fxGuiOpen;
const closeFxGui = () => {
const g = fxGuiOpenRef.current;
if (g) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }
if (g) {
// Capture preset CUỐI trước khi đóng (poll 1.5s có thể chưa kịp khi chỉnh
// xong đóng ngay) — gọi /preset TRƯỚC /close (close kill process bridge).
try { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}).then(() => { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }); } catch (e) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e2) {} }
}
setFxGuiOpen(null);
if (g && g.path) {
const _tk = 'track:' + ((track && track.id) || '') + ':' + g.path;
if (_presetRestartTimers[_tk]) { clearTimeout(_presetRestartTimers[_tk]); delete _presetRestartTimers[_tk]; }
delete _fxGuiBaselineByPath[g.path];
}
if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: null, vstGuiOpenName: null });
};
React.useEffect(() => () => { const g = fxGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
React.useEffect(() => () => { const g = fxGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} const _tk = 'track:' + ((track && track.id) || '') + ':' + g.path; if (_presetRestartTimers[_tk]) { clearTimeout(_presetRestartTimers[_tk]); delete _presetRestartTimers[_tk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset).
React.useEffect(() => {
if (!fxGuiOpen || !fxGuiOpen.url) return;
const iv = setInterval(() => {
fetch(fxGuiOpen.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64) _capturedPresetByPath[fxGuiOpen.path] = d.preset_b64; }).catch(() => {});
fetch(fxGuiOpen.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64) { const _p = fxGuiOpen.path; const _b = _fxGuiBaselineByPath[_p]; if (_b === undefined) { _fxGuiBaselineByPath[_p] = d.preset_b64; _capturedPresetByPath[_p] = d.preset_b64; return; } if (d.preset_b64 !== _b) { _fxGuiBaselineByPath[_p] = d.preset_b64; _capturedPresetByPath[_p] = d.preset_b64; const _tid = track && track.id; _schedulePresetRestart('track:' + (_tid || '') + ':' + _p, () => { if (_tid && window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(_tid, {}); }, 3000); } } }).catch(() => {});
}, 1500);
return () => clearInterval(iv);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -12680,7 +12821,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
if (!track || !track.vstGuiOpenPath || fxGuiOpen) return;
const s = (track.fxChain || []).find(x => x.path === track.vstGuiOpenPath);
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRtTrackShmName(track && track.id) }).then(r => {
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRtTrackShmName(track && track.id), preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
if (r && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url });
}).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -12688,7 +12829,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (rem) delete _capturedPresetByPath[rem.path]; if (idx === activeVstIdx) setActiveVstIdx(-1); setVstChain(vstChain.filter((_, i) => i !== idx)); };
const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };
const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRtTrackShmName(track && track.id) }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRtTrackShmName(track && track.id), preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; const next = [...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]; setVstChain(next); setActiveVstIdx(next.length - 1); };
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
const uploadVstPreset = (idx) => {
@@ -13244,17 +13385,25 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
masterGuiOpenRef.current = masterGuiOpen;
const closeMasterGui = () => {
const g = masterGuiOpenRef.current;
if (g) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }
if (g) {
// Capture preset CUỐI trước khi đóng (giống closeFxGui) — /preset rồi /close.
try { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}).then(() => { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }); } catch (e) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e2) {} }
}
setMasterGuiOpen(null);
if (g && g.path) {
const _mk = 'master:' + g.path;
if (_presetRestartTimers[_mk]) { clearTimeout(_presetRestartTimers[_mk]); delete _presetRestartTimers[_mk]; }
delete _fxGuiBaselineByPath[g.path];
}
setActiveMasterVstPath(null);
setOzState(prev => { const n = { ...(prev || {}) }; delete n.gui_open_path; delete n.gui_open_name; return n; });
};
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} const _mk = 'master:' + g.path; if (_presetRestartTimers[_mk]) { clearTimeout(_presetRestartTimers[_mk]); delete _presetRestartTimers[_mk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset).
React.useEffect(() => {
if (!masterGuiOpen || !masterGuiOpen.url) return;
const iv = setInterval(() => {
fetch(masterGuiOpen.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64) _capturedPresetByPath[masterGuiOpen.path] = d.preset_b64; }).catch(() => {});
fetch(masterGuiOpen.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64) { const _p = masterGuiOpen.path; const _b = _fxGuiBaselineByPath[_p]; if (_b === undefined) { _fxGuiBaselineByPath[_p] = d.preset_b64; _capturedPresetByPath[_p] = d.preset_b64; return; } if (d.preset_b64 !== _b) { _fxGuiBaselineByPath[_p] = d.preset_b64; _capturedPresetByPath[_p] = d.preset_b64; _schedulePresetRestart('master:' + _p, () => fxRtStart(), 3000); } } }).catch(() => {});
}, 1500);
return () => clearInterval(iv);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -13264,7 +13413,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
if (!isOpen || !ozState || !ozState.gui_open_path || masterGuiOpen) return;
const s = (ozState.vstFxChain || []).find(x => x.path === ozState.gui_open_path);
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true, shm: fxRt.sessionId || '' }).then(r => {
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url });
}).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -13290,7 +13439,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeMasterVst = (idx) => { const rem = masterVstChain[idx]; if (rem) delete _capturedPresetByPath[rem.path]; setMasterVstChain(masterVstChain.filter((_, i) => i !== idx)); };
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRt.sessionId || '' }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
@@ -19052,7 +19201,7 @@ const App = () => {
// masterConnected nhưng fxRt không tự dừng → tắt PWR mà âm không đổi.
try {
if (fxRtChain().length) { fxRtStart(); }
else if (fxRt.active) { fxRtStop(); }
else if (fxRt.active || fxRt.starting) { fxRtStop(); }
} catch (e) { console.warn('[FX RT] resync fail:', e); }
// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
// override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp
@@ -23684,11 +23833,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), paramsBySlot: chain.map(s => fxRtEntryParams(s, null)), inQueue: [], pumpTimer: null };
const entry = { sessionId: st.session_id, ws: null, node: null, ready: false, path, spliced: null, chainKey: fxRtTrackChainKey(chain), paramsBySlot: chain.map(s => fxRtEntryParams(s, null)), inQueue: [], pumpTimer: null, lastInSent: 0 };
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212401`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241200`);
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
entry.node = node;
const u = new URL(API_BASE_URL);
@@ -23725,16 +23874,34 @@ const App = () => {
}
if (d.type !== 'in') return;
// Burst 4 block (1024 mẫu) → tách 4 WS frame 512 floats → bridge batch 4.
if (ws.readyState === 1) {
try {
const f = new Float32Array(d.data);
if (f.length > 512) {
for (let i = 0; i < f.length; i += 512) ws.send(f.slice(i, i + 512).buffer);
} else {
ws.send(d.data);
// Pace 1 burst/21ms (giống master) — tránh dồn frame sau stall làm tràn ring.
entry.inQueue.push(d);
const pump = () => {
if (!entry.inQueue.length) return;
const now = Date.now();
while (entry.inQueue.length && now - entry.lastInSent >= FXRT_BURST_MS) {
entry.lastInSent = now;
const dd = entry.inQueue.shift();
if (entry.ws && entry.ws.readyState === 1) {
try {
const f = new Float32Array(dd.data);
if (f.length > 512) {
for (let i = 0; i < f.length; i += 512) entry.ws.send(f.slice(i, i + 512).buffer);
} else {
entry.ws.send(dd.data);
}
} catch (e) {}
}
} catch (e) {}
}
}
if (entry.inQueue.length) {
if (entry.pumpTimer) clearTimeout(entry.pumpTimer);
entry.pumpTimer = setTimeout(pump, Math.max(1, FXRT_BURST_MS - (now - entry.lastInSent)));
} else if (entry.pumpTimer) {
clearTimeout(entry.pumpTimer);
entry.pumpTimer = null;
}
};
pump();
};
} catch (e) {
console.warn('[FX RT track] setup fail', trackId, path, e);
@@ -23750,6 +23917,23 @@ const App = () => {
entry.spliced = null;
} catch (e) { console.warn('[FX RT track] unsplice fail', trackId, path, e); }
};
// Chain đổi trên session ĐANG CHẠY — set_chain KHÔNG restart (bridge swap
// atomic khi worker load xong) → load VST FX / đổi preset GUI không ngắt
// âm/crackle. entry.chainKey cập nhật ngay — reconcile sau đó thấy khớp.
const fxRtTrackSetChain = (trackId, path, track, chainKey) => {
const key = trackId + ':' + path;
const entry = fxRtTracks[key];
if (!entry || !entry.ws || entry.ws.readyState !== 1) return;
const chain = fxRtTrackChain(track);
if (!chain.length) return;
const chainPayload = chain.map(s => s.type === 'vst3'
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: false });
entry.chainKey = chainKey;
window.SonicAPI.fxRealtimeSetChain({ session_id: entry.sessionId, fx_chain: chainPayload }).then(() => {
try { (entry.paramsBySlot || []).forEach((pl, slot) => pl.forEach(p => { if (entry.ws && entry.ws.readyState === 1) entry.ws.send(JSON.stringify({ cmd: 'set_param', slot, key: p.key, value: p.value })); })); } catch (e) {}
}).catch(e => console.warn('[FX RT track] set-chain fail', trackId, path, e));
};
const fxRtTrackSplice = (trackId, path) => {
const key = trackId + ':' + path;
const entry = fxRtTracks[key];
@@ -23800,7 +23984,14 @@ const App = () => {
const key = trackId + ':' + p;
const entry = fxRtTracks[key];
if (!entry) fxRtTrackStart(trackId, track, p);
else if (entry.chainKey !== chainKey) { fxRtTrackStop(trackId, p, true); fxRtTrackStart(trackId, track, p); }
else if (entry.chainKey !== chainKey) {
// Chain đổi (load/đổi VST FX, preset GUI, bypass slot) — session
// đang chạy → set_chain KHÔNG restart (bridge swap atomic khi load
// xong) → hết crackle khi load VST / chỉnh thông số. Session chưa
// ready (start in-flight) → stop+start lại cho đúng chain.
if (entry.ready) fxRtTrackSetChain(trackId, track, p, chainKey);
else { fxRtTrackStop(trackId, p, true); fxRtTrackStart(trackId, track, p); }
}
else if (entry.ready && node) {
fxRtTrackUnsplice(trackId, p); // chain mới (sau rebuild) — splice lại đúng mods
fxRtTrackSplice(trackId, p);
File diff suppressed because one or more lines are too long
+1
View File
@@ -87,6 +87,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
// /ws/fx-realtime/{id} đẩy/kéo block audio; stop → tắt bridge.
fxRealtimeStart: (payload) => apiRequest('/api/v1/plugins/fx-realtime/start', { method: 'POST', body: JSON.stringify(payload) }),
fxRealtimeStop: (payload) => apiRequest('/api/v1/plugins/fx-realtime/stop', { method: 'POST', body: JSON.stringify(payload) }),
fxRealtimeSetChain: (payload) => apiRequest('/api/v1/plugins/fx-realtime/set-chain', { method: 'POST', body: JSON.stringify(payload) }),
// Mở native GUI của VST FX (mastering chain / FX rack) qua bridge
openFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui', { method: 'POST', body: JSON.stringify(payload) }),
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
+24 -5
View File
@@ -24,7 +24,8 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
super();
this.BLOCK = 256; // FXRT_BLOCK
this.BURST = 4; // số block gom 1 frame (bridge batch)
this.FILL = 8; // block buffer tối thiểu trước khi phát
this.FILL = 16; // 16 block ~86ms: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, solo restart)
this.REBUFFER = 2; // 2 block ~11ms: sau underrun chỉ cần bấy nhiêu để phát lại — gap ngắn, không còn 86ms im lặng
this.CAP = 32; // cap queue chặn overflow
this.bufLen = this.BLOCK * this.BURST;
this.inL = new Float32Array(this.bufLen);
@@ -33,6 +34,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.outQueue = []; // [{l: Float32Array, r: Float32Array}]
this.outPos = 0;
this.playing = false; // false → im lặng (đang chờ đủ FILL)
this.hasStarted = false; // đã phát block đầu chưa — phân biệt FILL khởi đầu vs REBUFFER sau underrun
this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click
this.outGain = 0; // gain phát hiện tại (0→1 trong fade)
this.fadeLeft = 0; // số mẫu còn lại của fade
this.port.onmessage = (e) => {
const d = e.data;
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
@@ -76,16 +81,30 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
for (let i = 0; i < n; i++) {
if (this.playing && this.outQueue.length === 0) this.playing = false;
if (!this.playing) {
if (this.outQueue.length >= this.FILL) this.playing = true;
else {
// Khởi đầu: chờ đủ FILL (hấp thụ jitter). Sau underrun: chỉ cần
// REBUFFER (2 block ~11ms) — gap ngắn thay vì 86ms im lặng.
const need = this.hasStarted ? this.REBUFFER : this.FILL;
if (this.outQueue.length >= need) {
this.playing = true;
this.hasStarted = true;
// Fade-in mượt (hết click lúc FILL xong / resume sau underrun); báo
// main thread để crossfade dry→fx đúng thời điểm.
this.outGain = 0;
this.fadeLeft = this.FADE;
this.port.postMessage({ type: 'started' });
} else {
outL[i] = 0;
if (outR) outR[i] = 0;
continue;
}
}
const blk = this.outQueue[0];
outL[i] = blk.l[this.outPos];
if (outR) outR[i] = blk.r[this.outPos];
if (this.fadeLeft > 0) {
this.outGain = Math.min(1, this.outGain + 1 / this.FADE);
this.fadeLeft--;
}
outL[i] = blk.l[this.outPos] * this.outGain;
if (outR) outR[i] = blk.r[this.outPos] * this.outGain;
this.outPos++;
if (this.outPos >= this.BLOCK) {
this.outQueue.shift();