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
+10 -2
View File
@@ -698,12 +698,20 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
raise HTTPException(status_code=500, detail="Không ghi được job file cho bridge.")
cmd = [exe, "--fx-gui" if req.embed else "--open-fx-gui", job_path]
try:
# fx-gui spawn o BELOW_NORMAL: process nay load instance VST THU HAI
# (CPU spike vai giay) - neu o NORMAL se preempt realtime audio loop
# (fx_vst_bridge --realtime-fx, thread NORMAL) -> underrun -> am "bop
# nghet" ngay luc mo GUI roi moi hoi phuc. BELOW_NORMAL cho load GUI
# khong canh tranh thread audio.
_gui_creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
if os.name == "nt":
_gui_creationflags |= getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0)
if req.embed:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True,
close_fds=os.name != "nt",
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
creationflags=_gui_creationflags)
# Bridge prints SF_FXGUI_PORT=<port> after binding its HTTP server.
embed_url = None
try:
@@ -741,7 +749,7 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
"embed_url": embed_url, "cmd": cmd}
proc = subprocess.Popen(cmd, close_fds=os.name != "nt",
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
creationflags=_gui_creationflags)
proc._sf_plugin_path = plugin_path
_register_fx_gui_process(proc)
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
+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
+1 -1
View File
@@ -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=202608242300" defer></script>
<script src="/static/js/app.precompiled.js?v=202608241630" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+5
View File
@@ -162,6 +162,11 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
// cuối. (winmm đã link sẵn — CMakeLists fx_vst_bridge.)
#ifdef _WIN32
timeBeginPeriod(1);
// Thread audio realtime ABOVE_NORMAL: khong bi tien trinh NORMAL khac
// (fx-gui spawn + VST load instance thu 2, native VSTi GUI open...) preempt
// -> underrun -> am bi "bop nghet"/ngat quan. ChannelWorker (main.cpp)
// da dung THREAD_PRIORITY_ABOVE_NORMAL.
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
#endif
// Chờ chain ĐẦU TIÊN được worker build xong (gen>=1) trước khi READY.
// setChain() là ASYNC — worker thread load VST3 + preset mất 1-4s; nếu set
Binary file not shown.