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:
+61
-3
@@ -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
|
||||
|
||||
@@ -328,12 +328,15 @@ else if(fxRt.chainKey!==ck){fxRtSetChain(chain);fxRt.chainKey=ck;return;}else re
|
||||
// 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||!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:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}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=202608312300`);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://';const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);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
|
||||
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=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);const wsProto=u.protocol==='https:'?'wss://':'ws://';const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);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.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){}};ws.onmessage=ev=>{// Gap 8 (PDC realtime): text frame {cmd:'latency', total} tu server ->
|
||||
// cap nhat delay dry-bypass path (bu latency chain VST3 realtime).
|
||||
if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='latency'&&typeof d.total==='number'){fxRt.latencyTotal=d.total;// 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;}if(!(ev.data instanceof ArrayBuffer)||!fxRt.node)return;const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}fxRt.node.port.postMessage({type:'out',l,r});};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
|
||||
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);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}fxRt.node.port.postMessage({type:'out',l,r});};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
|
||||
@@ -344,7 +347,10 @@ const now=Date.now();while(fxRt.inQueue.length&&now-fxRt.lastInSent>=FXRT_BURST_
|
||||
// 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==='stats'){// Telemetry worklet (mỗi ~2s): chẩn đoán crackle session thật.
|
||||
setTimeout(()=>fxRtApplyPdc(),250);},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;}if(d.type==='stats'){// Telemetry worklet (mỗi ~2s): chẩn đoán crackle session thật.
|
||||
fxRt.stats=d;console.debug('[FX RT stats]',d);return;}if(d.type!=='in')return;// Gửi NGAY, không pace setTimeout: input pacing = đồng hồ audio worklet
|
||||
// (1 frame/21.33ms), không phải timer main-thread. Timer bận/trễ → input
|
||||
// gửi trễ → engine/bridge chỉ sản xuất output khi có input → cushion cạn
|
||||
@@ -363,7 +369,7 @@ fxRtSendFrames(d);};// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn
|
||||
try{masterBus.outputAnalyser.disconnect(masterBus.output);}catch(e){}masterBus.outputAnalyser.connect(fxRt.node);fxRt.node.connect(masterBus.masterFxGain);fxRt.chainKey=ck;}catch(e){console.warn('[FX RT] setup fail:',e);fxRtStop(true);}}function fxRtStop(silent){if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}// 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){_fxStopNode=fxRt.node;if(masterBus&&masterBus.masterFxGain&&masterBus.masterFxDryGain){// Crossfade deactivation: ramp dry 0->1 / fx 1->0 roi MOI disconnect
|
||||
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{if(fxRt.node){_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).
|
||||
@@ -1826,13 +1832,16 @@ fxRtTrackReconcile(trackId,node,track);}catch(e){console.warn('rebuildTrackFxGra
|
||||
// Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart):
|
||||
// fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice
|
||||
// worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild).
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)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:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){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};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608312300`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu
|
||||
fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}catch(e){}};ws.onmessage=ev=>{if(!(ev.data instanceof ArrayBuffer)||!entry.node)return;const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}entry.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};entry.node.port.onmessage=ev=>{const d=ev.data;if(!d||!entry.ws)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;}if(d.type!=='in')return;// Burst 4 block (1024 mẫu) → tách 4 WS frame 512 floats → bridge batch 4.
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)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:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){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,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{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);const wsProto=u.protocol==='https:'?'wss://':'ws://';const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu
|
||||
fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}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);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}entry.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};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;}if(d.type!=='in')return;// Burst 4 block (1024 mẫu) → tách 4 WS frame 512 floats → bridge batch 4.
|
||||
// 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){}}}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);fxRtTrackStop(trackId,path,true);}};const fxRtTrackUnsplice=(trackId,path)=>{const entry=fxRtTracks[trackId+':'+path];if(!entry||!entry.spliced||!entry.node)return;try{entry.node.disconnect();try{entry.spliced.tail.connect(entry.spliced.dest);}catch(e){}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:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});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];const node=activeTrackNodesRef.current[trackId];if(!entry||!entry.ready||!entry.node||!node||entry.spliced)return;const tail=path==='audio'?node.fxEntry:node.sfEntry;const dest=path==='audio'?node.fxLegacyIn:node.sfOut;if(!tail||!dest)return;try{tail.disconnect(dest);entry.node.connect(dest);tail.connect(entry.node);entry.spliced={tail,dest};console.log('[FX RT track] spliced',trackId,path);}catch(e){console.warn('[FX RT track] splice fail',trackId,path,e);}};const fxRtTrackStop=(trackId,path,silent)=>{const key=trackId+':'+path;const entry=fxRtTracks[key];if(!entry)return;if(entry.pumpTimer){clearInterval(entry.pumpTimer);entry.pumpTimer=null;}entry.inQueue=[];try{fxRtTrackUnsplice(trackId,path);if(entry.node){entry.node.port.onmessage=null;try{entry.node.disconnect();}catch(e){}entry.node=null;}}catch(e){}try{if(entry.ws){entry.ws.onclose=null;entry.ws.onmessage=null;try{entry.ws.close();}catch(e){}entry.ws=null;}}catch(e){}const sid=entry.sessionId;delete fxRtTracks[key];fxRtGuiPushShm();if(sid){try{window.SonicAPI.fxRealtimeStop({session_id:sid}).catch(()=>{});}catch(e){}}};const fxRtTrackReconcile=(trackId,node,track)=>{try{const chain=fxRtTrackChain(track);const chainKey=fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0');const wantPaths=chain.length?fxRtTrackPaths(track):[];const prefix=trackId+':';Object.keys(fxRtTracks).forEach(k=>{if(!k.startsWith(prefix))return;const p=k.slice(prefix.length);if(wantPaths.indexOf(p)<0)fxRtTrackStop(trackId,p,true);});if(!isPlayingRef.current)return;wantPaths.forEach(p=>{const key=trackId+':'+p;const entry=fxRtTracks[key];if(!entry)fxRtTrackStart(trackId,track,p);else if(entry.chainKey!==chainKey){// Chain đổi (load/đổi VST FX, preset GUI, bypass slot) — session
|
||||
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:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});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];const node=activeTrackNodesRef.current[trackId];if(!entry||!entry.ready||!entry.node||!node||entry.spliced)return;const tail=path==='audio'?node.fxEntry:node.sfEntry;const dest=path==='audio'?node.fxLegacyIn:node.sfOut;if(!tail||!dest)return;try{tail.disconnect(dest);entry.node.connect(dest);tail.connect(entry.node);entry.spliced={tail,dest};console.log('[FX RT track] spliced',trackId,path);}catch(e){console.warn('[FX RT track] splice fail',trackId,path,e);}};const fxRtTrackStop=(trackId,path,silent)=>{const key=trackId+':'+path;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;}}catch(e){}try{if(entry.ws){entry.ws.onclose=null;entry.ws.onmessage=null;try{entry.ws.close();}catch(e){}entry.ws=null;}}catch(e){}const sid=entry.sessionId;delete fxRtTracks[key];fxRtGuiPushShm();if(sid){try{window.SonicAPI.fxRealtimeStop({session_id:sid}).catch(()=>{});}catch(e){}}};const fxRtTrackReconcile=(trackId,node,track)=>{try{const chain=fxRtTrackChain(track);const chainKey=fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0');const wantPaths=chain.length?fxRtTrackPaths(track):[];const prefix=trackId+':';Object.keys(fxRtTracks).forEach(k=>{if(!k.startsWith(prefix))return;const p=k.slice(prefix.length);if(wantPaths.indexOf(p)<0)fxRtTrackStop(trackId,p,true);});if(!isPlayingRef.current)return;wantPaths.forEach(p=>{const key=trackId+':'+p;const entry=fxRtTracks[key];if(!entry)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.
|
||||
@@ -2002,7 +2011,9 @@ setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (
|
||||
if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
|
||||
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;if(window.SonicSF){scheduleMidiNoteDispatch(subTrack,note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);scheduleNoteExpressionCC11(subTrack,note,subCh,context.currentTime,0,remainingDurMs/1000);}if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
|
||||
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
|
||||
const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();// V12: parity với startTrackPlayback — loop restart (solo/local) sau
|
||||
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
|
||||
// isPlayingRef NGAY (guardPlay race — xem startTrackPlayback).
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// ~5.5ms/call > budget 5.33ms → 179/s < 187.5/s → engine drop input →
|
||||
// crackle). Gửi burst 4 block → bridge take=4 → 1 call process 1024 mẫu
|
||||
// (amortize overhead: 14-19ms/4 block ≈ 3.5-4.75ms/block) → đủ 187.5/s.
|
||||
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
|
||||
// Latency thêm ~4 block (~21ms) — worklet outQueue 64 block hấp thụ.
|
||||
//
|
||||
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
|
||||
// ≥ FILL (24 block ~128ms @48k) rồi mới phát. Engine gửi ASAP (không pace
|
||||
@@ -22,14 +22,14 @@
|
||||
// NGAY khi có block mới (không đợi đủ FILL): đã im lặng → fade-in từ 0, đang
|
||||
// giữa fade → crossfade 64 mẫu từ mẫu hiện tại (hết jump). Cap-drop (queue
|
||||
// tràn sau stall) crossfade 64 mẫu (hết click) — giống bridgeAudioWorklet.
|
||||
// Cap 48 block (~256ms) chặn overflow khi bridge nhanh hơn realtime.
|
||||
// Cap 64 block (~341ms) chặn overflow khi bridge nhanh hơn realtime.
|
||||
// ponytail: thay fade-out bằng adaptive stretch khi cần chất lượng cao hơn.
|
||||
class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.BLOCK = 256; // FXRT_BLOCK
|
||||
this.BURST = 4; // số block gom 1 frame (bridge batch)
|
||||
this.FILL = 48; // 48 block ~256ms: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, VST panel)
|
||||
this.FILL = 24; // 24 block ~128ms @48k: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, VST panel). PDC 7936 = (7+24)*256 (fxRtApplyPdc).
|
||||
this.CAP = 64; // cap queue chặn overflow (~341ms)
|
||||
this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng)
|
||||
this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun
|
||||
@@ -42,6 +42,8 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
this.outPos = 0;
|
||||
this.playing = false; // false → im lặng (đang chờ đủ FILL)
|
||||
this.hasStarted = false; // đã phát block đầu chưa — FILL chỉ chờ ở lần khởi đầu
|
||||
this.flushPending = false; // V24: đang chờ engine flush (resume sau underrun) — drop block stale
|
||||
this.resumeFadeIn = false; // V24: interruption đã xử lí (clear+flush) — resume sẽ fade-in từ 0
|
||||
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
|
||||
@@ -55,7 +57,12 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
this.port.onmessage = (e) => {
|
||||
const d = e.data;
|
||||
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
|
||||
if (d && d.type === 'flush_done') { this.flushPending = false; return; }
|
||||
if (!d || d.type !== 'out' || !d.l || !d.r) return;
|
||||
// V24: đang chờ flush (resume sau underrun/stall) → drop block stale.
|
||||
// Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn
|
||||
// ~1s) — chỉ nhận block MỚI từ sau flush_done.
|
||||
if (this.flushPending) return;
|
||||
this.outQueue.push({ l: d.l, r: d.r });
|
||||
while (this.outQueue.length > this.CAP) {
|
||||
this.outQueue.shift();
|
||||
@@ -162,15 +169,32 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
// Không phát: resume khi đủ block (lần đầu chờ FILL; sau đó chỉ cần 1
|
||||
// block — hold che khoảng chờ).
|
||||
if (this.outQueue.length >= (this.hasStarted ? 1 : this.FILL)) {
|
||||
// V24: interruption (underrun sau stall) — backlog trong queue là audio
|
||||
// LẠC HẬU (chơi lại → lag cộng dồn ~1s). KHÔNG chơi backlog: clear
|
||||
// queue + báo main thread flush (engine xóa input/output ring, main
|
||||
// drop output stale in-flight) → chờ flush_done → resume chỉ với block
|
||||
// MỚI. flushPending → onmessage drop block stale trong lúc chờ (hết
|
||||
// race ack). Fade-out đang chạy (nếu có) tiếp tục tới im lặng — hết
|
||||
// click. resumeFadeIn guard chống re-trigger flush khi ack tới mà fade
|
||||
// chưa xong. resume → fade-in từ 0 (đã im) hoặc crossfade từ mẫu hiện
|
||||
// tại (giữa fade) — hết jump.
|
||||
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
|
||||
if (wasInterrupted) {
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.flushPending = true;
|
||||
this.resumeFadeIn = true;
|
||||
this.port.postMessage({ type: 'flush' });
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
this.playing = true;
|
||||
this.outPos = 0;
|
||||
const wasInterrupted = (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
|
||||
this.holdFadeLeft = 0;
|
||||
if (wasInterrupted) {
|
||||
if (this.resumeFadeIn) {
|
||||
this.resumeFadeIn = false;
|
||||
if (this.silenced) {
|
||||
// Đã im lặng (fade xong) → fade-in từ 0 (hết jump ~0→1).
|
||||
// Dùng flag silenced (không đo lastOut: mẫu cuối fade ≈ fadeFrom/1024
|
||||
// ≈ 1e-3, ngưỡng số dễ sai).
|
||||
// Đã im lặng (fade xong / sau flush) → fade-in từ 0 (hết jump ~0→1).
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
this.statFadeIn++;
|
||||
|
||||
Reference in New Issue
Block a user