V21: keep master FX session warm on user stop - no 4s VST reload on replay

- stopAllPlayback(fade=true) giữ master FX session (không teardown) → play
  kế tiếp FX xử lý ngay, hết cửa sổ dry ~4s khi reload VST
- per-track FX vẫn teardown khi user stop (VSTi nhiều track nặng CPU)
- idle timeout 120s (FX_RT_WARM_IDLE_MS): ngừng chơi lâu → teardown giải
  phóng CPU/VST
- fxRtStart/fxRtStop/startSubTabPlayback hủy idle timer khi play
This commit is contained in:
2026-08-26 18:31:25 +07:00
parent 3e6080603c
commit b95c63e7db
3 changed files with 48 additions and 14 deletions
+27 -4
View File
@@ -1714,9 +1714,14 @@ const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, w
// 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) / bridge-down / new-project / export-end KHÔNG set →
// teardown bình thường.
// stop thật (fade=true) giữ master FX warm + idle timeout (V21); bridge-down /
// new-project / export-end → teardown bình thường.
let fxRtKeepForRestart = false;
// V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX
// ngay, không chờ reload VST ~4s. Idle timeout: ngừng chơi lâu → teardown
// giải phóng CPU/VST (chấp nhận ~4s delay ở play sau khi idle lâu).
const FX_RT_WARM_IDLE_MS = 120000;
let fxRtWarmIdleTimer = null;
// Native VSTi GUI open/reload đang chạy (C++ reloadForGUI mute 1-3s khi mở
// lại GUI sau khi đóng) → master-silence watchdog (750ms) sẽ false-positive →
// rebuild + fxRtStop → dry→wet vài giây. Exempt watchdog trong cửa sổ này.
@@ -1797,6 +1802,9 @@ async function fxRtSetChain(chain) {
}
}
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);
if (fxRt.active) {
@@ -1970,6 +1978,7 @@ async function fxRtStart() {
}
}
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).
@@ -23645,6 +23654,9 @@ const App = () => {
const startSubTabPlayback = (st, offsetWallTime) => {
const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade
// V21: section play không gọi fxRtStart — hủy idle timer thủ công (không
// để master FX bị teardown giữa lúc section đang phát).
if (fxRtWarmIdleTimer) { clearTimeout(fxRtWarmIdleTimer); fxRtWarmIdleTimer = null; }
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
if (!st.buffer) {
st.buffer = context.createBuffer(1, 128, context.sampleRate);
@@ -25679,13 +25691,24 @@ const App = () => {
// Realtime VST FX: dừng session + khôi phục output→analyser. Nhánh restart
// (loop/seek/reschedule/solo...) set fxRtKeepForRestart=true → GIỮ session
// (fxRtStart early-return khi active + chain khớp) → hết bypass+reload ~4s
// mỗi pass loop. User stop thật (fade=true) → teardown bình thường.
const _keepFx = fxRtKeepForRestart;
// mỗi pass loop. User stop thật (fade=true) → V21 giữ master FX warm (idle
// timeout FX_RT_WARM_IDLE_MS) — play kế tiếp FX ngay; per-track teardown.
const _keepFx = fxRtKeepForRestart || !!fade;
fxRtKeepForRestart = false;
if (!_keepFx) {
try { fxRtStop(); } catch (e) { console.warn('[FX RT] stop error:', e); }
// Realtime VST FX per-track: dừng mọi session track + restore chain trực tiếp.
try { fxRtTrackStopAll(); } catch (e) { console.warn('[FX RT track] stop error:', e); }
} else if (fade) {
// V21: user stop — GIỮ master FX session warm (play kế tiếp có FX ngay,
// không chờ reload VST ~4s). Per-track vẫn teardown (VSTi nhiều track nặng).
// Idle timeout (FX_RT_WARM_IDLE_MS) giải phóng CPU khi ngừng chơi lâu.
try { fxRtTrackStopAll(); } catch (e) { console.warn('[FX RT track] stop error:', e); }
clearTimeout(fxRtWarmIdleTimer);
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ũ
// V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer
+20 -9
View File
@@ -294,9 +294,12 @@ const len=Math.floor(pcm.length/channels);const bps=4;const dataBytes=len*bps*ch
const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,lastInSent:0,chainKey:null,latencyTotal:0,fadedIn:false};// 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) / bridge-down / new-project / export-end KHÔNG set →
// teardown bình thường.
let fxRtKeepForRestart=false;// Native VSTi GUI open/reload đang chạy (C++ reloadForGUI mute 1-3s khi mở
// stop thật (fade=true) giữ master FX warm + idle timeout (V21); bridge-down /
// new-project / export-end → teardown bình thường.
let fxRtKeepForRestart=false;// V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX
// ngay, không chờ reload VST ~4s. Idle timeout: ngừng chơi lâu → teardown
// giải phóng CPU/VST (chấp nhận ~4s delay ở play sau khi idle lâu).
const FX_RT_WARM_IDLE_MS=120000;let fxRtWarmIdleTimer=null;// Native VSTi GUI open/reload đang chạy (C++ reloadForGUI mute 1-3s khi mở
// lại GUI sau khi đóng) → master-silence watchdog (750ms) sẽ false-positive →
// rebuild + fxRtStop → dry→wet vài giây. Exempt watchdog trong cửa sổ này.
let nativeGuiOpenUntilTs=0;// Pace input realtime: worklet sản xuất đúng realtime, nhưng sau khi main
@@ -313,7 +316,9 @@ function fxRtChainKey(chain){return(chain||[]).map(s=>(s.type||'')+':'+(s.type==
// default (passthrough) → power ON nghe như OFF. Merge ở đây làm chainKey đổi
// → fxRtStart tự restart session với preset thật.
return(ms.chain||[]).filter(s=>s&&s.active!==false&&(s.type!=='vst3'||s.path)).map(s=>s.type==='vst3'&&_capturedPresetByPath[s.path]?{...s,preset_b64:_capturedPresetByPath[s.path]}:s);}// Param master chain entry → C++ BuiltinFx key (mirror Python _BUILTIN_PARAM_MAP).
const FX_RT_PARAM_MAP={eq:[['g1','eqLowGain'],['g2','eqMid1Gain'],['g3','eqMid2Gain'],['g4','eqHighGain']],compressor:[['threshold','compThreshold'],['ratio','compRatio'],['makeup','compMakeup']],limiter:[['ceiling','limThreshold'],['mode','limMode'],['lookahead_ms','limLookahead']],exciter:[['drive','excDrive']],rebalance:[['mid','rebalMid'],['side','rebalSide']],imager:[['w1','w1'],['w2','w2'],['w3','w3'],['w4','w4']],maximizer:[['boost_db','maxGain'],['soft_clip','maxSoftClip'],['upward','maxUpward'],['ceiling_db','ceiling']]};function fxRtEntryParams(entry,ms){const out=[];if(!entry||entry.type==='vst3')return out;const src=ms||window.currentMasteringSettings||{};const push=(k,v)=>{const n=Number(v);if(isFinite(n))out.push({key:k,value:n});};const map=FX_RT_PARAM_MAP[entry.type]||[];map.forEach(([cppKey,jsKey])=>{if(entry.params&&entry.params[cppKey]!==undefined)push(cppKey,entry.params[cppKey]);else if(entry.params&&entry.params[jsKey]!==undefined)push(jsKey,entry.params[jsKey]);else if(src[jsKey]!==undefined)push(cppKey,src[jsKey]);});if(entry.type==='eqpro'){if(entry.params&&entry.params.amount!==undefined)push('amount',entry.params.amount);const bands=entry.params&&entry.params.bands||[];bands.forEach((b,i)=>{['freq','gain','q'].forEach(k=>{if(b&&b[k]!==undefined)push('band'+i+'_'+k,b[k]);});});}return out;}function fxRtPushAllParams(){if(!fxRt||!fxRt.ws||fxRt.ws.readyState!==1)return;const chain=fxRtChain();chain.forEach((s,i)=>{fxRtEntryParams(s,null).forEach(p=>{try{fxRt.ws.send(JSON.stringify({cmd:'set_param',slot:i,key:p.key,value:p.value}));}catch(e){}});});}async function fxRtSetChain(chain){const sid=fxRt.sessionId;if(!sid||!fxRt.ws||fxRt.ws.readyState!==1)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});try{await window.SonicAPI.fxRealtimeSetChain({session_id:sid,fx_chain:chainPayload});try{fxRtPushAllParams();}catch(e){}}catch(e){console.warn('[FX RT] set-chain fail:',e);}}async function fxRtStart(){const chain=fxRtChain();const ck=fxRtChainKey(chain);if(fxRt.active){// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart.
const FX_RT_PARAM_MAP={eq:[['g1','eqLowGain'],['g2','eqMid1Gain'],['g3','eqMid2Gain'],['g4','eqHighGain']],compressor:[['threshold','compThreshold'],['ratio','compRatio'],['makeup','compMakeup']],limiter:[['ceiling','limThreshold'],['mode','limMode'],['lookahead_ms','limLookahead']],exciter:[['drive','excDrive']],rebalance:[['mid','rebalMid'],['side','rebalSide']],imager:[['w1','w1'],['w2','w2'],['w3','w3'],['w4','w4']],maximizer:[['boost_db','maxGain'],['soft_clip','maxSoftClip'],['upward','maxUpward'],['ceiling_db','ceiling']]};function fxRtEntryParams(entry,ms){const out=[];if(!entry||entry.type==='vst3')return out;const src=ms||window.currentMasteringSettings||{};const push=(k,v)=>{const n=Number(v);if(isFinite(n))out.push({key:k,value:n});};const map=FX_RT_PARAM_MAP[entry.type]||[];map.forEach(([cppKey,jsKey])=>{if(entry.params&&entry.params[cppKey]!==undefined)push(cppKey,entry.params[cppKey]);else if(entry.params&&entry.params[jsKey]!==undefined)push(jsKey,entry.params[jsKey]);else if(src[jsKey]!==undefined)push(cppKey,src[jsKey]);});if(entry.type==='eqpro'){if(entry.params&&entry.params.amount!==undefined)push('amount',entry.params.amount);const bands=entry.params&&entry.params.bands||[];bands.forEach((b,i)=>{['freq','gain','q'].forEach(k=>{if(b&&b[k]!==undefined)push('band'+i+'_'+k,b[k]);});});}return out;}function fxRtPushAllParams(){if(!fxRt||!fxRt.ws||fxRt.ws.readyState!==1)return;const chain=fxRtChain();chain.forEach((s,i)=>{fxRtEntryParams(s,null).forEach(p=>{try{fxRt.ws.send(JSON.stringify({cmd:'set_param',slot:i,key:p.key,value:p.value}));}catch(e){}});});}async function fxRtSetChain(chain){const sid=fxRt.sessionId;if(!sid||!fxRt.ws||fxRt.ws.readyState!==1)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});try{await window.SonicAPI.fxRealtimeSetChain({session_id:sid,fx_chain:chainPayload});try{fxRtPushAllParams();}catch(e){}}catch(e){console.warn('[FX RT] set-chain fail:',e);}}async function fxRtStart(){// 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);if(fxRt.active){// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart.
if(!fxRt.node||fxRt.node.context!==getAudioContext())fxRtStop(true);// Chain thay đổi (insert/remove/bypass/reorder/preset GUI) — KHÔNG restart
// (ngắt âm + crackle): set_chain lên session ĐANG CHẠY → bridge worker
// load chain mới (audio giữ chain cũ tới khi swap) → swap atomic mượt.
@@ -355,7 +360,7 @@ fxRtSendFrames(d);};// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn
// masterFxDryGain -> pdcDelay -> output (gain 1); nhanh fx noi masterFxGain
// (gain 0) -> output. Tin hieu 'started' tu worklet -> ramp 2 gain -> switch
// muot, het click hard-rewire.
try{masterBus.outputAnalyser.disconnect(masterBus.output);}catch(e){}masterBus.outputAnalyser.connect(fxRt.node);fxRt.node.connect(masterBus.masterFxGain);fxRt.chainKey=ck;}catch(e){console.warn('[FX RT] setup fail:',e);fxRtStop(true);}}function fxRtStop(silent){// Phải bump token TRƯỚC guard early-return: lúc fxRtStart đang in-flight thì
try{masterBus.outputAnalyser.disconnect(masterBus.output);}catch(e){}masterBus.outputAnalyser.connect(fxRt.node);fxRt.node.connect(masterBus.masterFxGain);fxRt.chainKey=ck;}catch(e){console.warn('[FX RT] setup fail:',e);fxRtStop(true);}}function fxRtStop(silent){if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}// Phải bump token TRƯỚC guard early-return: lúc fxRtStart đang in-flight thì
// active/ws/node/sessionId đều chưa có → guard sẽ return, nhưng start vẫn
// phải bị hủy (token mismatch ở checkpoint).
fxRt.startToken++;fxRt.starting=false;if(fxRt.pumpTimer){clearInterval(fxRt.pumpTimer);fxRt.pumpTimer=null;}fxRt.inQueue=[];if(!fxRt.active&&!fxRt.ws&&!fxRt.node&&!fxRt.sessionId)return;let _fxStopNode=null;try{if(fxRt.node){_fxStopNode=fxRt.node;if(masterBus&&masterBus.masterFxGain&&masterBus.masterFxDryGain){// Crossfade deactivation: ramp dry 0->1 / fx 1->0 roi MOI disconnect
@@ -1664,7 +1669,9 @@ const loadServerWaveform=async fileId=>{try{const resp=await fetch(`${API_AUDIO}
const pollTaskResult=async(taskId,maxPoll=10)=>{for(let i=0;i<maxPoll;i++){await new Promise(r=>setTimeout(r,1500));try{const resp=await fetch(`${API_TASKS}/${taskId}`);if(!resp.ok)continue;const data=await resp.json();if(data.status==='SUCCESS')return data.result;if(data.status==='FAILURE')throw new Error(data.error||'Task failed');}catch(err){throw err;}}throw new Error('Task polling timeout');};// ── Wheel Zoom ──
useEffect(()=>{const timeline=timelineWrapperRef.current;if(!timeline)return;const handleWheel=e=>{if(e.ctrlKey||e.metaKey){e.preventDefault();const zoomFactor=e.deltaY>0?0.9:1.1;setZoom(prevZoom=>{let newZoom=prevZoom*zoomFactor;if(newZoom<minZoom)newZoom=minZoom;if(newZoom>50000)newZoom=50000;requestAnimationFrame(()=>{const centerTime=currentTimeRef.current;const newCenterX=centerTime*newZoom;const viewportWidth=timeline.clientWidth;timeline.scrollLeft=newCenterX-viewportWidth/2;});return newZoom;});}else if(e.shiftKey){e.preventDefault();timeline.scrollLeft+=e.deltaY;}};timeline.addEventListener('wheel',handleWheel,{passive:false});return()=>timeline.removeEventListener('wheel',handleWheel);},[minZoom]);// ── Update Playhead ──
const startSubTabPlayback=(st,offsetWallTime)=>{const context=getAudioContext();_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade
restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip gain
// V21: section play không gọi fxRtStart — hủy idle timer thủ công (không
// để master FX bị teardown giữa lúc section đang phát).
if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip gain
if(!st.buffer){st.buffer=context.createBuffer(1,128,context.sampleRate);}const speed=st.speed||1.0;const offsetBuffer=offsetWallTime*speed;const eff=st.buffer.getChannelData(0);const edBuffer=context.createBuffer(1,eff.length,st.buffer.sampleRate);const edData=edBuffer.getChannelData(0);edData.set(eff);const fx=st.effects||{};if(fx.reverse){const reversed=new Float32Array(edData);for(let i=0;i<edData.length;i++)reversed[i]=edData[edData.length-1-i];edBuffer.copyToChannel(reversed,0);}if(fx.gainDb!==0){const gain=Math.pow(10,fx.gainDb/20);for(let i=0;i<edData.length;i++)edData[i]=Math.max(-1,Math.min(1,edData[i]*gain));}if(fx.fadeInMs>0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i<fadeSamples;i++)edData[i]=edData[i]*(i/fadeSamples);}if(fx.fadeOutMs>0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeOutMs/1000*sr));for(let i=edData.length-fadeSamples;i<edData.length;i++){edData[i]=edData[i]*((edData.length-1-i)/fadeSamples);}}const source=context.createBufferSource();source.buffer=edBuffer;source.playbackRate.value=speed;activePlaybackSpeedRef.current=speed;// Graph Editor Automation Node Chain (15_GRAPH_EDIT.md §3)
// source → volumeGainNode → pannerNode → fadeGainNode → destination
const volumeGainNode=context.createGain();volumeGainNode.gain.setValueAtTime(1.0,context.currentTime);const pannerNode=context.createStereoPanner();pannerNode.pan.setValueAtTime(0.0,context.currentTime);const fadeGainNode=context.createGain();fadeGainNode.gain.setValueAtTime(1.0,context.currentTime);// Schedule volume automation nodes
@@ -2038,9 +2045,13 @@ stopAllPlayback();const prCtx=getAudioContext();const prTNode=activeTrackNodesRe
try{if(masterBus&&masterBus.output&&masterBus.output.gain){const _actx=getAudioContext();masterBus.output.gain.cancelScheduledValues(_actx.currentTime);masterBus.output.gain.setValueAtTime(1,_actx.currentTime);}}catch(e){}// Realtime VST FX: dừng session + khôi phục output→analyser. Nhánh restart
// (loop/seek/reschedule/solo...) set fxRtKeepForRestart=true → GIỮ session
// (fxRtStart early-return khi active + chain khớp) → hết bypass+reload ~4s
// mỗi pass loop. User stop thật (fade=true) → teardown bình thường.
const _keepFx=fxRtKeepForRestart;fxRtKeepForRestart=false;if(!_keepFx){try{fxRtStop();}catch(e){console.warn('[FX RT] stop error:',e);}// Realtime VST FX per-track: dừng mọi session track + restore chain trực tiếp.
try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}}_nativePlayEpoch++;// hủy mọi render/decode pending của lần play cũ
// mỗi pass loop. User stop thật (fade=true) → V21 giữ master FX warm (idle
// timeout FX_RT_WARM_IDLE_MS) — play kế tiếp FX ngay; per-track teardown.
const _keepFx=fxRtKeepForRestart||!!fade;fxRtKeepForRestart=false;if(!_keepFx){try{fxRtStop();}catch(e){console.warn('[FX RT] stop error:',e);}// Realtime VST FX per-track: dừng mọi session track + restore chain trực tiếp.
try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}}else if(fade){// V21: user stop — GIỮ master FX session warm (play kế tiếp có FX ngay,
// không chờ reload VST ~4s). Per-track vẫn teardown (VSTi nhiều track nặng).
// Idle timeout (FX_RT_WARM_IDLE_MS) giải phóng CPU khi ngừng chơi lâu.
try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}clearTimeout(fxRtWarmIdleTimer);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ũ
// 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++;try{clearAllPendingNoteOffs();}catch(e){}_cc11Epoch++;// CC11 timers lan play cu vo hieu (da clear o tren)