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:
@@ -730,7 +730,8 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
|||||||
if req.embed:
|
if req.embed:
|
||||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT, text=True,
|
stderr=subprocess.STDOUT, text=True,
|
||||||
close_fds=os.name != "nt")
|
close_fds=os.name != "nt",
|
||||||
|
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||||
# Bridge prints SF_FXGUI_PORT=<port> after binding its HTTP server.
|
# Bridge prints SF_FXGUI_PORT=<port> after binding its HTTP server.
|
||||||
embed_url = None
|
embed_url = None
|
||||||
try:
|
try:
|
||||||
@@ -754,7 +755,8 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
|||||||
_register_fx_gui_process(proc)
|
_register_fx_gui_process(proc)
|
||||||
return {"success": True, "started": True, "already_running": False,
|
return {"success": True, "started": True, "already_running": False,
|
||||||
"embed_url": embed_url, "cmd": cmd}
|
"embed_url": embed_url, "cmd": cmd}
|
||||||
proc = subprocess.Popen(cmd, close_fds=os.name != "nt")
|
proc = subprocess.Popen(cmd, close_fds=os.name != "nt",
|
||||||
|
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||||
proc._sf_plugin_path = plugin_path
|
proc._sf_plugin_path = plugin_path
|
||||||
_register_fx_gui_process(proc)
|
_register_fx_gui_process(proc)
|
||||||
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
|
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
|
||||||
|
|||||||
+37
-5
@@ -11909,6 +11909,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
|||||||
const boxRef = React.useRef(null);
|
const boxRef = React.useRef(null);
|
||||||
const deadRef = React.useRef(false);
|
const deadRef = React.useRef(false);
|
||||||
const screenRectRef = React.useRef(null);
|
const screenRectRef = React.useRef(null);
|
||||||
|
const shownRef = React.useRef(false);
|
||||||
const [dead, setDead] = React.useState(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) {} };
|
const post = (payload) => { try { fetch(url + '/input', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(() => {}); } catch (e) {} };
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -11920,7 +11921,12 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
|||||||
if (!deadRef.current) { deadRef.current = true; setDead(true); onDead && onDead(); }
|
if (!deadRef.current) { deadRef.current = true; setDead(true); onDead && onDead(); }
|
||||||
});
|
});
|
||||||
}, 2000);
|
}, 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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [url]);
|
}, [url]);
|
||||||
// Embed panel screen rect (physical px) for the bridge's anchored real-input
|
// 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.
|
// not running under Tauri. Refreshed ~500ms so scroll/resize stays current.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let alive = true;
|
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 refresh = () => {
|
||||||
const box = boxRef.current;
|
const box = boxRef.current;
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
@@ -11940,9 +11955,11 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
|||||||
if (!alive || !boxRef.current) return;
|
if (!alive || !boxRef.current) return;
|
||||||
const rb = boxRef.current.getBoundingClientRect();
|
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) };
|
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(() => {});
|
}).catch(() => {});
|
||||||
} else {
|
} 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) };
|
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) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
@@ -12085,10 +12102,14 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
|||||||
const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
|
const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
|
||||||
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 3)
|
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 3)
|
||||||
const [fxGuiOpen, setFxGuiOpen] = React.useState(null); // { path, url } | null
|
const [fxGuiOpen, setFxGuiOpen] = React.useState(null); // { path, url } | null
|
||||||
|
const fxGuiOpenRef = React.useRef(null);
|
||||||
|
fxGuiOpenRef.current = fxGuiOpen;
|
||||||
const closeFxGui = () => {
|
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 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 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); };
|
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)
|
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 2)
|
||||||
const [masterGuiOpen, setMasterGuiOpen] = React.useState(null); // { path, url } | null
|
const [masterGuiOpen, setMasterGuiOpen] = React.useState(null); // { path, url } | null
|
||||||
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
|
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
|
||||||
|
const masterGuiOpenRef = React.useRef(null);
|
||||||
|
masterGuiOpenRef.current = masterGuiOpen;
|
||||||
const closeMasterGui = () => {
|
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);
|
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 }));
|
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 toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||||
const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));
|
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); };
|
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;
|
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.
|
* 5. AUDIO SINK: register consumer of PCM frames from the bridge.
|
||||||
* cb(l: Float32Array, r: Float32Array)
|
* cb(l: Float32Array, r: Float32Array)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
|
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
|
||||||
<script src="/static/js/services/nativeBridgeService.js?v=202608181735"></script>
|
<script src="/static/js/services/nativeBridgeService.js?v=202608190001"></script>
|
||||||
<script src="/static/js/services/bridgeAudioNode.js?v=202608152200"></script>
|
<script src="/static/js/services/bridgeAudioNode.js?v=202608152200"></script>
|
||||||
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
|
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
|
||||||
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script>
|
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
<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/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608182359" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608190001" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
Binary file not shown.
@@ -2,6 +2,8 @@
|
|||||||
#ifndef RENDER_FX_JOB_H
|
#ifndef RENDER_FX_JOB_H
|
||||||
#define RENDER_FX_JOB_H
|
#define RENDER_FX_JOB_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// Offline FX render mode (Phase 0 of PLAN_MASTERBUS_FX_RACK_VST.md):
|
// Offline FX render mode (Phase 0 of PLAN_MASTERBUS_FX_RACK_VST.md):
|
||||||
@@ -53,4 +55,23 @@ int run_open_fx_gui(const std::string& jobPath);
|
|||||||
// --open-fx-gui.
|
// --open-fx-gui.
|
||||||
int run_fx_gui_server(const std::string& jobPath);
|
int run_fx_gui_server(const std::string& jobPath);
|
||||||
|
|
||||||
|
// Realtime FX chain (T2.4): live VST3 audio effects on the bridge master mix.
|
||||||
|
// setChain() is async (a worker thread loads the plugins and swaps the chain);
|
||||||
|
// process() runs ONLY on the audio thread; shutdown() joins the worker and must
|
||||||
|
// be called before CoUninitialize. v1: master chain only (per-track deferred).
|
||||||
|
class RealtimeFxChain {
|
||||||
|
public:
|
||||||
|
RealtimeFxChain();
|
||||||
|
~RealtimeFxChain();
|
||||||
|
// JSON: [{"path":"...","bypass":true|false},...] — chain entries replace
|
||||||
|
// the previous chain atomically. May be called from any thread.
|
||||||
|
void setChain(const std::string& json, double sampleRate, int32_t blockSize);
|
||||||
|
// Audio thread only: run the chain in-place over one stereo block.
|
||||||
|
void process(float* inL, float* inR, uint32_t n);
|
||||||
|
void shutdown();
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
#endif // RENDER_FX_JOB_H
|
#endif // RENDER_FX_JOB_H
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ void danceStart(HWND hwnd, HWND target, int x, int y) {
|
|||||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
|
||||||
SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||||||
}
|
}
|
||||||
SetWindowPos(hwnd, HWND_TOPMOST, g_embedRect.left, g_embedRect.top,
|
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
|
||||||
cw, ch, SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
cw, ch, SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||||
fxGuiRequestResize(hwnd, cw, ch);
|
fxGuiRequestResize(hwnd, cw, ch);
|
||||||
Sleep(60); // let the compositor repaint so the editor sees itself active
|
Sleep(60); // let the compositor repaint so the editor sees itself active
|
||||||
@@ -306,7 +306,7 @@ void danceStart(HWND hwnd, HWND target, int x, int y) {
|
|||||||
const int nx = wr.left + (cur.x - pt.x);
|
const int nx = wr.left + (cur.x - pt.x);
|
||||||
const int ny = wr.top + (cur.y - pt.y);
|
const int ny = wr.top + (cur.y - pt.y);
|
||||||
GetCursorPos(&g_savedCursor);
|
GetCursorPos(&g_savedCursor);
|
||||||
SetWindowPos(hwnd, HWND_TOPMOST, nx, ny, 0, 0,
|
SetWindowPos(hwnd, HWND_TOP, nx, ny, 0, 0,
|
||||||
SWP_NOSIZE | SWP_SHOWWINDOW);
|
SWP_NOSIZE | SWP_SHOWWINDOW);
|
||||||
Sleep(60); // let the compositor repaint so the editor sees itself active
|
Sleep(60); // let the compositor repaint so the editor sees itself active
|
||||||
g_dance = true;
|
g_dance = true;
|
||||||
@@ -516,6 +516,50 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
|||||||
if (root) std::free(root);
|
if (root) std::free(root);
|
||||||
sendResponse(c, 400, "application/json", "{\"error\":\"bad json\"}", 20);
|
sendResponse(c, 400, "application/json", "{\"error\":\"bad json\"}", 20);
|
||||||
}
|
}
|
||||||
|
} else if (req.path == "/hide") {
|
||||||
|
ShowWindow(hwnd, SW_HIDE);
|
||||||
|
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||||
|
} else if (req.path == "/show") {
|
||||||
|
// Optional body {rect:{x,y,w,h}} (screen px) — anchored placement
|
||||||
|
// over the embed panel; without a rect just show + top (never
|
||||||
|
// topmost — the GUI must not stay above other apps).
|
||||||
|
json_parse_result_s spres = {};
|
||||||
|
json_value_s* sroot = json_parse_ex(req.body.data(), req.body.size(),
|
||||||
|
json_parse_flags_default, nullptr, nullptr, &spres);
|
||||||
|
bool rectOk = false;
|
||||||
|
if (sroot && sroot->type == json_type_object) {
|
||||||
|
const json_object_s* so = static_cast<const json_object_s*>(sroot->payload);
|
||||||
|
const json_value_s* srv = memberValue(so, "rect");
|
||||||
|
if (srv && srv->type == json_type_object) {
|
||||||
|
const json_object_s* sro = static_cast<const json_object_s*>(srv->payload);
|
||||||
|
const LONG srw = (LONG)memberInt(sro, "w", 0);
|
||||||
|
const LONG srh = (LONG)memberInt(sro, "h", 0);
|
||||||
|
if (srw > 0 && srh > 0) {
|
||||||
|
g_embedRect.left = (LONG)memberInt(sro, "x", 0);
|
||||||
|
g_embedRect.top = (LONG)memberInt(sro, "y", 0);
|
||||||
|
g_embedRect.right = g_embedRect.left + srw;
|
||||||
|
g_embedRect.bottom = g_embedRect.top + srh;
|
||||||
|
rectOk = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sroot) std::free(sroot);
|
||||||
|
if (rectOk) {
|
||||||
|
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
|
||||||
|
g_embedRect.right - g_embedRect.left,
|
||||||
|
g_embedRect.bottom - g_embedRect.top,
|
||||||
|
SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||||
|
fxGuiRequestResize(hwnd, g_embedRect.right - g_embedRect.left,
|
||||||
|
g_embedRect.bottom - g_embedRect.top);
|
||||||
|
g_dance = true;
|
||||||
|
g_anchored = true;
|
||||||
|
g_parkUntil = 0;
|
||||||
|
} else {
|
||||||
|
ShowWindow(hwnd, SW_SHOW);
|
||||||
|
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0,
|
||||||
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||||
|
}
|
||||||
|
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||||
} else if (req.path == "/close") {
|
} else if (req.path == "/close") {
|
||||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||||
stop.store(true);
|
stop.store(true);
|
||||||
@@ -534,9 +578,13 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
|||||||
// Runs the embedded GUI server: starts the HTTP worker thread, then pumps
|
// Runs the embedded GUI server: starts the HTTP worker thread, then pumps
|
||||||
// messages on this (STA) thread until the host window is destroyed (user
|
// messages on this (STA) thread until the host window is destroyed (user
|
||||||
// closed it or POST /close). Returns 0.
|
// closed it or POST /close). Returns 0.
|
||||||
int fxGuiServerLoop(HWND hwnd) {
|
int fxGuiServerLoop(HWND hwnd, const std::function<void()>& onStarted) {
|
||||||
std::atomic<bool> stop{false};
|
std::atomic<bool> stop{false};
|
||||||
std::thread http(runHttpServer, hwnd, std::ref(stop));
|
std::thread http(runHttpServer, hwnd, std::ref(stop));
|
||||||
|
// onStarted runs on the pump (STA) thread AFTER the HTTP server is up and
|
||||||
|
// the port printed — used to defer the slow plugin load/attach in server
|
||||||
|
// mode so the caller (plugins.py) doesn't block on VST load.
|
||||||
|
if (onStarted) onStarted();
|
||||||
MSG msg;
|
MSG msg;
|
||||||
while (IsWindow(hwnd)) {
|
while (IsWindow(hwnd)) {
|
||||||
BOOL r = GetMessageA(&msg, nullptr, 0, 0);
|
BOOL r = GetMessageA(&msg, nullptr, 0, 0);
|
||||||
|
|||||||
@@ -47,9 +47,13 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -921,6 +925,168 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- RealtimeFxChain (T2.4) ---------------------------------------------------
|
||||||
|
// Live VST3 FX on the bridge master mix. setChain() enqueues JSON; a worker
|
||||||
|
// thread (own COM init) parses + loads plugins then swaps the chain under the
|
||||||
|
// mutex. The audio thread copies the shared_ptr (cheap, uncontended) and runs
|
||||||
|
// the plugins in-place — Vst3Fx::processAudio copies input to owned buffers
|
||||||
|
// before writing output, so out==in is safe. Old chains are retired on the
|
||||||
|
// worker so plugin destructors never run on the audio thread.
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// SEH frame holds only raw pointers / trivials (C2712 — no unwinding locals).
|
||||||
|
static void realtimeRunChain(Vst3Fx** fxs, const bool* bypass, uint32_t count,
|
||||||
|
float* inL, float* inR, uint32_t n) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
__try {
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
if (!fxs[i]) continue;
|
||||||
|
if (bypass && bypass[i]) continue;
|
||||||
|
fxs[i]->processAudio(inL, inR, inL, inR, n); // in-place safe
|
||||||
|
}
|
||||||
|
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||||
|
std::cerr << "[RealtimeFx] plugin crashed in process — chain bypassed" << std::endl;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)fxs; (void)bypass; (void)count; (void)inL; (void)inR; (void)n;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct RealtimeFxChain::Impl {
|
||||||
|
struct Entry { std::shared_ptr<Vst3Fx> fx; bool bypass = false; };
|
||||||
|
struct Chain { std::vector<Entry> entries; };
|
||||||
|
|
||||||
|
std::mutex mutex_; // guards chain_ / retired_
|
||||||
|
std::shared_ptr<Chain> chain_;
|
||||||
|
std::shared_ptr<Chain> retired_; // dtor deferred to the worker thread
|
||||||
|
|
||||||
|
std::mutex qmutex_; // guards q_ / quit_
|
||||||
|
std::condition_variable qcv_;
|
||||||
|
std::vector<std::string> q_;
|
||||||
|
bool quit_ = false;
|
||||||
|
|
||||||
|
std::thread worker_;
|
||||||
|
double sampleRate_ = 44100.0;
|
||||||
|
int32_t blockSize_ = 256;
|
||||||
|
|
||||||
|
std::shared_ptr<Chain> buildChain(const std::string& json);
|
||||||
|
void workerLoop();
|
||||||
|
};
|
||||||
|
|
||||||
|
RealtimeFxChain::RealtimeFxChain() : impl_(std::make_unique<Impl>()) {
|
||||||
|
impl_->worker_ = std::thread([this]() { impl_->workerLoop(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
RealtimeFxChain::~RealtimeFxChain() { shutdown(); }
|
||||||
|
|
||||||
|
void RealtimeFxChain::shutdown() {
|
||||||
|
if (!impl_) return;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(impl_->qmutex_);
|
||||||
|
if (impl_->quit_) return;
|
||||||
|
impl_->quit_ = true;
|
||||||
|
}
|
||||||
|
impl_->qcv_.notify_all();
|
||||||
|
if (impl_->worker_.joinable()) impl_->worker_.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(impl_->mutex_);
|
||||||
|
impl_->chain_.reset();
|
||||||
|
impl_->retired_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RealtimeFxChain::setChain(const std::string& json, double sampleRate, int32_t blockSize) {
|
||||||
|
if (!impl_) return;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(impl_->qmutex_);
|
||||||
|
if (sampleRate > 0) impl_->sampleRate_ = sampleRate;
|
||||||
|
if (blockSize > 0) impl_->blockSize_ = blockSize;
|
||||||
|
impl_->q_.push_back(json);
|
||||||
|
}
|
||||||
|
impl_->qcv_.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RealtimeFxChain::process(float* inL, float* inR, uint32_t n) {
|
||||||
|
if (!impl_ || n == 0) return;
|
||||||
|
std::shared_ptr<Impl::Chain> c;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(impl_->mutex_);
|
||||||
|
c = impl_->chain_;
|
||||||
|
}
|
||||||
|
if (!c || c->entries.empty()) return;
|
||||||
|
constexpr uint32_t kMaxSlots = 16; // v1 cap
|
||||||
|
Vst3Fx* fxs[kMaxSlots];
|
||||||
|
bool bypass[kMaxSlots];
|
||||||
|
uint32_t count = 0;
|
||||||
|
for (const auto& e : c->entries) {
|
||||||
|
if (count >= kMaxSlots) break;
|
||||||
|
fxs[count] = e.fx.get();
|
||||||
|
bypass[count] = e.bypass;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
realtimeRunChain(fxs, bypass, count, inL, inR, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<RealtimeFxChain::Impl::Chain> RealtimeFxChain::Impl::buildChain(const std::string& json) {
|
||||||
|
auto chain = std::make_shared<Chain>();
|
||||||
|
json_parse_result_s pres = {};
|
||||||
|
json_value_s* root = json_parse_ex(json.data(), json.size(), json_parse_flags_default,
|
||||||
|
nullptr, nullptr, &pres);
|
||||||
|
if (!root || root->type != json_type_array) {
|
||||||
|
if (root) std::free(root);
|
||||||
|
std::cerr << "[RealtimeFx] chain JSON parse error" << std::endl;
|
||||||
|
return chain; // empty chain = passthrough
|
||||||
|
}
|
||||||
|
const json_array_s* arr = static_cast<const json_array_s*>(root->payload);
|
||||||
|
for (const json_array_element_s* e = arr->start; e; e = e->next) {
|
||||||
|
if (!e->value || e->value->type != json_type_object) continue;
|
||||||
|
const json_object_s* o = static_cast<const json_object_s*>(e->value->payload);
|
||||||
|
const std::string path = memberString(o, "path", "");
|
||||||
|
if (path.empty()) continue;
|
||||||
|
Entry ent;
|
||||||
|
ent.bypass = memberInt(o, "bypass", 0) != 0;
|
||||||
|
if (!ent.bypass) {
|
||||||
|
auto fx = std::make_shared<Vst3Fx>();
|
||||||
|
if (fx->load(path, sampleRate_, blockSize_)) ent.fx = std::move(fx);
|
||||||
|
else {
|
||||||
|
std::cerr << "[RealtimeFx] load failed — slot bypassed: " << path << std::endl;
|
||||||
|
ent.bypass = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chain->entries.push_back(std::move(ent));
|
||||||
|
}
|
||||||
|
std::free(root);
|
||||||
|
std::cout << "[RealtimeFx] chain applied: " << chain->entries.size() << " slot(s)" << std::endl;
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RealtimeFxChain::Impl::workerLoop() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||||
|
#endif
|
||||||
|
for (;;) {
|
||||||
|
std::string job;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(qmutex_);
|
||||||
|
qcv_.wait(lk, [&]() { return quit_ || !q_.empty(); });
|
||||||
|
if (quit_) break;
|
||||||
|
job = std::move(q_.front());
|
||||||
|
q_.erase(q_.begin());
|
||||||
|
}
|
||||||
|
std::shared_ptr<Chain> next = buildChain(job); // slow: plugin load
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
retired_ = std::move(chain_); // old chain destroyed on THIS thread
|
||||||
|
chain_ = std::move(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef _WIN32
|
||||||
|
CoUninitialize();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// --- --open-fx-gui <job.json>: native editor window for one VST3 FX ----------
|
// --- --open-fx-gui <job.json>: native editor window for one VST3 FX ----------
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Worker (HTTP) thread -> pump thread editor-resize request. Declared
|
// Worker (HTTP) thread -> pump thread editor-resize request. Declared
|
||||||
@@ -989,12 +1155,9 @@ void fxGuiRequestResize(HWND hwnd, int w, int h) {
|
|||||||
PostMessageA(hwnd, WM_FXGUI_RESIZE, (WPARAM)w, (LPARAM)h);
|
PostMessageA(hwnd, WM_FXGUI_RESIZE, (WPARAM)w, (LPARAM)h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared setup for both GUI modes: parse job, load plugin, create host window
|
// --- split setup for server mode (startup speed) -----------------------------
|
||||||
// (offscreen for embedded capture), attach editor (SEH-guarded). Returns 0 ok,
|
// 1. Parse job { path, name } — fast. Returns 0 ok, 1 job/argument error.
|
||||||
// 1 job/argument error, 2 plugin load or editor attach failure.
|
static int fxGuiParseJob(const std::string& jobPath, std::string& path, std::string& name) {
|
||||||
static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
|
||||||
bool offscreen, LONG_PTR userData) {
|
|
||||||
// 1. Parse job { path, name }.
|
|
||||||
std::ifstream jf(jobPath, std::ios::binary);
|
std::ifstream jf(jobPath, std::ios::binary);
|
||||||
if (!jf) { std::cerr << "[FxGui] cannot read job file: " << jobPath << std::endl; return 1; }
|
if (!jf) { std::cerr << "[FxGui] cannot read job file: " << jobPath << std::endl; return 1; }
|
||||||
std::string data((std::istreambuf_iterator<char>(jf)), std::istreambuf_iterator<char>());
|
std::string data((std::istreambuf_iterator<char>(jf)), std::istreambuf_iterator<char>());
|
||||||
@@ -1006,20 +1169,21 @@ static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
|||||||
rootH.p = root;
|
rootH.p = root;
|
||||||
if (root->type != json_type_object) { std::cerr << "[FxGui] job root must be a JSON object" << std::endl; return 1; }
|
if (root->type != json_type_object) { std::cerr << "[FxGui] job root must be a JSON object" << std::endl; return 1; }
|
||||||
const json_object_s* job = static_cast<const json_object_s*>(root->payload);
|
const json_object_s* job = static_cast<const json_object_s*>(root->payload);
|
||||||
const std::string path = memberString(job, "path", "");
|
path = memberString(job, "path", "");
|
||||||
const std::string name = memberString(job, "name", "");
|
name = memberString(job, "name", "");
|
||||||
if (path.empty()) { std::cerr << "[FxGui] job missing path" << std::endl; return 1; }
|
if (path.empty()) { std::cerr << "[FxGui] job missing path" << std::endl; return 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Load the plugin (fixed 44.1k/512 — GUI only, no audio processed here).
|
// 2. Create the host window — fast. Embedded mode parks it far off-screen
|
||||||
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
|
// (never steals focus; still visible to GDI so BitBlt captures real pixels).
|
||||||
|
// WS_EX_TOOLWINDOW: an off-screen visible window would otherwise sit in the
|
||||||
// 3. Host window. Embedded mode parks it far off-screen (never steals
|
// taskbar; clicking its button "restores" it to -20000,-20000 and it looks
|
||||||
// focus; still visible to GDI so BitBlt captures real pixels).
|
// minimized/unrestorable. Tool window has no taskbar button. WS_MINIMIZEBOX
|
||||||
// WS_EX_TOOLWINDOW: an off-screen visible window would otherwise sit in
|
// is dropped too (a minimized tool window is hard to get back while parked).
|
||||||
// the taskbar; clicking its button "restores" it to -20000,-20000 and it
|
// Returns 0 ok, 1 window creation error.
|
||||||
// looks minimized/unrestorable. Tool window has no taskbar button.
|
static int fxGuiCreateWindow(const std::string& name, bool offscreen,
|
||||||
// WS_MINIMIZEBOX is dropped too (a minimized tool window is hard to get
|
LONG_PTR userData, HWND* hwndOut) {
|
||||||
// back while parked off-screen).
|
|
||||||
WNDCLASSA wc = {};
|
WNDCLASSA wc = {};
|
||||||
wc.lpfnWndProc = FxGuiWndProc;
|
wc.lpfnWndProc = FxGuiWndProc;
|
||||||
wc.hInstance = GetModuleHandleA(nullptr);
|
wc.hInstance = GetModuleHandleA(nullptr);
|
||||||
@@ -1037,18 +1201,37 @@ static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
|||||||
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
|
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
|
||||||
if (!hwnd) { std::cerr << "[FxGui] create window failed" << std::endl; return 1; }
|
if (!hwnd) { std::cerr << "[FxGui] create window failed" << std::endl; return 1; }
|
||||||
SetWindowLongPtrA(hwnd, GWLP_USERDATA, userData);
|
SetWindowLongPtrA(hwnd, GWLP_USERDATA, userData);
|
||||||
|
*hwndOut = hwnd;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Attach the plugin editor (SEH-guarded — plugin GUI code may crash).
|
// 3. Load the plugin + attach the editor (SEH-guarded — plugin GUI code may
|
||||||
|
// crash). Slow — runs on the pump thread in server mode, after HTTP is up.
|
||||||
|
// Returns 0 ok, 2 plugin load or editor attach failure.
|
||||||
|
static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::string& name, HWND hwnd) {
|
||||||
|
// Fixed 44.1k/512 — GUI only, no audio processed here.
|
||||||
|
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
|
||||||
if (!fxGuiAttachSafe(fx, hwnd)) {
|
if (!fxGuiAttachSafe(fx, hwnd)) {
|
||||||
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
|
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
|
||||||
DestroyWindow(hwnd);
|
DestroyWindow(hwnd);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
||||||
*hwndOut = hwnd;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combined setup (legacy open mode): parse + load + window + attach, in order.
|
||||||
|
static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
||||||
|
bool offscreen, LONG_PTR userData) {
|
||||||
|
std::string path, name;
|
||||||
|
int rc = fxGuiParseJob(jobPath, path, name);
|
||||||
|
if (rc) return rc;
|
||||||
|
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
|
||||||
|
rc = fxGuiCreateWindow(name, offscreen, userData, hwndOut);
|
||||||
|
if (rc) return rc;
|
||||||
|
return fxGuiLoadAttach(fx, path, name, *hwndOut);
|
||||||
|
}
|
||||||
|
|
||||||
int run_open_fx_gui(const std::string& jobPath) {
|
int run_open_fx_gui(const std::string& jobPath) {
|
||||||
// The editor window and its message pump live on this (STA) thread for the
|
// The editor window and its message pump live on this (STA) thread for the
|
||||||
// whole GUI session — VST3 editors need the COM apartment + pump together.
|
// whole GUI session — VST3 editors need the COM apartment + pump together.
|
||||||
@@ -1078,7 +1261,7 @@ int run_open_fx_gui(const std::string& jobPath) {
|
|||||||
|
|
||||||
// Embedded GUI server mode: same setup but off-screen, then hand the message
|
// Embedded GUI server mode: same setup but off-screen, then hand the message
|
||||||
// pump + HTTP capture/input to FxGuiServer.cpp (see PLAN_MASTERBUS_VST_GUI_EMBED.md).
|
// pump + HTTP capture/input to FxGuiServer.cpp (see PLAN_MASTERBUS_VST_GUI_EMBED.md).
|
||||||
extern int fxGuiServerLoop(HWND hwnd);
|
extern int fxGuiServerLoop(HWND hwnd, const std::function<void()>& onStarted);
|
||||||
|
|
||||||
int run_fx_gui_server(const std::string& jobPath) {
|
int run_fx_gui_server(const std::string& jobPath) {
|
||||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
@@ -1086,15 +1269,26 @@ int run_fx_gui_server(const std::string& jobPath) {
|
|||||||
|
|
||||||
Vst3Fx fx;
|
Vst3Fx fx;
|
||||||
HWND hwnd = nullptr;
|
HWND hwnd = nullptr;
|
||||||
const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/true, 0);
|
// Fast path: parse job + create the host window, then start the HTTP
|
||||||
|
// server — SF_FXGUI_PORT prints immediately. The slow VST load + editor
|
||||||
|
// attach is deferred to the pump thread (onStarted), so plugins.py stops
|
||||||
|
// blocking on a multi-second plugin load (startup speed issue).
|
||||||
|
std::string path, name;
|
||||||
|
int rc = fxGuiParseJob(jobPath, path, name);
|
||||||
|
if (rc) return rc;
|
||||||
|
rc = fxGuiCreateWindow(name, /*offscreen=*/true, 0, &hwnd);
|
||||||
if (rc) return rc;
|
if (rc) return rc;
|
||||||
SetPropA(hwnd, "FXGUI_FX", &fx); // for WM_FXGUI_RESIZE on the pump thread
|
SetPropA(hwnd, "FXGUI_FX", &fx); // for WM_FXGUI_RESIZE on the pump thread
|
||||||
|
|
||||||
const int srv = fxGuiServerLoop(hwnd);
|
int loadRc = 0;
|
||||||
|
const int srv = fxGuiServerLoop(hwnd, [&]() {
|
||||||
|
loadRc = fxGuiLoadAttach(&fx, path, name, hwnd);
|
||||||
|
if (loadRc != 0) PostMessageA(hwnd, WM_CLOSE, 0, 0); // exit; port already printed
|
||||||
|
});
|
||||||
RemovePropA(hwnd, "FXGUI_FX");
|
RemovePropA(hwnd, "FXGUI_FX");
|
||||||
fxGuiDetachSafe(&fx);
|
fxGuiDetachSafe(&fx);
|
||||||
std::cout << "[FxGuiServer] closed" << std::endl;
|
std::cout << "[FxGuiServer] closed" << std::endl;
|
||||||
return srv;
|
return loadRc ? loadRc : srv;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else // !_WIN32
|
#else // !_WIN32
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <imm.h>
|
#include <imm.h>
|
||||||
#include <mmsystem.h>
|
#include <mmsystem.h>
|
||||||
#include <process.h>
|
#include <process.h>
|
||||||
|
#include <cstdlib>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#else
|
#else
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -116,6 +117,9 @@ static std::mutex g_guiMutex;
|
|||||||
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
|
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
|
||||||
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
|
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
|
||||||
static ChannelWorker* g_uiWorker = nullptr;
|
static ChannelWorker* g_uiWorker = nullptr;
|
||||||
|
// T2.4: realtime master FX chain (live audio through VST3 FX). Created in
|
||||||
|
// main() once sampleRate is known; fed via control queue type 6.
|
||||||
|
static std::unique_ptr<RealtimeFxChain> g_fxChain;
|
||||||
|
|
||||||
// Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus)
|
// Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus)
|
||||||
// crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/
|
// crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/
|
||||||
@@ -574,6 +578,13 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Background daemon: detach the console window unless SF_KEEP_CONSOLE is
|
||||||
|
// set. Console apps spawned from the GUI (tauri-shell, Popen) flash a
|
||||||
|
// console window otherwise. Redirected stdio (pipe to bridge.log / the
|
||||||
|
// fx-gui port reader) survives FreeConsole.
|
||||||
|
if (!std::getenv("SF_KEEP_CONSOLE")) FreeConsole();
|
||||||
|
#endif
|
||||||
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
|
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Disable Power Throttling (Efficiency Mode) for this background process
|
// Disable Power Throttling (Efficiency Mode) for this background process
|
||||||
@@ -758,6 +769,8 @@ int main(int argc, char* argv[]) {
|
|||||||
if (const char* e = std::getenv("SF_STATE_FILE")) stateFile = e;
|
if (const char* e = std::getenv("SF_STATE_FILE")) stateFile = e;
|
||||||
if (stateFile.empty()) stateFile = StateStore::defaultPath();
|
if (stateFile.empty()) stateFile = StateStore::defaultPath();
|
||||||
const uint32_t block = AUDIO_BLOCK_SIZE;
|
const uint32_t block = AUDIO_BLOCK_SIZE;
|
||||||
|
// T2.4: realtime master FX chain — created once, fed via control type 6.
|
||||||
|
g_fxChain = std::make_unique<RealtimeFxChain>();
|
||||||
std::atomic<uint64_t> playheadSamples{0};
|
std::atomic<uint64_t> playheadSamples{0};
|
||||||
// V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS
|
// V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS
|
||||||
// note-on timer co the bay toi sau STOP (guardPlay tre do React re-render)
|
// note-on timer co the bay toi sau STOP (guardPlay tre do React re-render)
|
||||||
@@ -1374,6 +1387,8 @@ int main(int argc, char* argv[]) {
|
|||||||
} else if (c.type == 5) { // DUMP_STATE (G2.2): Rust pre-kill flush
|
} else if (c.type == 5) { // DUMP_STATE (G2.2): Rust pre-kill flush
|
||||||
std::cout << "[NativeBridge] DUMP_STATE - flushing state now" << std::endl;
|
std::cout << "[NativeBridge] DUMP_STATE - flushing state now" << std::endl;
|
||||||
uiWorker.post([&]() { saveStateJob(); });
|
uiWorker.post([&]() { saveStateJob(); });
|
||||||
|
} else if (c.type == 6) { // SET_FX_CHAIN (T2.4): arg2 = JSON [{"path","bypass"},...]
|
||||||
|
if (g_fxChain) g_fxChain->setChain(std::string(c.arg2), sampleRate, (int32_t)block);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shmIPC->controlQueueCount = 0;
|
shmIPC->controlQueueCount = 0;
|
||||||
@@ -1479,6 +1494,12 @@ int main(int argc, char* argv[]) {
|
|||||||
dispatch(evts[ei]);
|
dispatch(evts[ei]);
|
||||||
}
|
}
|
||||||
if (cursor < block) renderSegment(cursor, block);
|
if (cursor < block) renderSegment(cursor, block);
|
||||||
|
// T2.4: realtime master FX chain — apply to the just-rendered slot
|
||||||
|
// (in-place, owned buffers) before publishing.
|
||||||
|
if (g_fxChain) {
|
||||||
|
uint32_t slot = shmIPC->bridgeWriteIndex & shmIPC->ringMask;
|
||||||
|
g_fxChain->process(shmIPC->ringLeft[slot], shmIPC->ringRight[slot], block);
|
||||||
|
}
|
||||||
shmIPC->bridgeWriteIndex++;
|
shmIPC->bridgeWriteIndex++;
|
||||||
|
|
||||||
// D. Parent died / window closed -> exit (no orphan process)
|
// D. Parent died / window closed -> exit (no orphan process)
|
||||||
@@ -1509,6 +1530,7 @@ int main(int argc, char* argv[]) {
|
|||||||
UnmapViewOfFile(shmIPC);
|
UnmapViewOfFile(shmIPC);
|
||||||
CloseHandle(hMapFile);
|
CloseHandle(hMapFile);
|
||||||
timeEndPeriod(1);
|
timeEndPeriod(1);
|
||||||
|
if (g_fxChain) g_fxChain->shutdown(); // destroy VST3 FX before COM teardown
|
||||||
CoUninitialize();
|
CoUninitialize();
|
||||||
#else
|
#else
|
||||||
std::free(shmIPC);
|
std::free(shmIPC);
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ pub fn run() {
|
|||||||
open_vst_gui,
|
open_vst_gui,
|
||||||
bridge_status,
|
bridge_status,
|
||||||
transport_control,
|
transport_control,
|
||||||
|
set_fx_chain,
|
||||||
restart_bridge_with_sample_rate
|
restart_bridge_with_sample_rate
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
@@ -857,6 +858,19 @@ fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Res
|
|||||||
.ok_or_else(|| "control queue full".to_string())
|
.ok_or_else(|| "control queue full".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_fx_chain(app: AppHandle, chain_json: String) -> Result<(), String> {
|
||||||
|
if chain_json.len() >= shm::CONTROL_PATH_MAX {
|
||||||
|
return Err("fx chain JSON too large".to_string());
|
||||||
|
}
|
||||||
|
let state = app.state::<ShmState>();
|
||||||
|
let guard = state.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
|
||||||
|
shm.push_control(6, 0, 0, 0, &chain_json)
|
||||||
|
.then_some(())
|
||||||
|
.ok_or_else(|| "control queue full".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<(), String> {
|
fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<(), String> {
|
||||||
println!("restart_bridge_with_sample_rate: {}", sample_rate);
|
println!("restart_bridge_with_sample_rate: {}", sample_rate);
|
||||||
|
|||||||
Reference in New Issue
Block a user