fix(fxrt): mo GUI VST khong gay crackle - GUI spawn BELOW_NORMAL, mien watchdog khi mo native GUI, realtime loop ABOVE_NORMAL

This commit is contained in:
2026-08-24 16:38:52 +07:00
parent 9137e7e276
commit 3a3f46b7d3
7 changed files with 52 additions and 13 deletions
+22 -6
View File
@@ -1654,6 +1654,10 @@ const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, w
// 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ở
// 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
// thread bị block (solo restart, GUI decode, rAF) các frame 'in' dồn lại —
// gửi 1 loạt làm SHM ring 4-slot tràn → mất block → ngắt quãng. Gửi tối đa 1
@@ -7223,6 +7227,10 @@ const ensureAndOpenVstGui = async (track) => {
// can spam. 20 lan x 500ms tao deferred storm (50+ event/moi restart) va
// re-attach view dang attached (crash USER32 0xc000041d). Cap 5.
const openVstGuiRetry = async (instrumentId, bch, attempts) => {
// C++ reloadForGUI (mở lại GUI sau khi đóng) mute audio 1-3s → watchdog
// master-silence false-positive → rebuild + fxRtStop → dry→wet vài giây.
// Exempt watchdog 12s kể từ lần mở này (đủ cho retry + load/reload).
nativeGuiOpenUntilTs = performance.now() + 12000;
const n = Math.min((attempts && attempts > 0) ? attempts : 5, 5);
for (let i = 0; i < n; i++) {
try { await window.NativeBridgeService.openNativeGUI(instrumentId, bch); }
@@ -23362,12 +23370,19 @@ const App = () => {
// giữa các note > 750ms là BÌNH THƯỜNG → false-positive = recovery
// hủy play + restart notes (glitch) — đúng chuỗi log recovery trước.
if ((isPianoRoll || isSectionTab ? nanOut : (pk < 0.001 || nanOut))) {
masterSilenceFramesRef.current++;
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
// NaN = chain CHẾT chắc chắn → rebuild NGAY (3 frame ≈ 50ms).
// pk<0.001 (im lặng nghi ngờ — main) giữ 45 frame (750ms) để loại
// transient gap false-positive. Cooldown 3000 chống rebuild-loop.
if (masterSilenceFramesRef.current > (nanOut ? 3 : 45) && sinceRebuild > 3000) {
// Native VSTi GUI đang mở/reload (reloadForGUI mute 1-3s) — im
// lặng là TỰ NHIÊN (C++ đang rebuild instance), không phải graph
// hỏng. Không rebuild → không stopAllPlayback → không fxRtStop →
// hết chuỗi dry→wet vài giây sau mỗi lần mở GUI track.
if (performance.now() < nativeGuiOpenUntilTs) {
masterSilenceFramesRef.current = 0;
} else {
masterSilenceFramesRef.current++;
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
// NaN = chain CHẾT chắc chắn → rebuild NGAY (3 frame ≈ 50ms).
// pk<0.001 (im lặng nghi ngờ — main) giữ 45 frame (750ms) để loại
// transient gap false-positive. Cooldown 3000 chống rebuild-loop.
if (masterSilenceFramesRef.current > (nanOut ? 3 : 45) && sinceRebuild > 3000) {
masterSilenceFramesRef.current = 0;
lastMasterRebuildTimeRef.current = performance.now();
console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');
@@ -23408,6 +23423,7 @@ const App = () => {
}
} catch (e) { console.warn('[Recovery] rebuild error:', e); }
}
}
} else {
masterSilenceFramesRef.current = 0;
}
+14 -4
View File
@@ -285,7 +285,10 @@ const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node
// (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;// Pace input realtime: worklet sản xuất đúng realtime, nhưng sau khi main
let fxRtKeepForRestart=false;// 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
// thread bị block (solo restart, GUI decode, rAF) các frame 'in' dồn lại —
// gửi 1 loạt làm SHM ring 4-slot tràn → mất block → ngắt quãng. Gửi tối đa 1
// burst (4 block ~21ms) mỗi 21ms → ring luôn kịp tiêu thụ, audio hồi phục
@@ -651,7 +654,10 @@ try{const ok=await window.NativeBridgeService.loadInstrument(lc.path,/\.dll$/i.t
// som → C++ defer va tu fulfil khi assign() xong (pending-GUI sweep) → khong
// can spam. 20 lan x 500ms tao deferred storm (50+ event/moi restart) va
// re-attach view dang attached (crash USER32 0xc000041d). Cap 5.
const openVstGuiRetry=async(instrumentId,bch,attempts)=>{const n=Math.min(attempts&&attempts>0?attempts:5,5);for(let i=0;i<n;i++){try{await window.NativeBridgeService.openNativeGUI(instrumentId,bch);}catch(e){console.warn('[Bridge] openNativeGUI retry',i+1,e);}await new Promise(r=>setTimeout(r,500));}return true;};// Wheel trên range input (Vol/Pan TCP): scale step theo deltaY — trackpad
const openVstGuiRetry=async(instrumentId,bch,attempts)=>{// C++ reloadForGUI (mở lại GUI sau khi đóng) mute audio 1-3s → watchdog
// master-silence false-positive → rebuild + fxRtStop → dry→wet vài giây.
// Exempt watchdog 12s kể từ lần mở này (đủ cho retry + load/reload).
nativeGuiOpenUntilTs=performance.now()+12000;const n=Math.min(attempts&&attempts>0?attempts:5,5);for(let i=0;i<n;i++){try{await window.NativeBridgeService.openNativeGUI(instrumentId,bch);}catch(e){console.warn('[Bridge] openNativeGUI retry',i+1,e);}await new Promise(r=>setTimeout(r,500));}return true;};// Wheel trên range input (Vol/Pan TCP): scale step theo deltaY — trackpad
// smooth gửi deltaY nhỏ (~1-10px/sự kiện) nhưng rất nhiều sự kiện; wheel
// notch gửi ~100-120px. Tích lũy theo key → trackpad lăn 100px = 1 nấc
// (0.5dB vol / 5 pan), wheel notch giữ nguyên tốc độ cũ. deltaMode:
@@ -1640,14 +1646,18 @@ let nanOut=false;if(isPianoRoll){try{const f=new Float32Array(128);masterBus.ana
// pk<0.001 (im lặng) KHÔNG trigger cho piano roll — rests tự nhiên
// giữa các note > 750ms là BÌNH THƯỜNG → false-positive = recovery
// hủy play + restart notes (glitch) — đúng chuỗi log recovery trước.
if(isPianoRoll||isSectionTab?nanOut:pk<0.001||nanOut){masterSilenceFramesRef.current++;const sinceRebuild=performance.now()-(lastMasterRebuildTimeRef.current||0);// NaN = chain CHẾT chắc chắn → rebuild NGAY (3 frame ≈ 50ms).
if(isPianoRoll||isSectionTab?nanOut:pk<0.001||nanOut){// Native VSTi GUI đang mở/reload (reloadForGUI mute 1-3s) — im
// lặng là TỰ NHIÊN (C++ đang rebuild instance), không phải graph
// hỏng. Không rebuild → không stopAllPlayback → không fxRtStop →
// hết chuỗi dry→wet vài giây sau mỗi lần mở GUI track.
if(performance.now()<nativeGuiOpenUntilTs){masterSilenceFramesRef.current=0;}else{masterSilenceFramesRef.current++;const sinceRebuild=performance.now()-(lastMasterRebuildTimeRef.current||0);// NaN = chain CHẾT chắc chắn → rebuild NGAY (3 frame ≈ 50ms).
// pk<0.001 (im lặng nghi ngờ — main) giữ 45 frame (750ms) để loại
// transient gap false-positive. Cooldown 3000 chống rebuild-loop.
if(masterSilenceFramesRef.current>(nanOut?3:45)&&sinceRebuild>3000){masterSilenceFramesRef.current=0;lastMasterRebuildTimeRef.current=performance.now();console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');try{const pt=currentTimeRef.current;stopAllPlayback();Object.keys(activeTrackNodesRef.current).forEach(k=>{const n=activeTrackNodesRef.current[k];try{if(n&&n.gainNode&&n.gainNode.disconnect)n.gainNode.disconnect();}catch(e){}});activeTrackNodesRef.current={};// Tháo chain cũ khỏi destination rồi rebuild THẬT — initMasterBus
// early-return khi masterBus còn tồn tại → recovery trước đây là
// no-op → chain "state is bad" bị STUCK vĩnh viễn.
try{if(masterBus){try{if(masterBus.analyser)masterBus.analyser.disconnect();}catch(e){}try{if(masterBus.output)masterBus.output.disconnect();}catch(e){}try{if(masterBus.dryOutput)masterBus.dryOutput.disconnect();}catch(e){}}masterBus=null;initMasterBus(getAudioContext());}catch(e){console.warn('[Recovery] initMasterBus error:',e);}// Resume ĐÚNG chế độ play hiện tại (main hoặc sub-tab piano roll)
const curTab=activeTabRef.current;const subSt=subTabsRef.current.find(s=>s.id===curTab);if(subSt&&(subSt.type==='PIANO_ROLL'||subSt.buffer)){const resumeAt=subSt.currentTime||0;if(subSt.type==='PIANO_ROLL')schedulePianoRollMidi(subSt,resumeAt);startSubTabPlayback(subSt,resumeAt);setSubTabs(prev=>prev.map(s=>s.id===curTab?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),isPlaying:true,currentTime:resumeAt}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{setIsPlaying(true);startTrackPlayback(pt);}}catch(e){console.warn('[Recovery] rebuild error:',e);}}}else{masterSilenceFramesRef.current=0;}}else{masterSilenceFramesRef.current=0;}}catch(e){masterSilenceFramesRef.current=0;}}else{masterSilenceFramesRef.current=0;}if(recordingStateRef.current==='RECORDING'){const audioCtx=getAudioContext();const lookahead=0.1;// 100ms
const curTab=activeTabRef.current;const subSt=subTabsRef.current.find(s=>s.id===curTab);if(subSt&&(subSt.type==='PIANO_ROLL'||subSt.buffer)){const resumeAt=subSt.currentTime||0;if(subSt.type==='PIANO_ROLL')schedulePianoRollMidi(subSt,resumeAt);startSubTabPlayback(subSt,resumeAt);setSubTabs(prev=>prev.map(s=>s.id===curTab?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),isPlaying:true,currentTime:resumeAt}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{setIsPlaying(true);startTrackPlayback(pt);}}catch(e){console.warn('[Recovery] rebuild error:',e);}}}}else{masterSilenceFramesRef.current=0;}}else{masterSilenceFramesRef.current=0;}}catch(e){masterSilenceFramesRef.current=0;}}else{masterSilenceFramesRef.current=0;}if(recordingStateRef.current==='RECORDING'){const audioCtx=getAudioContext();const lookahead=0.1;// 100ms
const secondsPerBeat=60.0/(parseInt(bpmRef.current)||120);// Metronome Click Scheduler
while(true){const beatNum=nextMetronomeBeatRef.current;const elapsedBeats=beatNum-recordingStartTimeRef.current/secondsPerBeat;const beatTime=startAudioTimeRef.current+elapsedBeats*secondsPerBeat;if(beatTime<audioCtx.currentTime+lookahead){const isDownbeat=beatNum%4===0;playMetronomeClick(beatTime,isDownbeat);nextMetronomeBeatRef.current++;}else{break;}}// Throttled Live Preview compilation (every 200ms)
const now=Date.now();if(now-lastTempCompileTimeRef.current>200){lastTempCompileTimeRef.current=now;// Audio preview