diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 8066ed3..ba1a09e 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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 diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index bf366e3..4e536c0 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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;isetTimeout(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(newZoom50000)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;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeOutMs/1000*sr));for(let i=edData.length-fadeSamples;i{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) diff --git a/app/templates/index.html b/app/templates/index.html index a02e51d..d3bddbc 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +