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:
@@ -283,7 +283,7 @@ void danceStart(HWND hwnd, HWND target, int x, int y) {
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
|
||||
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);
|
||||
fxGuiRequestResize(hwnd, cw, ch);
|
||||
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 ny = wr.top + (cur.y - pt.y);
|
||||
GetCursorPos(&g_savedCursor);
|
||||
SetWindowPos(hwnd, HWND_TOPMOST, nx, ny, 0, 0,
|
||||
SetWindowPos(hwnd, HWND_TOP, nx, ny, 0, 0,
|
||||
SWP_NOSIZE | SWP_SHOWWINDOW);
|
||||
Sleep(60); // let the compositor repaint so the editor sees itself active
|
||||
g_dance = true;
|
||||
@@ -516,6 +516,50 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
if (root) std::free(root);
|
||||
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") {
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
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
|
||||
// messages on this (STA) thread until the host window is destroyed (user
|
||||
// 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::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;
|
||||
while (IsWindow(hwnd)) {
|
||||
BOOL r = GetMessageA(&msg, nullptr, 0, 0);
|
||||
|
||||
@@ -47,9 +47,13 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -921,6 +925,168 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
|
||||
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 ----------
|
||||
#ifdef _WIN32
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Shared setup for both GUI modes: parse job, load plugin, create host window
|
||||
// (offscreen for embedded capture), attach editor (SEH-guarded). Returns 0 ok,
|
||||
// 1 job/argument error, 2 plugin load or editor attach failure.
|
||||
static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
||||
bool offscreen, LONG_PTR userData) {
|
||||
// 1. Parse job { path, name }.
|
||||
// --- split setup for server mode (startup speed) -----------------------------
|
||||
// 1. Parse job { path, name } — fast. Returns 0 ok, 1 job/argument error.
|
||||
static int fxGuiParseJob(const std::string& jobPath, std::string& path, std::string& name) {
|
||||
std::ifstream jf(jobPath, std::ios::binary);
|
||||
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>());
|
||||
@@ -1006,20 +1169,21 @@ static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
||||
rootH.p = root;
|
||||
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 std::string path = memberString(job, "path", "");
|
||||
const std::string name = memberString(job, "name", "");
|
||||
path = memberString(job, "path", "");
|
||||
name = memberString(job, "name", "");
|
||||
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).
|
||||
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
|
||||
|
||||
// 3. Host window. Embedded mode parks it far off-screen (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 taskbar; clicking its button "restores" it to -20000,-20000 and it
|
||||
// looks minimized/unrestorable. Tool window has no taskbar button.
|
||||
// WS_MINIMIZEBOX is dropped too (a minimized tool window is hard to get
|
||||
// back while parked off-screen).
|
||||
// 2. Create the host window — fast. Embedded mode parks it far off-screen
|
||||
// (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
|
||||
// taskbar; clicking its button "restores" it to -20000,-20000 and it looks
|
||||
// minimized/unrestorable. Tool window has no taskbar button. WS_MINIMIZEBOX
|
||||
// is dropped too (a minimized tool window is hard to get back while parked).
|
||||
// Returns 0 ok, 1 window creation error.
|
||||
static int fxGuiCreateWindow(const std::string& name, bool offscreen,
|
||||
LONG_PTR userData, HWND* hwndOut) {
|
||||
WNDCLASSA wc = {};
|
||||
wc.lpfnWndProc = FxGuiWndProc;
|
||||
wc.hInstance = GetModuleHandleA(nullptr);
|
||||
@@ -1037,18 +1201,37 @@ static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
||||
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
|
||||
if (!hwnd) { std::cerr << "[FxGui] create window failed" << std::endl; return 1; }
|
||||
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)) {
|
||||
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
|
||||
DestroyWindow(hwnd);
|
||||
return 2;
|
||||
}
|
||||
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
||||
*hwndOut = hwnd;
|
||||
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) {
|
||||
// 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.
|
||||
@@ -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
|
||||
// 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) {
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
@@ -1086,15 +1269,26 @@ int run_fx_gui_server(const std::string& jobPath) {
|
||||
|
||||
Vst3Fx fx;
|
||||
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;
|
||||
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");
|
||||
fxGuiDetachSafe(&fx);
|
||||
std::cout << "[FxGuiServer] closed" << std::endl;
|
||||
return srv;
|
||||
return loadRc ? loadRc : srv;
|
||||
}
|
||||
|
||||
#else // !_WIN32
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <imm.h>
|
||||
#include <mmsystem.h>
|
||||
#include <process.h>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#else
|
||||
#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<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
|
||||
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)
|
||||
// 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[]) {
|
||||
#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;
|
||||
#ifdef _WIN32
|
||||
// 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 (stateFile.empty()) stateFile = StateStore::defaultPath();
|
||||
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};
|
||||
// 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)
|
||||
@@ -1374,6 +1387,8 @@ int main(int argc, char* argv[]) {
|
||||
} else if (c.type == 5) { // DUMP_STATE (G2.2): Rust pre-kill flush
|
||||
std::cout << "[NativeBridge] DUMP_STATE - flushing state now" << std::endl;
|
||||
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;
|
||||
@@ -1479,6 +1494,12 @@ int main(int argc, char* argv[]) {
|
||||
dispatch(evts[ei]);
|
||||
}
|
||||
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++;
|
||||
|
||||
// D. Parent died / window closed -> exit (no orphan process)
|
||||
@@ -1509,6 +1530,7 @@ int main(int argc, char* argv[]) {
|
||||
UnmapViewOfFile(shmIPC);
|
||||
CloseHandle(hMapFile);
|
||||
timeEndPeriod(1);
|
||||
if (g_fxChain) g_fxChain->shutdown(); // destroy VST3 FX before COM teardown
|
||||
CoUninitialize();
|
||||
#else
|
||||
std::free(shmIPC);
|
||||
|
||||
Reference in New Issue
Block a user