diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 0bb240d..dc6f651 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -2149,29 +2149,6 @@ async function fxRtStart() { fxRt.inQueue = []; fxRt.outL = null; fxRt.outR = null; - // M26b diag: PWR ON wake — snap pdcDelay/gains + hoi worklet state luc - // wake, luc 'started' (wet bat dau) + 180/400/800ms sau: do wet-lag that, - // delay node co ve 0 truoc crossfade khong. Bo diag o build fix. - let _m26onWlArmed = false; - const _snapOn = (tag, _sched) => { - let _pd='na',_gd='na',_gw='na',_ps='na',_pa='na'; - const _b=masterBus, _c=getAudioContext(); - try { if(_b&&_b.pdcDelay) _pd=(_b.pdcDelay.delayTime.value*1000).toFixed(1); } catch(e){} - try { if(_b&&_b.masterFxDryGain) _gd=_b.masterFxDryGain.gain.value.toFixed(4); } catch(e){} - try { if(_b&&_b.masterFxGain) _gw=_b.masterFxGain.gain.value.toFixed(4); } catch(e){} - try { _ps=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na'; } catch(e){} - try { _pa=typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na'; } catch(e){} - _dbgLog(tag,'pdcMs='+_pd+' gDry='+_gd+' gWet='+_gw+' lat='+(fxRt.latencyTotal||0)+' ps='+_ps+' pa='+_pa+' act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' faded='+(fxRt.fadedIn?1:0)+' ctx='+(_c?_c.currentTime.toFixed(3):'na')); - try { if (fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'dbg_state', k: tag }); } catch(e){} - if (_sched && _sched.length) _sched.forEach((_ms,_ix)=>setTimeout(()=>_snapOn('ph_on'+( _ix+1)),_ms)); - }; - try { if (fxRt.node && fxRt.node.port && !_m26onWlArmed) { - _m26onWlArmed = true; - const _wl = (ev) => { try { const d = ev.data; if (d && d.type === 'dbg_state_r') _dbgLog('ph_on_wl','k='+d.k+' t='+d.t+' q='+d.q+' play='+d.playing+' hs='+d.hasStarted+' sil='+d.silenced+' fp='+d.flushPending+' stp='+d.stopped+' wm='+d.wrapMode+' wfb='+d.wrapFadeBlocks+' lat='+d.latencySamples+' rfill='+d.resumeFill+' inC='+d.inCount+' outPos='+d.outPos+' fadeL='+d.fadeLeft+' holdL='+d.holdFadeLeft); } catch(e){} }; - fxRt.node.port.addEventListener('message', _wl); - setTimeout(()=>{ try { if (fxRt.node && fxRt.node.port) fxRt.node.port.removeEventListener('message', _wl); } catch(e){} }, 2600); - } } catch(e){} - _snapOn('ph_on_a', null); // M19 Bug A fix: wake PWR ON sau warm-bypass. Trước đây chỉ flush engine // ring + fadedIn=true NGAY — nhưng worklet lúc bypass rời graph không // process: outQueue còn ~FILL block audio CŨ + hasStarted=true (không gửi @@ -2192,7 +2169,6 @@ async function fxRtStart() { try { if (ev.data && ev.data.type === 'started' && wToken === fxRt.startToken) { fxRt.fadedIn = true; - try { _snapOn('ph_on_s', [180, 400, 800]); } catch (e) {} fxRtCrossfade(true, wToken); setTimeout(() => { if (wToken === fxRt.startToken) fxRtApplyPdc(); }, 250); } @@ -2658,13 +2634,15 @@ async function fxRtEnsureWarmForGui(path) { // Master chain (worklet → bridge VST3) trễ ~2 block round-trip + latency // plugin. Track bypass mastering đi dry path (dryInput→dryOutput→output) KHÔNG // qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng -// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật -// (tag seq trên block) khi cần sample-accurate. -// M18: FILL 40 (sf-fx-realtime.js V48) → RTT = batching 4 block (~7 block @48k -// ≈ 1792) + FILL buffer 40 block (10240) = 47 block ≈ 12032 (~251ms @48k). Giữ -// đồng bộ với sf-fx-realtime.js. FX_RT_START_EST_MS: ước lượng khởi đầu khi -// chưa có phép đo (fx_play_first) — dùng cho freeze playhead thay V39 0.31s. -const FX_RT_ROUNDTRIP_SAMPLES = 12032; +// de align. M26: pdc = wet-lag FILL - content nghe = S(t-FILL); M26b do onset +// +208-221ms = FILL 40 block (10240 @48k ~213ms) + RTT nho. KHONG tin VST +// report (Ozone bao 17536 ~365ms nhung content-latency that ~0). ponytail: +// neu plugin content-latency that L>0 -> pdc = FILL + L (display-lat giu FILL +// vi processed da gom L). +// Dong bo voi sf-fx-realtime.js: FILL 40 block (10240 @48k). FX_RT_START_EST_MS: +// uoc luong khoi dau khi chua co phep do (fx_play_first) - dung cho freeze +// playhead thay V39 0.31s. +const FX_RT_WET_LAG_SAMPLES = 10240; const FX_RT_START_EST_MS = 450; // G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr // (da tru latency -> bam vi tri mau NGHE DUOC; pre-roll tu giu tai startOffset — @@ -2687,11 +2665,10 @@ function fxRtApplyPdc() { const ctx = getAudioContext(); const bus = masterBus; if (!ctx || !bus || !bus.pdcDelay) return; - // M23 Bug C: bo +FX_RT_ROUNDTRIP_SAMPLES — latencyTotal (bridge 'latency' - // frame) DA gom round-trip that (log M21/M22: display active = wall - lat). - // +12032 double-count -> dry path delay > wet lag ~250ms -> crossfade lech pha - // (scratch). ponytail: neu engine pipeline doi RTT that -> do lai o day. - const l = (fxRt && fxRt.active) ? (fxRt.latencyTotal || 0) : 0; + // M26: pdc = wet-lag FILL 10240 (~213ms) - content nghe = S(t-FILL); VST + // report (17536) la plugin buffer, content-latency that ~0 (M26b do onset). + // ponytail: neu content-latency that L>0 -> FX_RT_WET_LAG_SAMPLES + L. + const l = (fxRt && fxRt.active) ? FX_RT_WET_LAG_SAMPLES : 0; try { bus.pdcDelay.delayTime.setTargetAtTime(l / (ctx.sampleRate || 44100), ctx.currentTime, 0.05); } catch (e) {} } @@ -21022,10 +20999,10 @@ const App = () => { const _off2 = startOffsetTimeRef.current || 0; const _at2 = startAudioTimeRef.current || _c2.currentTime; const _eng2 = (typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) - ? Math.max(_off2, _off2 + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / _sr2) + ? Math.max(_off2, _off2 + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / _sr2) : null; const _wall2 = _off2 + (_c2.currentTime - _at2); - const _content2 = (_eng2 !== null) ? _eng2 : (_wall2 - (fxRt.latencyTotal || 0) / _sr2); + const _content2 = (_eng2 !== null) ? _eng2 : (_wall2 - FX_RT_WET_LAG_SAMPLES / _sr2); startOffsetTimeRef.current = _content2; startAudioTimeRef.current = _c2.currentTime; _dbgLog('ph_offhold', 'cur=' + (currentTimeRef.current == null ? 'na' : currentTimeRef.current.toFixed(3)) + ' wall=' + _wall2.toFixed(3) + ' content=' + _content2.toFixed(3) + ' off=' + startOffsetTimeRef.current.toFixed(3) + ' at=' + startAudioTimeRef.current.toFixed(3) + ' ctx=' + _c2.currentTime.toFixed(3) + ' lat=' + (fxRt.latencyTotal || 0) + ' hold=1'); @@ -21034,38 +21011,6 @@ const App = () => { } } catch (e) { try { _dbgLog('ph_offhold', 'err ' + String(e)); } catch (e2) {} } fxRtWarmBypass(); - // M26 diag: PWR OFF mid-play — snap pdcDelay/gains + hoi worklet - // state tai t0/+180/+400/+800ms de do: pdc thuc te khi dry mo, - // wet tail song bao lau, gains ramp ra sao. Bo diag o build fix. - if (_plNow) { - const _busB = masterBus; - const _ctxB = getAudioContext(); - const _snapB = (k) => { - let _pdB = 'na', _gdB = 'na', _gwB = 'na', _psB = 'na', _paB = 'na'; - try { if (_busB && _busB.pdcDelay) _pdB = (_busB.pdcDelay.delayTime.value * 1000).toFixed(1); } catch (e) {} - try { if (_busB && _busB.masterFxDryGain) _gdB = _busB.masterFxDryGain.gain.value.toFixed(4); } catch (e) {} - try { if (_busB && _busB.masterFxGain) _gwB = _busB.masterFxGain.gain.value.toFixed(4); } catch (e) {} - try { _psB = typeof fxRt.processedSamples === 'number' ? fxRt.processedSamples : 'na'; } catch (e) {} - try { _paB = typeof fxRt.processedAnchor === 'number' ? fxRt.processedAnchor : 'na'; } catch (e) {} - const _oB = startOffsetTimeRef.current || 0; - const _aB = startAudioTimeRef.current || 0; - const _tB = _ctxB ? _ctxB.currentTime : 0; - _dbgLog('ph_echo' + k, 'pdcMs=' + _pdB + ' gDry=' + _gdB + ' gWet=' + _gwB + ' lat=' + (fxRt.latencyTotal || 0) + ' ps=' + _psB + ' pa=' + _paB + ' cur=' + (currentTimeRef.current == null ? 'na' : currentTimeRef.current.toFixed(3)) + ' off=' + _oB.toFixed(3) + ' wall=' + (_oB + _tB - _aB).toFixed(3) + ' ctx=' + _tB.toFixed(3)); - try { if (fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'dbg_state', k: k }); } catch (e) {} - }; - const _wlB = (ev) => { - try { - const d = ev.data; - if (d && d.type === 'dbg_state_r') { - _dbgLog('ph_echo_wl', 'k=' + d.k + ' t=' + d.t + ' q=' + d.q + ' play=' + d.playing + ' hs=' + d.hasStarted + ' sil=' + d.silenced + ' fp=' + d.flushPending + ' stp=' + d.stopped + ' wm=' + d.wrapMode + ' wfb=' + d.wrapFadeBlocks + ' lat=' + d.latencySamples + ' rfill=' + d.resumeFill + ' inC=' + d.inCount + ' outPos=' + d.outPos + ' fadeL=' + d.fadeLeft + ' holdL=' + d.holdFadeLeft); - } - } catch (e) {} - }; - try { if (fxRt.node && fxRt.node.port) fxRt.node.port.addEventListener('message', _wlB); } catch (e) {} - _snapB(0); - [180, 400, 800].forEach((_msB, _iB) => setTimeout(() => _snapB(_iB + 1), _msB)); - setTimeout(() => { try { if (fxRt.node && fxRt.node.port) fxRt.node.port.removeEventListener('message', _wlB); } catch (e) {} }, 1600); - } } } else { fxRtStop(); @@ -22090,7 +22035,7 @@ const App = () => { const _contentNow = (startOffsetTimeRef.current || 0) + (_c.currentTime - (startAudioTimeRef.current || _c.currentTime)); startOffsetTimeRef.current = _contentNow; startAudioTimeRef.current = _c.currentTime; - if (_pa !== null) fxRt.processedAnchor = _pa - Math.round(fxRt.latencyTotal || 0); + if (_pa !== null) fxRt.processedAnchor = _pa - FX_RT_WET_LAG_SAMPLES; fxRtWarmPlayHold = false; } else { // (M22) rebase wall — khi khong OFF-giua-play (warm tu stop), off/at la @@ -25268,7 +25213,7 @@ const App = () => { if (fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) { _posSamples = Math.max( Math.floor(startOffsetTimeRef.current * (_pctx.sampleRate || 44100)), - Math.floor(startOffsetTimeRef.current * (_pctx.sampleRate || 44100) + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0))) + Math.floor(startOffsetTimeRef.current * (_pctx.sampleRate || 44100) + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES)) ); } else { const _elapsed = _pctx.currentTime - (startAudioTimeRef.current || _pctx.currentTime); @@ -25534,7 +25479,7 @@ const App = () => { const context = getAudioContext(); let _ph = startOffsetTimeRef.current + (context.currentTime - startAudioTimeRef.current); if (fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) { - _ph = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / (context.sampleRate || 44100); + _ph = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / (context.sampleRate || 44100); } let _local = _ph; const _itemInfo = getPianoRollItemInfo(st); @@ -25562,7 +25507,7 @@ const App = () => { let wallTime = startOffsetTimeRef.current + elapsed; const bufferPos = startBufferOffsetRef.current + elapsed * speedFactor; if (fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) { - wallTime = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / (context.sampleRate || 44100); + wallTime = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / (context.sampleRate || 44100); } // Loop sub-tab selection (bufferPos is buffer-time) @@ -25640,7 +25585,7 @@ const App = () => { const _playheadSec = (_fxRtSeekFreeze && fxRt && fxRt.active) ? startOffsetTimeRef.current : ((fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) - ? Math.max(startOffsetTimeRef.current, startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / (context.sampleRate || 44100)) + ? Math.max(startOffsetTimeRef.current, startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / (context.sampleRate || 44100)) : updatedTime); // Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2 diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 8f6bc7e..dab7233 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -390,7 +390,7 @@ else if(data instanceof ArrayBuffer)this._worker.postMessage({kind:'send',buf:da // SecurityError -> fxRtStart chet -> master FX khong hoat dong (M24 dung // new WebSocket, khong bi same-origin). Fetch text qua engine (CORS * da bat) // -> Blob URL same-origin -> worker chay duoc ca Tauri lan http dev. -try{if(!fxRtWorkerUrl){const _r=await fetch(`${API_BASE_URL}/static/js/fx-rt-worker.js?v=202609071745`,{cache:'no-store'});if(!_r.ok)throw new Error('fx-rt-worker load '+_r.status);fxRtWorkerUrl=URL.createObjectURL(new Blob([await _r.text()],{type:'text/javascript'}));}shim._worker=new Worker(fxRtWorkerUrl);}catch(e){throw e;}// giong ws constructor that -> caller catch cu giu nguyen +try{if(!fxRtWorkerUrl){const _r=await fetch(`${API_BASE_URL}/static/js/fx-rt-worker.js?v=202609071819`,{cache:'no-store'});if(!_r.ok)throw new Error('fx-rt-worker load '+_r.status);fxRtWorkerUrl=URL.createObjectURL(new Blob([await _r.text()],{type:'text/javascript'}));}shim._worker=new Worker(fxRtWorkerUrl);}catch(e){throw e;}// giong ws constructor that -> caller catch cu giu nguyen shim._worker.onmessage=ev=>{const d=ev.data;if(!d||shim._closed)return;if(d.kind==='open'){shim.readyState=1;if(shim.onopen){try{shim.onopen();}catch(e){}}return;}if(d.kind==='close'){_fireClose(shim);return;}if(d.kind==='text'){if(shim.onmessage){try{shim.onmessage({data:d.text});}catch(e){}}}};shim._worker.onerror=()=>_fireClose(shim);try{shim._worker.postMessage({kind:'init',wsUrl,port:directPort||undefined},directPort?[directPort]:[]);}catch(e){}return shim;}async function fxRtStart(){// V21: mọi play/chain-change gọi đây — hủy idle timer của lần stop trước // (không để session warm bị teardown giữa lúc đang chơi). if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}const chain=fxRtChain();const ck=fxRtChainKey(chain);const ckStruct=fxRtChainStructKey(chain);if(fxRt.active){// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart. @@ -419,10 +419,7 @@ return;}const wToken=++fxRt.startToken;fxRt.starting=false;fxRt.warm=false;// No const _wNodeOk=!!(fxRt.node&&fxRt.node.context===getAudioContext()&&masterBus&&masterBus.outputAnalyser&&masterBus.masterFxGain);if(!_wNodeOk){fxRtStop(true);// fallthrough -> cold start phía dưới (fxRtStop đã bump token) }else{if(fxRt.chainStructKey!==ckStruct){await fxRtSetChain(chain);if(wToken!==fxRt.startToken)return;fxRt.chainStructKey=ckStruct;}fxRt.chainKey=ck;// Reconnect edges: disconnect-then-connect — toggle nhanh <180ms bypass // timer chưa kịp cắt -> disconnect ở đây tránh duplicate edge. -try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.connect(fxRt.node);}catch(e){}try{if(fxRt.node)fxRt.node.disconnect(masterBus.masterFxGain);}catch(e){}try{if(fxRt.node)fxRt.node.connect(masterBus.masterFxGain);}catch(e){}fxRt.active=true;fxRt.inQueue=[];fxRt.outL=null;fxRt.outR=null;// M26b diag: PWR ON wake — snap pdcDelay/gains + hoi worklet state luc -// wake, luc 'started' (wet bat dau) + 180/400/800ms sau: do wet-lag that, -// delay node co ve 0 truoc crossfade khong. Bo diag o build fix. -let _m26onWlArmed=false;const _snapOn=(tag,_sched)=>{let _pd='na',_gd='na',_gw='na',_ps='na',_pa='na';const _b=masterBus,_c=getAudioContext();try{if(_b&&_b.pdcDelay)_pd=(_b.pdcDelay.delayTime.value*1000).toFixed(1);}catch(e){}try{if(_b&&_b.masterFxDryGain)_gd=_b.masterFxDryGain.gain.value.toFixed(4);}catch(e){}try{if(_b&&_b.masterFxGain)_gw=_b.masterFxGain.gain.value.toFixed(4);}catch(e){}try{_ps=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na';}catch(e){}try{_pa=typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na';}catch(e){}_dbgLog(tag,'pdcMs='+_pd+' gDry='+_gd+' gWet='+_gw+' lat='+(fxRt.latencyTotal||0)+' ps='+_ps+' pa='+_pa+' act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' faded='+(fxRt.fadedIn?1:0)+' ctx='+(_c?_c.currentTime.toFixed(3):'na'));try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'dbg_state',k:tag});}catch(e){}if(_sched&&_sched.length)_sched.forEach((_ms,_ix)=>setTimeout(()=>_snapOn('ph_on'+(_ix+1)),_ms));};try{if(fxRt.node&&fxRt.node.port&&!_m26onWlArmed){_m26onWlArmed=true;const _wl=ev=>{try{const d=ev.data;if(d&&d.type==='dbg_state_r')_dbgLog('ph_on_wl','k='+d.k+' t='+d.t+' q='+d.q+' play='+d.playing+' hs='+d.hasStarted+' sil='+d.silenced+' fp='+d.flushPending+' stp='+d.stopped+' wm='+d.wrapMode+' wfb='+d.wrapFadeBlocks+' lat='+d.latencySamples+' rfill='+d.resumeFill+' inC='+d.inCount+' outPos='+d.outPos+' fadeL='+d.fadeLeft+' holdL='+d.holdFadeLeft);}catch(e){}};fxRt.node.port.addEventListener('message',_wl);setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.removeEventListener('message',_wl);}catch(e){}},2600);}}catch(e){}_snapOn('ph_on_a',null);// M19 Bug A fix: wake PWR ON sau warm-bypass. Trước đây chỉ flush engine +try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.connect(fxRt.node);}catch(e){}try{if(fxRt.node)fxRt.node.disconnect(masterBus.masterFxGain);}catch(e){}try{if(fxRt.node)fxRt.node.connect(masterBus.masterFxGain);}catch(e){}fxRt.active=true;fxRt.inQueue=[];fxRt.outL=null;fxRt.outR=null;// M19 Bug A fix: wake PWR ON sau warm-bypass. Trước đây chỉ flush engine // ring + fadedIn=true NGAY — nhưng worklet lúc bypass rời graph không // process: outQueue còn ~FILL block audio CŨ + hasStarted=true (không gửi // 'started' lại) → reconnect xong phát stale block ngay + crossfade dry→fx @@ -433,7 +430,7 @@ let _m26onWlArmed=false;const _snapOn=(tag,_sched)=>{let _pd='na',_gd='na',_gw=' if(fxRt.ws.readyState===1){try{fxRt.ws.send(JSON.stringify({cmd:'flush'}));}catch(e){}}try{fxRtPushAllParams();}catch(e){}try{fxRt.node.port.postMessage({type:'clear_queue',mode:'play'});}catch(e){}fxRt.fadedIn=false;// port.onmessage hiện tại là closure của cold-start cũ — guard 'started' // bằng token CŨ (≠ wToken) nên sẽ bỏ qua. Hook tạm bắt 'started' cho wake // (crossfade + PDC đúng lúc), forward mọi message khác; restore sau 3s. -const _wOld=fxRt.node.port.onmessage;const _wHook=ev=>{try{if(ev.data&&ev.data.type==='started'&&wToken===fxRt.startToken){fxRt.fadedIn=true;try{_snapOn('ph_on_s',[180,400,800]);}catch(e){}fxRtCrossfade(true,wToken);setTimeout(()=>{if(wToken===fxRt.startToken)fxRtApplyPdc();},250);}}catch(e){}try{_wOld(ev);}catch(e){}};fxRt.node.port.onmessage=_wHook;setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port.onmessage===_wHook)fxRt.node.port.onmessage=_wOld;}catch(e){}},3000);return;}}if(!chain.length){fxRtWarmPlayHold=false;try{fxRtApplyPdc();}catch(e){}return;}// C: warm session còn nhưng không wake được (ws đóng/session mất) -> teardown +const _wOld=fxRt.node.port.onmessage;const _wHook=ev=>{try{if(ev.data&&ev.data.type==='started'&&wToken===fxRt.startToken){fxRt.fadedIn=true;fxRtCrossfade(true,wToken);setTimeout(()=>{if(wToken===fxRt.startToken)fxRtApplyPdc();},250);}}catch(e){}try{_wOld(ev);}catch(e){}};fxRt.node.port.onmessage=_wHook;setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port.onmessage===_wHook)fxRt.node.port.onmessage=_wOld;}catch(e){}},3000);return;}}if(!chain.length){fxRtWarmPlayHold=false;try{fxRtApplyPdc();}catch(e){}return;}// C: warm session còn nhưng không wake được (ws đóng/session mất) -> teardown // trước cold-spawn (tránh 2 session song song). if(fxRt.warm)fxRtStop(true);// 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 @@ -441,7 +438,7 @@ if(fxRt.warm)fxRtStop(true);// Token chống race bypass: user bấm PWR/Bypass // 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=202609071745`);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=202609071819`);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):''}`;// M25: WS chay tren Web Worker - binary output/input di THANG @@ -552,22 +549,23 @@ for(let _a=0;_a<3;_a++){const sid=await fxRtWarmEnsure(want);if(sid&&fxRt.sessio // Master chain (worklet → bridge VST3) trễ ~2 block round-trip + latency // plugin. Track bypass mastering đi dry path (dryInput→dryOutput→output) KHÔNG // qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng -// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật -// (tag seq trên block) khi cần sample-accurate. -// M18: FILL 40 (sf-fx-realtime.js V48) → RTT = batching 4 block (~7 block @48k -// ≈ 1792) + FILL buffer 40 block (10240) = 47 block ≈ 12032 (~251ms @48k). Giữ -// đồng bộ với sf-fx-realtime.js. FX_RT_START_EST_MS: ước lượng khởi đầu khi -// chưa có phép đo (fx_play_first) — dùng cho freeze playhead thay V39 0.31s. -const FX_RT_ROUNDTRIP_SAMPLES=12032;const FX_RT_START_EST_MS=450;// G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr +// de align. M26: pdc = wet-lag FILL - content nghe = S(t-FILL); M26b do onset +// +208-221ms = FILL 40 block (10240 @48k ~213ms) + RTT nho. KHONG tin VST +// report (Ozone bao 17536 ~365ms nhung content-latency that ~0). ponytail: +// neu plugin content-latency that L>0 -> pdc = FILL + L (display-lat giu FILL +// vi processed da gom L). +// Dong bo voi sf-fx-realtime.js: FILL 40 block (10240 @48k). FX_RT_START_EST_MS: +// uoc luong khoi dau khi chua co phep do (fx_play_first) - dung cho freeze +// playhead thay V39 0.31s. +const FX_RT_WET_LAG_SAMPLES=10240;const FX_RT_START_EST_MS=450;// G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr // (da tru latency -> bam vi tri mau NGHE DUOC; pre-roll tu giu tai startOffset — // thay freeze/snap V40). Anchor moi play/wrap restart qua __fxRtPlayheadFreeze // (moi play/resume path deu goi); cold start (chua active) -> null -> WS message // dau tien anchor. -function fxRtAnchorProcessed(){const _phPrevAnchor=fxRt&&fxRt.processedAnchor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'){fxRt.processedAnchor=fxRt.processedSamples;}else{fxRt.processedAnchor=null;}try{if(_phArmed())_dbgLog('ph_anchor','prevA='+(typeof _phPrevAnchor==='number'?_phPrevAnchor:'na')+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' act='+(fxRt&&fxRt.active?1:0));}catch(e){}}function fxRtApplyPdc(){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.pdcDelay)return;// M23 Bug C: bo +FX_RT_ROUNDTRIP_SAMPLES — latencyTotal (bridge 'latency' -// frame) DA gom round-trip that (log M21/M22: display active = wall - lat). -// +12032 double-count -> dry path delay > wet lag ~250ms -> crossfade lech pha -// (scratch). ponytail: neu engine pipeline doi RTT that -> do lai o day. -const l=fxRt&&fxRt.active?fxRt.latencyTotal||0:0;try{bus.pdcDelay.delayTime.setTargetAtTime(l/(ctx.sampleRate||44100),ctx.currentTime,0.05);}catch(e){}}// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi +function fxRtAnchorProcessed(){const _phPrevAnchor=fxRt&&fxRt.processedAnchor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'){fxRt.processedAnchor=fxRt.processedSamples;}else{fxRt.processedAnchor=null;}try{if(_phArmed())_dbgLog('ph_anchor','prevA='+(typeof _phPrevAnchor==='number'?_phPrevAnchor:'na')+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' act='+(fxRt&&fxRt.active?1:0));}catch(e){}}function fxRtApplyPdc(){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.pdcDelay)return;// M26: pdc = wet-lag FILL 10240 (~213ms) - content nghe = S(t-FILL); VST +// report (17536) la plugin buffer, content-latency that ~0 (M26b do onset). +// ponytail: neu content-latency that L>0 -> FX_RT_WET_LAG_SAMPLES + L. +const l=fxRt&&fxRt.active?FX_RT_WET_LAG_SAMPLES:0;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 @@ -1740,10 +1738,7 @@ const _guiVst=masteringSettings.gui_open_path&&(masteringSettings.chain||[]).som const _hasVst=!!(masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false));if(fxRt.active||fxRt.starting||fxRt.warm){if(_hasVst&&fxRt.sessionId&&(fxRt.active||fxRt.warm)){if(fxRt.active){// M23 Bug C: PWR OFF giua play — giu PDC (dry = content, khong // jump/scratch) + rebase display ve content de warm display lien // tuc (khong nhay +lat nhu M22). -let _plNow=false;try{_plNow=!!(isPlayingRef&&isPlayingRef.current)||!!(subTabsRef&&subTabsRef.current&&subTabsRef.current.some(s=>s&&s.isPlaying));if(_plNow){fxRtWarmPlayHold=true;const _c2=getAudioContext();const _sr2=_c2.sampleRate||44100;const _off2=startOffsetTimeRef.current||0;const _at2=startAudioTimeRef.current||_c2.currentTime;const _eng2=typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(_off2,_off2+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/_sr2):null;const _wall2=_off2+(_c2.currentTime-_at2);const _content2=_eng2!==null?_eng2:_wall2-(fxRt.latencyTotal||0)/_sr2;startOffsetTimeRef.current=_content2;startAudioTimeRef.current=_c2.currentTime;_dbgLog('ph_offhold','cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' wall='+_wall2.toFixed(3)+' content='+_content2.toFixed(3)+' off='+startOffsetTimeRef.current.toFixed(3)+' at='+startAudioTimeRef.current.toFixed(3)+' ctx='+_c2.currentTime.toFixed(3)+' lat='+(fxRt.latencyTotal||0)+' hold=1');}else{_dbgLog('ph_offhold','cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' hold=0');}}catch(e){try{_dbgLog('ph_offhold','err '+String(e));}catch(e2){}}fxRtWarmBypass();// M26 diag: PWR OFF mid-play — snap pdcDelay/gains + hoi worklet -// state tai t0/+180/+400/+800ms de do: pdc thuc te khi dry mo, -// wet tail song bao lau, gains ramp ra sao. Bo diag o build fix. -if(_plNow){const _busB=masterBus;const _ctxB=getAudioContext();const _snapB=k=>{let _pdB='na',_gdB='na',_gwB='na',_psB='na',_paB='na';try{if(_busB&&_busB.pdcDelay)_pdB=(_busB.pdcDelay.delayTime.value*1000).toFixed(1);}catch(e){}try{if(_busB&&_busB.masterFxDryGain)_gdB=_busB.masterFxDryGain.gain.value.toFixed(4);}catch(e){}try{if(_busB&&_busB.masterFxGain)_gwB=_busB.masterFxGain.gain.value.toFixed(4);}catch(e){}try{_psB=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na';}catch(e){}try{_paB=typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na';}catch(e){}const _oB=startOffsetTimeRef.current||0;const _aB=startAudioTimeRef.current||0;const _tB=_ctxB?_ctxB.currentTime:0;_dbgLog('ph_echo'+k,'pdcMs='+_pdB+' gDry='+_gdB+' gWet='+_gwB+' lat='+(fxRt.latencyTotal||0)+' ps='+_psB+' pa='+_paB+' cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' off='+_oB.toFixed(3)+' wall='+(_oB+_tB-_aB).toFixed(3)+' ctx='+_tB.toFixed(3));try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'dbg_state',k:k});}catch(e){}};const _wlB=ev=>{try{const d=ev.data;if(d&&d.type==='dbg_state_r'){_dbgLog('ph_echo_wl','k='+d.k+' t='+d.t+' q='+d.q+' play='+d.playing+' hs='+d.hasStarted+' sil='+d.silenced+' fp='+d.flushPending+' stp='+d.stopped+' wm='+d.wrapMode+' wfb='+d.wrapFadeBlocks+' lat='+d.latencySamples+' rfill='+d.resumeFill+' inC='+d.inCount+' outPos='+d.outPos+' fadeL='+d.fadeLeft+' holdL='+d.holdFadeLeft);}}catch(e){}};try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.addEventListener('message',_wlB);}catch(e){}_snapB(0);[180,400,800].forEach((_msB,_iB)=>setTimeout(()=>_snapB(_iB+1),_msB));setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.removeEventListener('message',_wlB);}catch(e){}},1600);}}}else{fxRtStop();}}}}catch(e){console.warn('[FX RT] resync fail:',e);}// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị +let _plNow=false;try{_plNow=!!(isPlayingRef&&isPlayingRef.current)||!!(subTabsRef&&subTabsRef.current&&subTabsRef.current.some(s=>s&&s.isPlaying));if(_plNow){fxRtWarmPlayHold=true;const _c2=getAudioContext();const _sr2=_c2.sampleRate||44100;const _off2=startOffsetTimeRef.current||0;const _at2=startAudioTimeRef.current||_c2.currentTime;const _eng2=typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(_off2,_off2+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)/_sr2):null;const _wall2=_off2+(_c2.currentTime-_at2);const _content2=_eng2!==null?_eng2:_wall2-FX_RT_WET_LAG_SAMPLES/_sr2;startOffsetTimeRef.current=_content2;startAudioTimeRef.current=_c2.currentTime;_dbgLog('ph_offhold','cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' wall='+_wall2.toFixed(3)+' content='+_content2.toFixed(3)+' off='+startOffsetTimeRef.current.toFixed(3)+' at='+startAudioTimeRef.current.toFixed(3)+' ctx='+_c2.currentTime.toFixed(3)+' lat='+(fxRt.latencyTotal||0)+' hold=1');}else{_dbgLog('ph_offhold','cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' hold=0');}}catch(e){try{_dbgLog('ph_offhold','err '+String(e));}catch(e2){}}fxRtWarmBypass();}}else{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 // ngay lên node đang phát (không chờ tracks effect). try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});// Sub-nodes (section tabs, key '_sub_'): PWR master ON → @@ -1863,7 +1858,7 @@ window.__fxRtWakeRebase=function(){try{if(!fxRt||!fxRt.active)return;const _c=ge // rebase tiep toi content-hien-tai (off + elapsed-warm) roi anchor pa = // ps - lat de engine-display noi lien content (het freeze ~0.37s + am // resume tre 1/4 beat nhu M22). Ha co hold. -const _contentNow=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));startOffsetTimeRef.current=_contentNow;startAudioTimeRef.current=_c.currentTime;if(_pa!==null)fxRt.processedAnchor=_pa-Math.round(fxRt.latencyTotal||0);fxRtWarmPlayHold=false;}else{// (M22) rebase wall — khi khong OFF-giua-play (warm tu stop), off/at la +const _contentNow=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));startOffsetTimeRef.current=_contentNow;startAudioTimeRef.current=_c.currentTime;if(_pa!==null)fxRt.processedAnchor=_pa-FX_RT_WET_LAG_SAMPLES;fxRtWarmPlayHold=false;}else{// (M22) rebase wall — khi khong OFF-giua-play (warm tu stop), off/at la // grid that nen giu hanh vi cu. startOffsetTimeRef.current=_wall;startAudioTimeRef.current=_c.currentTime;if(_pa!==null)fxRt.processedAnchor=_pa;}if(_fxRtSeekFreeze){_fxRtSeekFreeze=false;if(_fxRtSeekFreezeTimer){clearTimeout(_fxRtSeekFreezeTimer);_fxRtSeekFreezeTimer=null;}}try{_dbgLog('ph_rebase','wall='+_wall.toFixed(3)+' at='+startAudioTimeRef.current.toFixed(3)+' off='+startOffsetTimeRef.current.toFixed(3)+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' lat='+(fxRt.latencyTotal||0));}catch(e){}}catch(e){try{_dbgLog('ph_rebase','err '+String(e));}catch(e2){}}};// M21 ph-diag hooks (Bug C — playhead nhay khi PWR master FX toggle) // __phArmPwr: user bam PWR OFF/ON -> arm cua so log 6s + snapshot ngay lap tuc. @@ -2101,7 +2096,7 @@ const triggerRescheduleNow=()=>{try{lastRescheduleTimeRef.current=0;}catch(e){}} // timeline sample-accurate (scheduling theo sampleOffset). if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected&&(isPlaying||isPlayingRef.current)){try{const _pctx=getAudioContext();// G3: FX active -> position tu processed engine (da tru latency) — bam // vi tri mau NGHE duoc, khong freeze/snap; fallback wall-clock. -let _posSamples;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_posSamples=Math.max(Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)),Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))));}else{const _elapsed=_pctx.currentTime-(startAudioTimeRef.current||_pctx.currentTime);_posSamples=Math.max(0,Math.floor((startOffsetTimeRef.current+_elapsed)*(_pctx.sampleRate||44100)));}bridgePlayheadSampleRef.current=_posSamples;// set_position ~mỗi giây: C++ A13 cập nhật timeline anchor, tối ưu cho +let _posSamples;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_posSamples=Math.max(Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)),Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)));}else{const _elapsed=_pctx.currentTime-(startAudioTimeRef.current||_pctx.currentTime);_posSamples=Math.max(0,Math.floor((startOffsetTimeRef.current+_elapsed)*(_pctx.sampleRate||44100)));}bridgePlayheadSampleRef.current=_posSamples;// set_position ~mỗi giây: C++ A13 cập nhật timeline anchor, tối ưu cho // seek/loop (Rust pump + C++ không tự biết vị trí; JS là nguồn duy nhất). try{const _sr=_pctx.sampleRate||44100;if(!window.__bridgeLastPosSent)window.__bridgeLastPosSent=0;if(bridgePlayheadSampleRef.current-window.__bridgeLastPosSent>_sr){window.__bridgeLastPosSent=bridgePlayheadSampleRef.current;window.NativeBridgeService.transport('set_position',bridgePlayheadSampleRef.current);}}catch(e2){}}catch(e){}}// Realtime re-schedule: khi đang play (loop play) mà items (vị trí/speed/ // duration — kể cả nội dung section tab) thay đổi → dừng + schedule lại từ @@ -2170,10 +2165,10 @@ if(!st.buffer&&st.type!=='PIANO_ROLL')return;if(!st.isPlaying){// PIANO ROLL / s // item (st.currentTime = playPos - item.startTime); playhead vượt ra // ngoài duration item → bắt đầu từ đầu (0). Không có play nào → đứng // yên (chế độ edit). Loop luôn tiếp tục (rAF). -if(isPlaying){const context=getAudioContext();let _ph=startOffsetTimeRef.current+(context.currentTime-startAudioTimeRef.current);if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_ph=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/(context.sampleRate||44100);}let _local=_ph;const _itemInfo=getPianoRollItemInfo(st);if(_itemInfo){if(_ph<_itemInfo.startTime)_local=0;else if(_ph>=_itemInfo.startTime+_itemInfo.duration)_local=0;// vượt duration item → đầu +if(isPlaying){const context=getAudioContext();let _ph=startOffsetTimeRef.current+(context.currentTime-startAudioTimeRef.current);if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_ph=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)/(context.sampleRate||44100);}let _local=_ph;const _itemInfo=getPianoRollItemInfo(st);if(_itemInfo){if(_ph<_itemInfo.startTime)_local=0;else if(_ph>=_itemInfo.startTime+_itemInfo.duration)_local=0;// vượt duration item → đầu else _local=_ph-_itemInfo.startTime;}// Log chẩn đoán (rate-limited ~2 lần/giây): xác nhận đồng bộ MAIN/ // SECTION ↔ PIANO ROLL — ph (vị trí play) → local (trong item). -try{const _sf=window.__syncFrame=(window.__syncFrame||0)+1;if(_sf%45===0){console.log('[Sync] PR',st.id,'ph=',_ph.toFixed(2),'item.start=',_itemInfo?_itemInfo.startTime.toFixed(2):'-','dur=',_itemInfo?_itemInfo.duration.toFixed(2):'-','→ local=',_local.toFixed(2),'tab=',activeTabRef.current);}}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:_local}:s));}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;let wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){wallTime=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/(context.sampleRate||44100);}// Loop sub-tab selection (bufferPos is buffer-time) +try{const _sf=window.__syncFrame=(window.__syncFrame||0)+1;if(_sf%45===0){console.log('[Sync] PR',st.id,'ph=',_ph.toFixed(2),'item.start=',_itemInfo?_itemInfo.startTime.toFixed(2):'-','dur=',_itemInfo?_itemInfo.duration.toFixed(2):'-','→ local=',_local.toFixed(2),'tab=',activeTabRef.current);}}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:_local}:s));}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;let wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){wallTime=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)/(context.sampleRate||44100);}// Loop sub-tab selection (bufferPos is buffer-time) if(st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionStart!==st.selectionEnd&&st.isLooping){const start=Math.min(st.selectionStart,st.selectionEnd);const end=Math.max(st.selectionStart,st.selectionEnd);if(bufferPos>=end){fxRtWrapRestart=true;// V38: loop wrap restart try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime stopAllPlayback();setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),currentTime:start,isPlaying:true}:s));schedulePianoRollMidi(st,start);startSubTabPlayback(st,start);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}const effectiveDuration=st.type==='PIANO_ROLL'?(()=>{const notes=st.notes||[];const bpmVal=parseInt(bpmRef?.current||bpm)||120;const beatSec=60.0/bpmVal;if(recordingStateRef.current==='RECORDING')return 600.0;// 10 min during recording @@ -2183,7 +2178,7 @@ stopAllPlayback();if(st.isLooping){setSubTabs(prev=>prev.map(s=>s.id===activeTab // nếu chỉ chạy silent buffer). if(st.type==='PIANO_ROLL')schedulePianoRollMidi(st,0);startSubTabPlayback(st,0);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:0,isPlaying:false}:s));}return;}setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:wallTime}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}if(!isPlaying)return;const context=getAudioContext();const elapsed=context.currentTime-startAudioTimeRef.current;const updatedTime=startOffsetTimeRef.current+elapsed;// G3: playhead display engine-derived khi FX active (wrap/loop trigger giu // wall-clock — transport truth). -const _playheadSec=_fxRtSeekFreeze&&fxRt&&fxRt.active?startOffsetTimeRef.current:fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(startOffsetTimeRef.current,startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/(context.sampleRate||44100)):updatedTime;// Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2 +const _playheadSec=_fxRtSeekFreeze&&fxRt&&fxRt.active?startOffsetTimeRef.current:fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(startOffsetTimeRef.current,startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)/(context.sampleRate||44100)):updatedTime;// Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2 // If selection cleared by user, play linearly (don't loop) if(!selectionCleared&&isLoopingSelection&&selLeft!==null&&selRight!==null){if(selRight>selLeft&&updatedTime>=selRight){if(hasAnySolo||selectionMode==='local'){fxRtWrapRestart=true;// V38: loop wrap restart try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime @@ -2251,7 +2246,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=202609071745`);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=202609071819`);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;ifxRtTrackStop(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