feat(native): realtime master FX chain + fx-gui fast start, hide/show, non-topmost

- FxGuiServer: HWND_TOP (no longer always-on-top), /hide + /show endpoints,
  onStarted callback prints SF_FXGUI_PORT before plugin load (fast connect)
- RenderFxJob: RealtimeFxChain (mutex+worker, SEH-guarded, bypass slots,
  deferred Vst3Fx dtor); --fx-gui split into parse/create/load-attach stages
- main: FreeConsole unless SF_KEEP_CONSOLE; control type 6 SET_FX_CHAIN feeds
  g_fxChain on the audio loop; shutdown before CoUninitialize
- plugins.py: CREATE_NO_WINDOW on both Popen paths
- lib.rs: set_fx_chain command -> push_control(6)
- app.jsx/nativeBridgeService: setFxChain sync from MasteringModal; panel close
  uses /hide + refs to fix closure bug; VstGuiEmbed rect refresh -> /show
This commit is contained in:
2026-08-19 09:59:13 +07:00
parent c6de0720f4
commit da0f8fa40e
11 changed files with 391 additions and 40 deletions
+37 -5
View File
@@ -11909,6 +11909,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
const boxRef = React.useRef(null);
const deadRef = React.useRef(false);
const screenRectRef = React.useRef(null);
const shownRef = React.useRef(false);
const [dead, setDead] = React.useState(false);
const post = (payload) => { try { fetch(url + '/input', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(() => {}); } catch (e) {} };
React.useEffect(() => {
@@ -11920,7 +11921,12 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
if (!deadRef.current) { deadRef.current = true; setDead(true); onDead && onDead(); }
});
}, 2000);
return () => { clearInterval(tick); clearInterval(ping); };
return () => {
clearInterval(tick); clearInterval(ping);
// Panel closed -> hide the native window (bridge stays alive so
// reopening is fast). The "Đóng GUI" button still POSTs /close (exit).
try { fetch(url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// Embed panel screen rect (physical px) for the bridge's anchored real-input
@@ -11928,6 +11934,15 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
// not running under Tauri. Refreshed ~500ms so scroll/resize stays current.
React.useEffect(() => {
let alive = true;
const maybeShow = () => {
const r = screenRectRef.current;
if (!shownRef.current && r && r.w >= 4 && r.h >= 4) {
shownRef.current = true;
// Show the native window anchored over the panel (bridge stays
// alive; hidden again by /hide on unmount).
try { fetch(url + '/show', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rect: r }) }).catch(() => {}); } catch (e) {}
}
};
const refresh = () => {
const box = boxRef.current;
if (!box) return;
@@ -11940,9 +11955,11 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
if (!alive || !boxRef.current) return;
const rb = boxRef.current.getBoundingClientRect();
screenRectRef.current = { x: Math.round(pos.x + rb.left * sf), y: Math.round(pos.y + rb.top * sf), w: Math.round(rb.width * sf), h: Math.round(rb.height * sf) };
maybeShow();
}).catch(() => {});
} else {
screenRectRef.current = { x: Math.round((window.screenX || 0) + r.left), y: Math.round((window.screenY || 0) + r.top), w: Math.round(r.width), h: Math.round(r.height) };
maybeShow();
}
} catch (e) {}
};
@@ -12085,10 +12102,14 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 3)
const [fxGuiOpen, setFxGuiOpen] = React.useState(null); // { path, url } | null
const fxGuiOpenRef = React.useRef(null);
fxGuiOpenRef.current = fxGuiOpen;
const closeFxGui = () => {
if (fxGuiOpen) { try { fetch(fxGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} setFxGuiOpen(null); }
const g = fxGuiOpenRef.current;
if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} }
setFxGuiOpen(null);
};
React.useEffect(() => () => { if (fxGuiOpen) { try { fetch(fxGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
React.useEffect(() => () => { const g = fxGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (idx === activeVstIdx) setActiveVstIdx(-1); setVstChain(vstChain.filter((_, i) => i !== idx)); };
const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };
@@ -12621,12 +12642,23 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 2)
const [masterGuiOpen, setMasterGuiOpen] = React.useState(null); // { path, url } | null
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
const masterGuiOpenRef = React.useRef(null);
masterGuiOpenRef.current = masterGuiOpen;
const closeMasterGui = () => {
if (masterGuiOpen) { try { fetch(masterGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} setMasterGuiOpen(null); }
const g = masterGuiOpenRef.current;
if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} }
setMasterGuiOpen(null);
setActiveMasterVstPath(null);
};
React.useEffect(() => () => { if (masterGuiOpen) { try { fetch(masterGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next }));
// T2.4: push the master VST FX chain to the realtime bridge (live master
// mix). v1: master chain only — per-track vstChain stays offline (defer).
React.useEffect(() => {
if (!window.NativeBridgeService || !window.NativeBridgeService.setFxChain) return;
const chain = (ozState.vstFxChain || []).map(s => ({ path: s.path || '', bypass: !!s.bypass }));
window.NativeBridgeService.setFxChain(JSON.stringify(chain));
}, [ozState.vstFxChain]);
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
File diff suppressed because one or more lines are too long
@@ -169,6 +169,18 @@
return true;
},
/**
* 6. REALTIME MASTER FX CHAIN (T2.4)
* chainJson: JSON array [{path, bypass}, ...] applied to the live master
* mix via the bridge control queue (type 6). Async — never blocks UI.
*/
setFxChain: function (chainJson) {
if (!this._tauri()) return false;
window.__TAURI__.core.invoke('set_fx_chain', { chainJson: chainJson })
.catch(function (e) { console.warn('[BridgeService] set_fx_chain:', e); });
return true;
},
/**
* 5. AUDIO SINK: register consumer of PCM frames from the bridge.
* cb(l: Float32Array, r: Float32Array)