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:
+27
-4
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608311000" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608311100" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user