V45.14: diag preview_audio - dem bridge-audio frames 500ms sau note-on (tach engine render vs JS routing) v=202608311112

This commit is contained in:
2026-08-31 11:11:47 +07:00
parent ad71ac1168
commit 752db690b3
3 changed files with 21 additions and 5 deletions
+14
View File
@@ -2379,6 +2379,17 @@ function getAudioContext() {
_dbgLog('play_first_audible', 'delta_ms=' + Math.round(performance.now() - _dbgPlayStartMs));
}
}
try {
if (window.__dbgPreviewAudioArmed) {
const _p = window.__dbgPreviewAudioArmed;
_p.count++;
if (l && r) { for (var _j = 0; _j < l.length; _j++) { if (l[_j] !== 0 || r[_j] !== 0) { _p.nz++; break; } } }
if (performance.now() - _p.t0 > 500) {
_dbgLog('preview_audio', 'frames=' + _p.count + ' nz=' + _p.nz);
window.__dbgPreviewAudioArmed = null;
}
}
} catch (e) {}
window.BridgeAudioNode.onAudio(l, r);
} catch (e) {
console.warn('[Bridge] audio callback error:', e && e.message || e);
@@ -23821,6 +23832,9 @@ const App = () => {
const _pdSub = subTabsRef.current ? (subTabsRef.current.find(s => s.id === _pdTab) || {}).type : null;
_dbgLog('preview_diag', 'armed=' + armed.length + ' tab=' + _pdTab + ' sub=' + (_pdSub || 'none') + ' play=' + (isPlayingRef.current ? 1 : 0) + ' br=' + (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() ? 1 : 0) + ' fxRt=' + (fxRt.active ? 1 : 0) + ' dest=' + _pdDest + ' bd=' + (_pdD ? ('i=' + _pdD.initialized + ' wl=' + _pdD.useWorklet + ' nd=' + _pdD.hasWorkletNode + ' pre=' + _pdD.preBufferLen + ' ch=' + _pdD.chunksLen + ' g=' + _pdD.gain) : 'MISSING') + ' mb=' + (!!masterBus));
} catch (e) {}
// DIAG Bug 3/4: arm bo dem audio frames (500ms sau note-on) de biet
// engine co gui audio ve JS khong (tach engine vs JS routing).
try { window.__dbgPreviewAudioArmed = { count: 0, nz: 0, t0: performance.now() }; } catch (e) {}
activeMidiPitchesRef.current.add(pitch);
setActiveMidiPitches(new Set(activeMidiPitchesRef.current));
vkbHeldRef.current.add(pitch);
+6 -4
View File
@@ -339,7 +339,7 @@ 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=202608311101`);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://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
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=202608311112`);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://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
// co listener IPv6 -> SYN hang ~2s) roi moi fallback 127.0.0.1. Dung
// thang 127.0.0.1 (loopback) -> WS open ~5ms. Server chap nhan loopback.
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${wsHost}${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
@@ -460,7 +460,7 @@ if(window.NativeBridgeService&&window.SonicMidiRouter&&!window.__bridgeBootstrap
// luôn kill+respawn → first play sau launch im lặng ~0.5s (chờ bridge
// respawn + restore instrument từ bridge_state.json).
if(st.sample_rate&&st.sample_rate!==audioCtx.sampleRate){try{console.log('[Bridge] Requesting restart to match sampleRate:',audioCtx.sampleRate,'(bridge was',st.sample_rate+')');await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate',{sampleRate:audioCtx.sampleRate});// Bridge restart → mất hết instrument → reset map để reconnect reload.
window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}}else{console.log('[Bridge] sampleRate match — skip restart:',audioCtx.sampleRate);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){try{if(_dbgFirstAudioPending){_dbgFirstAudioPending=false;_dbgLog('play_first_audio','delta_ms='+Math.round(performance.now()-_dbgPlayStartMs));}if(_dbgFirstAudiblePending&&l&&r){var _nz=false;for(var _i=0;_i<l.length;_i++){if(l[_i]!==0||r[_i]!==0){_nz=true;break;}}if(_nz){_dbgFirstAudiblePending=false;_dbgLog('play_first_audible','delta_ms='+Math.round(performance.now()-_dbgPlayStartMs));}}window.BridgeAudioNode.onAudio(l,r);}catch(e){console.warn('[Bridge] audio callback error:',e&&e.message||e);}});// A14: plugin MIDI output -> route notes to the selected track
window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}}else{console.log('[Bridge] sampleRate match — skip restart:',audioCtx.sampleRate);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){try{if(_dbgFirstAudioPending){_dbgFirstAudioPending=false;_dbgLog('play_first_audio','delta_ms='+Math.round(performance.now()-_dbgPlayStartMs));}if(_dbgFirstAudiblePending&&l&&r){var _nz=false;for(var _i=0;_i<l.length;_i++){if(l[_i]!==0||r[_i]!==0){_nz=true;break;}}if(_nz){_dbgFirstAudiblePending=false;_dbgLog('play_first_audible','delta_ms='+Math.round(performance.now()-_dbgPlayStartMs));}}try{if(window.__dbgPreviewAudioArmed){const _p=window.__dbgPreviewAudioArmed;_p.count++;if(l&&r){for(var _j=0;_j<l.length;_j++){if(l[_j]!==0||r[_j]!==0){_p.nz++;break;}}}if(performance.now()-_p.t0>500){_dbgLog('preview_audio','frames='+_p.count+' nz='+_p.nz);window.__dbgPreviewAudioArmed=null;}}}catch(e){}window.BridgeAudioNode.onAudio(l,r);}catch(e){console.warn('[Bridge] audio callback error:',e&&e.message||e);}});// A14: plugin MIDI output -> route notes to the selected track
// (Scaler2/Instacomposer chords play the selected track's
// instrument). Feedback guard: skip events whose source channel
// is the selected track's own channel (would loop generator into
@@ -1762,7 +1762,9 @@ const showToast=(text,type='info',actionText=null,onActionClick=null)=>{if(toast
// per-track channel alloc; native SF/VST; SonicSF fallback; VU + giữ
// counter (heldMidiNotesRef); forward recorder (live record vào midiItems).
const vkbNoteOn=(pitch,vel)=>{try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}const allTracks=activeTracksRef.current||[];const armed=allTracks.filter(function(t){return t.isArmed;});const targets=armed.length?armed:allTracks;if(!armed.length&&!window.__vkbNoArmToast){window.__vkbNoArmToast=true;showToast('Không có track nào ARM — chơi vào tất cả track.','info');}// DIAG Bug 3/4 (preview khong am): trang thai routing tai note-on.
try{let _pdDest='none';if(window.AudioRoutingEngine&&window.AudioRoutingEngine._connected){_pdDest=window.AudioRoutingEngine._dest&&window.AudioRoutingEngine._dest.constructor?window.AudioRoutingEngine._dest.constructor.name:'null';_pdDest+='/'+window.AudioRoutingEngine._trackId;}else{_pdDest='NOT_CONNECTED';}const _pdD=window.BridgeAudioNode?window.BridgeAudioNode.getDiag():null;const _pdTab=activeTabRef.current;const _pdSub=subTabsRef.current?(subTabsRef.current.find(s=>s.id===_pdTab)||{}).type:null;_dbgLog('preview_diag','armed='+armed.length+' tab='+_pdTab+' sub='+(_pdSub||'none')+' play='+(isPlayingRef.current?1:0)+' br='+(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()?1:0)+' fxRt='+(fxRt.active?1:0)+' dest='+_pdDest+' bd='+(_pdD?'i='+_pdD.initialized+' wl='+_pdD.useWorklet+' nd='+_pdD.hasWorkletNode+' pre='+_pdD.preBufferLen+' ch='+_pdD.chunksLen+' g='+_pdD.gain:'MISSING')+' mb='+!!masterBus);}catch(e){}activeMidiPitchesRef.current.add(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));vkbHeldRef.current.add(pitch);targets.forEach(function(t){const ctx=resolveTrackInstrumentCtx(t,allTracks);ensureSonicInstrument(ctx);if(window.triggerMidiVuActivity)window.triggerMidiVuActivity(t.id,Math.round(vel*127));try{heldMidiNotesRef.current[t.id]=(heldMidiNotesRef.current[t.id]||0)+1;}catch(err){}const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(t.id,ctx.program,ctx.synthEngine);}catch(er){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:t.id,pitch:pitch,velocity:vel,percussion:percussion,live:true});}catch(e){}}else if(isStandaloneSf()&&(isSfTrackEngine(ctx.synthEngine)||isVstTrackEngine(ctx.synthEngine))){if(isSfTrackEngine(ctx.synthEngine))playNativeSfNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);else playNativeVstNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);}else if(window.SonicSF){const dest=activeTrackNodesRef.current[t.id]&&activeTrackNodesRef.current[t.id].gainNode||null;window.SonicSF.playNote(pitch,Math.max(1,Math.round(vel*127)),60000,undefined,ctx.program,dest,ctx.ch,ctx.synthEngine);}});// Live record: forward tới mọi recorder đang chạy (timeline take / piano roll)
try{let _pdDest='none';if(window.AudioRoutingEngine&&window.AudioRoutingEngine._connected){_pdDest=window.AudioRoutingEngine._dest&&window.AudioRoutingEngine._dest.constructor?window.AudioRoutingEngine._dest.constructor.name:'null';_pdDest+='/'+window.AudioRoutingEngine._trackId;}else{_pdDest='NOT_CONNECTED';}const _pdD=window.BridgeAudioNode?window.BridgeAudioNode.getDiag():null;const _pdTab=activeTabRef.current;const _pdSub=subTabsRef.current?(subTabsRef.current.find(s=>s.id===_pdTab)||{}).type:null;_dbgLog('preview_diag','armed='+armed.length+' tab='+_pdTab+' sub='+(_pdSub||'none')+' play='+(isPlayingRef.current?1:0)+' br='+(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()?1:0)+' fxRt='+(fxRt.active?1:0)+' dest='+_pdDest+' bd='+(_pdD?'i='+_pdD.initialized+' wl='+_pdD.useWorklet+' nd='+_pdD.hasWorkletNode+' pre='+_pdD.preBufferLen+' ch='+_pdD.chunksLen+' g='+_pdD.gain:'MISSING')+' mb='+!!masterBus);}catch(e){}// DIAG Bug 3/4: arm bo dem audio frames (500ms sau note-on) de biet
// engine co gui audio ve JS khong (tach engine vs JS routing).
try{window.__dbgPreviewAudioArmed={count:0,nz:0,t0:performance.now()};}catch(e){}activeMidiPitchesRef.current.add(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));vkbHeldRef.current.add(pitch);targets.forEach(function(t){const ctx=resolveTrackInstrumentCtx(t,allTracks);ensureSonicInstrument(ctx);if(window.triggerMidiVuActivity)window.triggerMidiVuActivity(t.id,Math.round(vel*127));try{heldMidiNotesRef.current[t.id]=(heldMidiNotesRef.current[t.id]||0)+1;}catch(err){}const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(t.id,ctx.program,ctx.synthEngine);}catch(er){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:t.id,pitch:pitch,velocity:vel,percussion:percussion,live:true});}catch(e){}}else if(isStandaloneSf()&&(isSfTrackEngine(ctx.synthEngine)||isVstTrackEngine(ctx.synthEngine))){if(isSfTrackEngine(ctx.synthEngine))playNativeSfNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);else playNativeVstNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);}else if(window.SonicSF){const dest=activeTrackNodesRef.current[t.id]&&activeTrackNodesRef.current[t.id].gainNode||null;window.SonicSF.playNote(pitch,Math.max(1,Math.round(vel*127)),60000,undefined,ctx.program,dest,ctx.ch,ctx.synthEngine);}});// Live record: forward tới mọi recorder đang chạy (timeline take / piano roll)
if(activeMIDIRecordersRef.current){const msg={data:[0x90,pitch,Math.max(1,Math.round(vel*127))]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};const vkbNoteOff=pitch=>{vkbHeldRef.current.delete(pitch);activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const allTracks=activeTracksRef.current||[];allTracks.forEach(function(t){const ctx=resolveTrackInstrumentCtx(t,allTracks);const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:pitch,velocity:0,percussion:percussion,live:true});}catch(e){}}else if(window.SonicSF&&window.SonicSF.stopNote){if(isStandaloneSf()){stopNativeSfNote('vkb_'+t.id+'_'+pitch);stopNativeVstNote('vkb_'+t.id+'_'+pitch);}window.SonicSF.stopNote(ctx.ch,pitch);}try{if(heldMidiNotesRef.current[t.id]!==undefined){heldMidiNotesRef.current[t.id]=Math.max(0,heldMidiNotesRef.current[t.id]-1);if(heldMidiNotesRef.current[t.id]===0)delete heldMidiNotesRef.current[t.id];}}catch(err){}});if(activeMIDIRecordersRef.current){const msg={data:[0x80,pitch,0]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};vkbNoteOnRef.current=vkbNoteOn;vkbNoteOffRef.current=vkbNoteOff;window.__toggleVkb=function(){setVkbOpen(function(p){const next=!p;if(!next&&vkbHeldRef.current.size){Array.from(vkbHeldRef.current).forEach(function(pitch){try{vkbNoteOffRef.current(pitch);}catch(e){}});}return next;});};window.__toggleChordsPanel=function(){setChordsPanelOpen(function(p){return!p;});};// ── Server-side upload ──
// WAV encoder tối thiểu — upload buffer-only clip khi LƯU (nếu chưa có
// serverFileId — upload sớm thất bại/race → clip mất sau reload).
@@ -1945,7 +1947,7 @@ 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,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311101`);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://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
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=202608311112`);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://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${wsHost}${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