diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 07c7a46..e32b449 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -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= 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} diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 4cf3610..35fcafc 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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; } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index bceef3e..61992cd 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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;isetTimeout(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;isetTimeout(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()(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(beatTime200){lastTempCompileTimeRef.current=now;// Audio preview diff --git a/app/templates/index.html b/app/templates/index.html index 8410159..9defd69 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +