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