diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index d6f3019..4d6cd23 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -1405,6 +1405,9 @@ function initMasterBus(ctx) { console.warn('initMasterBus apply mastering error:', e); } } + // M23 Bug B fix: bus vua tao/rebuild — ap lai master fader (state set tu luc + // project load khi bus chua ton tai). + try { if (typeof fxMasterVolApply === 'function') fxMasterVolApply(); } catch (e) {} return masterBus; } @@ -1461,6 +1464,17 @@ function applyMasterVolumesTo(bus, volL, volR, muted, time) { bus.muteGain.gain.setValueAtTime(muted ? 0 : 1, time); } } +// M23 Bug B fix: master fader state global + ap lai khi masterBus (re)create — +// volume L/R/MUTE phai LUON la MAIN OUT, khong chi khi Mixer panel mo. +window.__masterVolState = null; +function fxMasterVolApply() { + try { + const _st = window.__masterVolState; + if (!_st) return; + const _b = masterBus || window.masterBus; + if (_b && _b.volLGain) applyMasterVolumesTo(_b, _st.volL, _st.volR, !!_st.muted); + } catch (e) {} +} let _lastMasteringActive = null; let _lastMasteringSig = null; @@ -1865,6 +1879,10 @@ function encodeFloatWavBlob(pcm, sampleRate, channels) { // tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường // chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out. const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, chainStructKey: null, latencyTotal: 0, fadedIn: false, pdcCalibMs: 0, processedSamples: null, processedAnchor: null, warm: false }; +// M23 Bug C fix: PWR OFF giua play — giu pdcDelay (content dry = content wet, +// khong nhay/scratch khi warm). Wake/stop ha co (__fxRtWakeRebase, +// stopAllPlayback, fxRtStop, fxRtStart chain-empty). +let fxRtWarmPlayHold = false; // Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không // teardown) → fxRtStart early-return → KHÔNG reload VST ~4s mỗi pass loop // (hết bypass rồi mới load lại). Chỉ set TRƯỚC nhánh restart/resume; user @@ -2041,7 +2059,11 @@ async function fxRtStart() { // master native — KHÔNG teardown (giết editor + preset). Play đi dry path; // PWR ON sau đó wake nối graph ở đây. Teardown khi vô chủ do settings // effect (OP12) xử lý. - if (!chain.length) return; + if (!chain.length) { + fxRtWarmPlayHold = false; + try { fxRtApplyPdc(); } catch (e) {} // zero delay — play mới không FX + return; + } const wToken = ++fxRt.startToken; fxRt.starting = false; fxRt.warm = false; @@ -2103,7 +2125,11 @@ async function fxRtStart() { return; } } - if (!chain.length) 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); @@ -2308,6 +2334,7 @@ function fxRtStop(silent) { if (fxRt.flushTimer) { clearTimeout(fxRt.flushTimer); fxRt.flushTimer = null; } fxRt.inQueue = []; fxRt.flushing = false; + fxRtWarmPlayHold = false; if (!fxRt.active && !fxRt.ws && !fxRt.node && !fxRt.sessionId) return; let _fxStopNode = null; try { @@ -2383,7 +2410,9 @@ function fxRtWarmBypass() { try { if (masterBus && masterBus.outputAnalyser) masterBus.outputAnalyser.disconnect(fxRt.node); } catch (e) {} try { fxRt.node.disconnect(); } catch (e) {} fxRt.fadedIn = false; - fxRtApplyPdc(); + // M23 Bug C: OFF giua play — giu PDC (content lien tuc qua warm); zero + // delay chi khi stop han (stopAllPlayback clear hold + applyPdc). + if (!fxRtWarmPlayHold) fxRtApplyPdc(); }, 180); } else if (fxRt.node) { // Graph cũ (không crossfade gains) — hard switch + nối lại dry. @@ -2392,7 +2421,7 @@ function fxRtWarmBypass() { if (masterBus && masterBus.outputAnalyser && masterBus.output) { try { masterBus.outputAnalyser.connect(masterBus.output); } catch (e) {} } - fxRtApplyPdc(); + if (!fxRtWarmPlayHold) fxRtApplyPdc(); } } catch (e) {} // Không null node/ws/onmessage/sessionId — wake cần node cũ để reconnect; @@ -2570,7 +2599,11 @@ function fxRtApplyPdc() { const ctx = getAudioContext(); const bus = masterBus; if (!ctx || !bus || !bus.pdcDelay) return; - const l = (fxRt && fxRt.active) ? (FX_RT_ROUNDTRIP_SAMPLES + (fxRt.latencyTotal || 0)) : 0; + // 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) {} } @@ -19816,6 +19849,15 @@ const App = () => { window.__masterAutoVol = masterAutoVol; window.__masterAutoOn = masterAutoOn; }, [masterAutoVol, masterAutoOn]); + // M23 Bug B fix: master fader ap NGAY moi luc (main out) — khong cho Mixer + // panel mount (MasterStripConsole). State global de initMasterBus ap lai. + React.useEffect(() => { + window.__masterVolState = { volL: masterVolL, volR: masterVolR, muted: !!masterMuted }; + const _mb = masterBus || window.masterBus; + if (_mb && _mb.volLGain && (typeof audioCtx !== 'undefined' && audioCtx)) { + try { applyMasterVolumesTo(_mb, masterVolL, masterVolR, !!masterMuted); } catch (e) {} + } + }, [masterVolL, masterVolR, masterMuted]); React.useEffect(() => { if (isPlaying && typeof applyMasterAutomationSchedule === 'function') { try { applyMasterAutomationSchedule(currentTimeRef.current || 0); } catch (e) { console.warn('[Auto] reschedule error:', e); } @@ -20878,7 +20920,32 @@ const App = () => { 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) fxRtWarmBypass(); + 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). + try { + const _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(); + } } else { fxRtStop(); } @@ -21894,9 +21961,23 @@ const App = () => { const _c = getAudioContext(); const _wall = (startOffsetTimeRef.current || 0) + (_c.currentTime - (startAudioTimeRef.current || _c.currentTime)); const _pa = (typeof fxRt.processedSamples === 'number') ? fxRt.processedSamples : null; - startOffsetTimeRef.current = _wall; - startAudioTimeRef.current = _c.currentTime; - if (_pa !== null) fxRt.processedAnchor = _pa; + if (fxRtWarmPlayHold) { + // M23 Bug C: wake tu OFF-giua-play — off/at DA rebase ve content tai OFF; + // 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 + // 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) {} } @@ -27191,6 +27272,10 @@ const App = () => { try { fxRtStop(); } catch (e) { console.warn('[FX RT] idle stop error:', e); } }, FX_RT_WARM_IDLE_MS); } + // M23 Bug C: dung han (fade) — ha hold + PDC ve 0 (am dang fade -> khong + // nghe jump; warm sau do dry delay = 0 nhu cu). + fxRtWarmPlayHold = false; + try { fxRtApplyPdc(); } catch (e) {} } _nativePlayEpoch++; // hủy mọi render/decode pending của lần play cũ // V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 61ae1fc..3dc2eb5 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -265,14 +265,18 @@ if(window.currentMasteringSettings){try{// Reset sig-cache: chain MỚI (biquad/ // INIT (gain 0, width 100…) — applyMasteringSettings early-return vì // _lastMasteringSig không đổi (module-level, persist qua recreation) → // chain FLAT ("spectrum hiển thị nhưng không xử lí âm thanh"). -_lastMasteringSig=null;toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected,window.currentMasteringSettings.isBypassed);applyMasteringSettings(window.currentMasteringSettings);}catch(e){console.warn('initMasterBus apply mastering error:',e);}}return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}// Gap 7: master volume automation lane — schedule envelope tren master output +_lastMasteringSig=null;toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected,window.currentMasteringSettings.isBypassed);applyMasteringSettings(window.currentMasteringSettings);}catch(e){console.warn('initMasterBus apply mastering error:',e);}}// M23 Bug B fix: bus vua tao/rebuild — ap lai master fader (state set tu luc +// project load khi bus chua ton tai). +try{if(typeof fxMasterVolApply==='function')fxMasterVolApply();}catch(e){}return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}// Gap 7: master volume automation lane — schedule envelope tren master output // gain (dB tuyen tinh, clamp -30..+3 nhu render_engine._apply_master_volume_automation). // Trang thai tu React qua window.__masterAutoOn/__masterAutoVol (pattern // window.__currentZoom). playheadSec = vi tri timeline giay luc bat dau nghe. function applyMasterAutomationSchedule(playheadSec){const bus=masterBus;if(!bus||!bus.output||!bus.output.gain)return;const ctx=audioCtx||getAudioContext();const now=ctx.currentTime;const env=window.__masterAutoOn&&Array.isArray(window.__masterAutoVol)?window.__masterAutoVol.filter(p=>p&&typeof p.time==='number'&&typeof p.db==='number').slice().sort((a,b)=>a.time-b.time):[];bus.output.gain.cancelScheduledValues(now);if(!env.length){bus.output.gain.setValueAtTime(1,now);return;}const ph=playheadSec||0;let idx=0;while(idx=env.length)initDb=env[env.length-1].db;else{const a=env[idx-1],b=env[idx];const span=b.time-a.time||1;initDb=a.db+(b.db-a.db)*Math.max(0,Math.min(1,(ph-a.time)/span));}bus.output.gain.setValueAtTime(Math.pow(10,Math.max(-30,Math.min(3,initDb))/20),now);for(let k=idx;know)bus.output.gain.linearRampToValueAtTime(Math.pow(10,Math.max(-30,Math.min(3,env[k].db))/20),t);}}// Master L/R faders + LINK + MUTE - per-channel gain stage (output -> volSplit -> // volLGain/volRGain -> volMerge -> muteGain -> analyser). time===undefined -> // live (smoothing); co time -> offline (setValueAtTime). -50dB ~ -inf. -function applyMasterVolumesTo(bus,volL,volR,muted,time){if(!bus||!bus.volLGain||!bus.volRGain||!bus.muteGain)return;var gl=volL<=-50?0:Math.pow(10,volL/20);var gr=volR<=-50?0:Math.pow(10,volR/20);if(time===undefined){var now=audioCtx.currentTime;bus.volLGain.gain.setTargetAtTime(gl,now,0.01);bus.volRGain.gain.setTargetAtTime(gr,now,0.01);bus.muteGain.gain.setTargetAtTime(muted?0:1,now,0.01);}else{bus.volLGain.gain.setValueAtTime(gl,time);bus.volRGain.gain.setValueAtTime(gr,time);bus.muteGain.gain.setValueAtTime(muted?0:1,time);}}let _lastMasteringActive=null;let _lastMasteringSig=null;let _lastChainSig=null;function chainSignature(chainArray){return(chainArray||[]).map(m=>(m.type||'')+(m.active?'1':'0')).join(',');}// ── Track FX module factory (mastering_expand.md §II.4) ── +function applyMasterVolumesTo(bus,volL,volR,muted,time){if(!bus||!bus.volLGain||!bus.volRGain||!bus.muteGain)return;var gl=volL<=-50?0:Math.pow(10,volL/20);var gr=volR<=-50?0:Math.pow(10,volR/20);if(time===undefined){var now=audioCtx.currentTime;bus.volLGain.gain.setTargetAtTime(gl,now,0.01);bus.volRGain.gain.setTargetAtTime(gr,now,0.01);bus.muteGain.gain.setTargetAtTime(muted?0:1,now,0.01);}else{bus.volLGain.gain.setValueAtTime(gl,time);bus.volRGain.gain.setValueAtTime(gr,time);bus.muteGain.gain.setValueAtTime(muted?0:1,time);}}// M23 Bug B fix: master fader state global + ap lai khi masterBus (re)create — +// volume L/R/MUTE phai LUON la MAIN OUT, khong chi khi Mixer panel mo. +window.__masterVolState=null;function fxMasterVolApply(){try{const _st=window.__masterVolState;if(!_st)return;const _b=masterBus||window.masterBus;if(_b&&_b.volLGain)applyMasterVolumesTo(_b,_st.volL,_st.volR,!!_st.muted);}catch(e){}}let _lastMasteringActive=null;let _lastMasteringSig=null;let _lastChainSig=null;function chainSignature(chainArray){return(chainArray||[]).map(m=>(m.type||'')+(m.active?'1':'0')).join(',');}// ── Track FX module factory (mastering_expand.md §II.4) ── // The SAME module DSP used in the mastering chain, instantiated per-track for // the [FX] button on track strips. Returns { input, output, nodes, dispose }. // ── Track FX module factory (mastering_expand.md §II.4 / unified_fx_rack_panel.md) ── @@ -323,7 +327,10 @@ const len=Math.floor(pcm.length/channels);const bps=4;const dataBytes=len*bps*ch // worklet → analyser (sau master fader, trước metering/destination — khớp thứ // tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường // chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out. -const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,lastInSent:0,chainKey:null,chainStructKey:null,latencyTotal:0,fadedIn:false,pdcCalibMs:0,processedSamples:null,processedAnchor:null,warm:false};// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không +const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,lastInSent:0,chainKey:null,chainStructKey:null,latencyTotal:0,fadedIn:false,pdcCalibMs:0,processedSamples:null,processedAnchor:null,warm:false};// M23 Bug C fix: PWR OFF giua play — giu pdcDelay (content dry = content wet, +// khong nhay/scratch khi warm). Wake/stop ha co (__fxRtWakeRebase, +// stopAllPlayback, fxRtStop, fxRtStart chain-empty). +let fxRtWarmPlayHold=false;// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không // teardown) → fxRtStart early-return → KHÔNG reload VST ~4s mỗi pass loop // (hết bypass rồi mới load lại). Chỉ set TRƯỚC nhánh restart/resume; user // stop thật (fade=true) giữ master FX warm + idle timeout (V21); bridge-down / @@ -389,7 +396,8 @@ if(fxRt.warm&&!fxRt.starting&&fxRt.sessionId&&fxRt.ws&&fxRt.ws.readyState===1){/ // master native — KHÔNG teardown (giết editor + preset). Play đi dry path; // PWR ON sau đó wake nối graph ở đây. Teardown khi vô chủ do settings // effect (OP12) xử lý. -if(!chain.length)return;const wToken=++fxRt.startToken;fxRt.starting=false;fxRt.warm=false;// Node/context/gains còn nguyên? Nếu masterBus rebuild (recovery/export) từ +if(!chain.length){fxRtWarmPlayHold=false;try{fxRtApplyPdc();}catch(e){}// zero delay — play mới không FX +return;}const wToken=++fxRt.startToken;fxRt.starting=false;fxRt.warm=false;// Node/context/gains còn nguyên? Nếu masterBus rebuild (recovery/export) từ // lúc warm -> node thuộc graph cũ -> không nối được -> fallthrough cold start // bên dưới. Edge hẹp: PWR OFF từ đầu + GUI mở + PWR ON chưa từng play -> // warm-ensure chỉ tạo session (chưa node) -> editor native bị giết (session @@ -409,7 +417,7 @@ try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(f 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;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)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 @@ -417,7 +425,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=202609071038`);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=202609071125`);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 @@ -469,7 +477,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;fxRt.chainStructKey=ckStruct;}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;}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 +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;fxRtWarmPlayHold=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). @@ -486,8 +494,10 @@ function fxRtWarmBypass(){if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer); // sớm khi dry chưa chiếm chỗ -> click. Giữ node/ws/session cho wake. try{fxRtCrossfade(false);}catch(e){}setTimeout(()=>{// Guard toggle nhanh: PWR ON trong 180ms -> wake đã nối lại node — // KHÔNG cắt (cắt ở đây làm đứt node đang chạy). -if(fxRt.active||fxRt.startToken!==_byToken)return;try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}fxRt.fadedIn=false;fxRtApplyPdc();},180);}else if(fxRt.node){// Graph cũ (không crossfade gains) — hard switch + nối lại dry. -try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}if(masterBus&&masterBus.outputAnalyser&&masterBus.output){try{masterBus.outputAnalyser.connect(masterBus.output);}catch(e){}}fxRtApplyPdc();}}catch(e){}// Không null node/ws/onmessage/sessionId — wake cần node cũ để reconnect; +if(fxRt.active||fxRt.startToken!==_byToken)return;try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}fxRt.fadedIn=false;// M23 Bug C: OFF giua play — giu PDC (content lien tuc qua warm); zero +// delay chi khi stop han (stopAllPlayback clear hold + applyPdc). +if(!fxRtWarmPlayHold)fxRtApplyPdc();},180);}else if(fxRt.node){// Graph cũ (không crossfade gains) — hard switch + nối lại dry. +try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}if(masterBus&&masterBus.outputAnalyser&&masterBus.output){try{masterBus.outputAnalyser.connect(masterBus.output);}catch(e){}}if(!fxRtWarmPlayHold)fxRtApplyPdc();}}catch(e){}// Không null node/ws/onmessage/sessionId — wake cần node cũ để reconnect; // worklet rời graph (0 cạnh) -> không process -> không gửi frame -> engine // idle an toàn (bridge vẫn sống cho GUI native). fxRt.active=false;fxRt.warm=true;fxRt.latencyTotal=0;}// C: warm-ensure — tạo session realtime KHÔNG nối node (GUI native cần session @@ -529,7 +539,11 @@ const FX_RT_ROUNDTRIP_SAMPLES=12032;const FX_RT_START_EST_MS=450;// G3: anchor p // 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;const l=fxRt&&fxRt.active?FX_RT_ROUNDTRIP_SAMPLES+(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;// 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 // 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 @@ -1562,7 +1576,9 @@ const[masterLink,setMasterLink]=useState(true);// LINK L/R const[masterMuted,setMasterMuted]=useState(false);// MUTE master const[masterAutoVol,setMasterAutoVol]=useState([]);// Gap 7: master volume automation [{time s, db}] const[masterAutoOn,setMasterAutoOn]=useState(false);// Gap 7: automation active -React.useEffect(()=>{window.__masterAutoVol=masterAutoVol;window.__masterAutoOn=masterAutoOn;},[masterAutoVol,masterAutoOn]);React.useEffect(()=>{if(isPlaying&&typeof applyMasterAutomationSchedule==='function'){try{applyMasterAutomationSchedule(currentTimeRef.current||0);}catch(e){console.warn('[Auto] reschedule error:',e);}}},[masterAutoVol,masterAutoOn,isPlaying]);const[masterVU,setMasterVU]=useState(0);// 0-1 +React.useEffect(()=>{window.__masterAutoVol=masterAutoVol;window.__masterAutoOn=masterAutoOn;},[masterAutoVol,masterAutoOn]);// M23 Bug B fix: master fader ap NGAY moi luc (main out) — khong cho Mixer +// panel mount (MasterStripConsole). State global de initMasterBus ap lai. +React.useEffect(()=>{window.__masterVolState={volL:masterVolL,volR:masterVolR,muted:!!masterMuted};const _mb=masterBus||window.masterBus;if(_mb&&_mb.volLGain&&typeof audioCtx!=='undefined'&&audioCtx){try{applyMasterVolumesTo(_mb,masterVolL,masterVolR,!!masterMuted);}catch(e){}}},[masterVolL,masterVolR,masterMuted]);React.useEffect(()=>{if(isPlaying&&typeof applyMasterAutomationSchedule==='function'){try{applyMasterAutomationSchedule(currentTimeRef.current||0);}catch(e){console.warn('[Auto] reschedule error:',e);}}},[masterAutoVol,masterAutoOn,isPlaying]);const[masterVU,setMasterVU]=useState(0);// 0-1 const[masterMeterPeak,setMasterMeterPeak]=useState(0);const[rightSidebarWidth,setRightSidebarWidth]=useState(320);const[tcpWidth,setTcpWidth]=useState(320);const[mediaExplorerHeight,setMediaExplorerHeight]=useState(50);const[panelPositions,setPanelPositions]=useState({export:'bottom',ai:'right',python_tools:'bottom',selection:'bottom',media_explorer:'bottom',fx_rack:'bottom',midi_events:'bottom'});const[panelDropZone,setPanelDropZone]=useState(null);const[dragGhostPos,setDragGhostPos]=useState(null);const[dragGhostPanel,setDragGhostPanel]=useState(null);const panelDragRef=useRef(null);const trackVuRefs=useRef({});const workspaceRef=useRef(null);const colResizerRef=useRef(null);const rowResizerRef=useRef(null);const[aiConfig,setAiConfig]=useState({baseUrl:localStorage.getItem('ai_base_url')||`${API_BASE_URL}`,apiKey:localStorage.getItem('ai_api_key')||'',model:localStorage.getItem('ai_model')||'deepseek-chat'});const[aiProviders,setAiProviders]=useState([]);useEffect(()=>{(async()=>{try{const data=await window.SonicAPI.getAIConfigs();if(data&&data.providers){setAiProviders(data.providers);const active=data.providers.find(p=>p.is_active)||data.providers[0];if(active)setSelectedProviderId(active.id);}}catch(e){/* server may not have config endpoint */}})();},[]);const[analysisState,setAnalysisState]=useState({status:'Sẵn sàng. Chạy AI để phân tích nhịp.',data:null,isRunning:false});const[aiPrompt,setAiPrompt]=useState('');const[aiComposeMode,setAiComposeMode]=useState('SIMILAR_VARIATION');const[promptHistory,setPromptHistory]=useState([]);const[promptHistIdx,setPromptHistIdx]=useState(-1);const promptHistRef=useRef([]);const aiPromptUndoRef=useRef({stack:[],idx:-1,max:30});const aiPromptUndoPush=text=>{const u=aiPromptUndoRef.current;u.stack.push(text);if(u.stack.length>u.max)u.stack.shift();u.idx=u.stack.length-1;};const[aiProvider,setAiProvider]=useState('OpenAI');const[aiModel,setAiModel]=useState('GPT-4o');const[aiActionLog,setAiActionLog]=useState([]);const actionLogContainerRef=useRef(null);useEffect(()=>{if(actionLogContainerRef.current){actionLogContainerRef.current.scrollTop=actionLogContainerRef.current.scrollHeight;}},[aiActionLog]);const[aiProcessing,setAiProcessing]=useState(false);const[aiSuggestions,setAiSuggestions]=useState([]);const[showAiTypeahead,setShowAiTypeahead]=useState(false);const[showAISuggestions,setShowAISuggestions]=useState(true);const[showAIActionLog,setShowAIActionLog]=useState(false);const aiPromptMgrRef=useRef(null);const aiTypeaheadRef=useRef(null);const[selectedProviderId,setSelectedProviderId]=useState('');const[exportSettings,setExportSettings]=useState(()=>{let expDir='',remExpDir=false;try{expDir=localStorage.getItem('sonic_export_dir')||'';}catch(e){}try{remExpDir=localStorage.getItem('sonic_remember_export_dir')==='1';}catch(e){}return{sampleRate:'44100',bitDepth:'16',format:'wav',source:'project',quality:'44khz',channels:'stereo',loop:false,loopBars:4,dither:false,normalize:true,normalizeTarget:'-14lufs',monoCheck:true,realtime:false,// Gap 13: BWF/INFO metadata cho WAV mastering export metaTitle:'',metaArtist:'',metaIsrc:'',// Export file + thư mục (ExportModal) — dir lưu localStorage nếu remember fileName:'',exportDir:expDir,rememberExportDir:remExpDir};});// Persist export dir mặc định (chỉ khi user bật remember) — ExportModal @@ -1697,7 +1713,10 @@ const _recNow=activeAudioRecordersRef&&activeAudioRecordersRef.current&&Object.k const _guiVst=masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false)?masteringSettings.gui_open_path:null;if(!_guiVst){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}else{clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}}}else{// C: PWR OFF / bypass / chain rỗng. GUI marker còn + session sống -> // giữ WARM (WebAudio bypass + PDC 0, session/node/ws sống cho GUI // native); không marker -> teardown thật (hành vi cũ). -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)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ị +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). +try{const _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();}}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 → @@ -1813,7 +1832,13 @@ const mainResumeRef=useRef(null);currentTimeRef.current=currentTime;// M22 Bug C // engine-derived nhảy LÙI về vị trí trước OFF (ph_jump d âm lớn). Rebase: // dời startOffsetTimeRef/startAudioTimeRef về wall hiện tại + anchor=ps → // engine clock nối lại đúng chỗ, playhead liên tục (lệch ~latencyTotal). -window.__fxRtWakeRebase=function(){try{if(!fxRt||!fxRt.active)return;const _c=getAudioContext();const _wall=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));const _pa=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:null;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) +window.__fxRtWakeRebase=function(){try{if(!fxRt||!fxRt.active)return;const _c=getAudioContext();const _wall=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));const _pa=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:null;if(fxRtWarmPlayHold){// M23 Bug C: wake tu OFF-giua-play — off/at DA rebase ve content tai OFF; +// 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 +// 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. // __phSnapAll: snapshot refs/fxRt tai diem goi (chi log khi cua so dang mo). window.__phArmPwr=function(dir){try{_phDiagArmedUntil=Date.now()+6000;_dbgLog('ph_pwr','dir='+dir+' cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' off='+(startOffsetTimeRef.current==null?'na':startOffsetTimeRef.current.toFixed(3))+' at='+(startAudioTimeRef.current==null?'na':startAudioTimeRef.current.toFixed(3))+' ctx='+(getAudioContext().currentTime||0).toFixed(3)+' fx='+(fxRt?'act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' start='+(fxRt.starting?1:0)+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' lat='+(fxRt.latencyTotal||0)+' pdc='+(fxRt.pdcCalibMs||0)+' sid='+(fxRt.sessionId||'na'):'na')+' freeze='+(_fxRtSeekFreeze?1:0)+' snap='+(_fxRtPlayheadSnapPending?1:0)+' wh='+(wrapHoldRef.current?wrapHoldRef.current.end.toFixed(3)+'/'+wrapHoldRef.current.v0.toFixed(3):'na'));}catch(e){try{_dbgLog('ph_pwr','err '+String(e));}catch(e2){}}};window.__phSnapAll=function(tag){try{if(!_phArmed())return;_dbgLog(tag,'cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' off='+(startOffsetTimeRef.current==null?'na':startOffsetTimeRef.current.toFixed(3))+' at='+(startAudioTimeRef.current==null?'na':startAudioTimeRef.current.toFixed(3))+' ctx='+(getAudioContext().currentTime||0).toFixed(3)+' fx='+(fxRt?'act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' start='+(fxRt.starting?1:0)+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' lat='+(fxRt.latencyTotal||0)+' pdc='+(fxRt.pdcCalibMs||0)+' sid='+(fxRt.sessionId||'na'):'na')+' freeze='+(_fxRtSeekFreeze?1:0)+' snap='+(_fxRtPlayheadSnapPending?1:0)+' wh='+(wrapHoldRef.current?wrapHoldRef.current.end.toFixed(3)+'/'+wrapHoldRef.current.v0.toFixed(3):'na'));}catch(e){try{_dbgLog(tag,'err '+String(e));}catch(e2){}}};// ── Keyboard Shortcuts ── @@ -2199,7 +2224,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=202609071038`);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=202609071125`);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 @@ -2490,7 +2515,9 @@ try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}}e // C: GUI master native đang mở (marker + slot active trong chain) -> KHÔNG // arm — session cần sống cho editor; teardown sẽ giết editor + mất preset. // closeMasterGui xoá marker -> settings effect (OP12) arm lại khi dừng. -try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}const _stopGuiVst=masteringSettings&&masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false);clearTimeout(fxRtWarmIdleTimer);if(_stopGuiVst){fxRtWarmIdleTimer=null;}else{fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}}_nativePlayEpoch++;// hủy mọi render/decode pending của lần play cũ +try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}const _stopGuiVst=masteringSettings&&masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false);clearTimeout(fxRtWarmIdleTimer);if(_stopGuiVst){fxRtWarmIdleTimer=null;}else{fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}// M23 Bug C: dung han (fade) — ha hold + PDC ve 0 (am dang fade -> khong +// nghe jump; warm sau do dry delay = 0 nhu cu). +fxRtWarmPlayHold=false;try{fxRtApplyPdc();}catch(e){}}_nativePlayEpoch++;// hủy mọi render/decode pending của lần play cũ // V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer // cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref). bridgeNoteEpochRef.current++;// D18e: seam ring-out - chup pitch dang keu (timelinePitchesRef) co NOTE_OFF diff --git a/app/templates/index.html b/app/templates/index.html index a8b844e..f3f90c5 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +