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:
+37
-5
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user