feat: open native VST FX GUI from mastering panel + FX rack (bridge --open-fx-gui)

This commit is contained in:
2026-08-17 21:52:40 +07:00
parent b261eb0d7e
commit 38f4cbd8b5
7 changed files with 313 additions and 10 deletions
+214
View File
@@ -23,6 +23,7 @@
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstprocesscontext.h"
#include "pluginterfaces/vst/ivstmessage.h"
#include "pluginterfaces/gui/iplugview.h"
#endif
#ifdef _WIN32
@@ -318,6 +319,24 @@ public:
tresult restartComponent(Steinberg::int32) override { return kResultOk; }
};
using Steinberg::IPlugView;
using Steinberg::kPlatformTypeHWND;
// Minimal IPlugFrame so the plugin can resize its editor view.
class HostPlugFrameFx : public Steinberg::IPlugFrame {
public:
tresult queryInterface(const char*, void** v) override {
*v = nullptr;
return kNoInterface;
}
Steinberg::uint32 addRef() override { return 1; }
Steinberg::uint32 release() override { return 1; }
tresult resizeView(Steinberg::IPlugView* view, Steinberg::ViewRect* newSize) override {
if (view && newSize) view->onSize(newSize);
return kResultOk;
}
};
struct Vst3FxState {
VST3::Hosting::Module::Ptr module; // destroyed LAST (owns the factory)
IPtr<IComponent> component;
@@ -329,6 +348,8 @@ struct Vst3FxState {
int32 inputChannels = 2;
int32 outputChannels = 2;
bool controllerIsComponent = false;
Steinberg::IPlugFrame* plugFrame = nullptr; // owned; freed in closeEditor
IPtr<IPlugView> view; // editor view while GUI open
};
// Class selection for FX: any kVstAudioEffectClass whose subcategory does NOT
@@ -590,6 +611,80 @@ public:
std::cout << "[RenderFx] preset applied component=" << comp.size()
<< " controller=" << ctrl.size() << std::endl;
return true;
#endif
}
// Native editor: attach the plugin's IPlugView to a host window (must run
// on the thread that will pump its messages — same rule as Vst3Instrument).
bool openEditor(void* parentWindowHandle) {
#ifndef HAVE_VST3SDK
(void)parentWindowHandle;
return false;
#else
auto* s = static_cast<Vst3FxState*>(state_);
if (!s || !s->controller || !parentWindowHandle) return false;
if (s->plugFrame) return true; // already attached
s->plugFrame = new HostPlugFrameFx();
IPlugView* rawView = nullptr;
s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
FUnknownPtr<IPlugView> view(rawView);
if (!view) {
// Some plugins expose IPlugView on the component instead.
IPlugView* rawViewC = nullptr;
s->component->queryInterface(IPlugView::iid, (void**)&rawViewC);
view = FUnknownPtr<IPlugView>(rawViewC);
}
if (!view) {
// Official editorhost.cpp pattern: IEditController::createView.
// JUCE-based plugins (Scaler2) expose the editor only this way.
view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor));
}
if (!view) {
delete s->plugFrame;
s->plugFrame = nullptr;
return false;
}
view->setFrame(s->plugFrame);
view->isPlatformTypeSupported(kPlatformTypeHWND);
if (view->attached(parentWindowHandle, kPlatformTypeHWND) != kResultOk) {
view->setFrame(nullptr);
delete s->plugFrame;
s->plugFrame = nullptr;
return false;
}
s->view = view;
#ifdef _WIN32
HWND hwnd = (HWND)parentWindowHandle;
Steinberg::ViewRect rect;
if (view->getSize(&rect) == kResultOk) {
int w = rect.right - rect.left;
int h = rect.bottom - rect.top;
RECT r = { 0, 0, w, h };
AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
SetWindowPos(hwnd, nullptr, 0, 0, r.right - r.left, r.bottom - r.top,
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
view->onSize(&rect); // some Skia/OpenGL editors skip first paint
}
#endif
return true;
#endif
}
// Detach editor. Must run on the SAME thread that attached (the GUI message
// loop has exited). removed() is intentionally NOT called — Vst3Instrument
// documents a deadlock when the pump is not running.
void closeEditor() {
#ifndef HAVE_VST3SDK
return;
#else
auto* s = static_cast<Vst3FxState*>(state_);
if (!s) return;
if (s->view) {
s->view->setFrame(nullptr);
s->view = nullptr; // IPtr releases the view
}
if (s->plugFrame) {
delete s->plugFrame;
s->plugFrame = nullptr;
}
#endif
}
bool loaded() const { return state_ != nullptr; }
@@ -791,6 +886,125 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
return rc;
}
// --- --open-fx-gui <job.json>: native editor window for one VST3 FX ----------
#ifdef _WIN32
namespace {
const char* kFxGuiWndClass = "SonicForge_FX_GUI_Class";
struct FxGuiCtx {
bool closing = false;
};
LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
FxGuiCtx* ctx = (FxGuiCtx*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
switch (msg) {
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
if (ctx) ctx->closing = true;
return 0;
default:
return DefWindowProcA(hwnd, msg, wp, lp);
}
}
} // namespace
// SEH frames hold only raw pointers / trivials (C2712 — no unwinding locals).
static bool fxGuiAttachSafe(void* fx, HWND hwnd) {
#ifdef HAVE_VST3SDK
__try { return static_cast<Vst3Fx*>(fx)->openEditor(hwnd); }
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
#else
(void)fx; (void)hwnd;
return false;
#endif
}
static void fxGuiDetachSafe(void* fx) {
#ifdef HAVE_VST3SDK
__try { static_cast<Vst3Fx*>(fx)->closeEditor(); }
__except (EXCEPTION_EXECUTE_HANDLER) {}
#else
(void)fx;
#endif
}
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.
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
// 1. Parse job { path, 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>());
json_parse_result_s pres = {};
json_value_s* root = json_parse_ex(data.data(), data.size(), json_parse_flags_default,
nullptr, nullptr, &pres);
if (!root) { std::cerr << "[FxGui] job JSON parse error (code " << std::to_string(pres.error) << ")" << std::endl; return 1; }
struct RootHolder { json_value_s* p = nullptr; ~RootHolder() { if (p) std::free(p); } } rootH;
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", "");
if (path.empty()) { std::cerr << "[FxGui] job missing path" << std::endl; return 1; }
// 2. Load the plugin (fixed 44.1k/512 — GUI only, no audio processed here).
Vst3Fx fx;
if (!fx.load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
// 3. Host window.
WNDCLASSA wc = {};
wc.lpfnWndProc = FxGuiWndProc;
wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = kFxGuiWndClass;
RegisterClassA(&wc);
FxGuiCtx ctx;
HWND hwnd = CreateWindowExA(0, kFxGuiWndClass,
(name.empty() ? "VST FX" : name).c_str(),
WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
if (!hwnd) { std::cerr << "[FxGui] create window failed" << std::endl; return 1; }
SetWindowLongPtrA(hwnd, GWLP_USERDATA, (LONG_PTR)&ctx);
// 4. Attach the plugin editor (SEH-guarded — plugin GUI code may crash).
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;
// 5. Pump messages until the window is destroyed.
MSG msg;
while (!ctx.closing) {
BOOL r = GetMessageA(&msg, nullptr, 0, 0);
if (r <= 0) break;
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
// 6. Detach + exit.
fxGuiDetachSafe(&fx);
std::cout << "[FxGui] editor closed" << std::endl;
return 0;
}
#else // !_WIN32
int run_open_fx_gui(const std::string& jobPath) {
(void)jobPath;
std::cerr << "[FxGui] GUI mode chỉ hỗ trợ Windows" << std::endl;
return 1;
}
#endif // _WIN32
// --- --scan: classify VST3 modules as instrument/effect ----------------------
namespace {
+8
View File
@@ -638,6 +638,14 @@ int main(int argc, char* argv[]) {
}
if (!scanDir.empty()) return run_scan_dir(scanDir);
// --open-fx-gui <job.json>: one-shot native editor window for a VST3 FX
// (Mastering/FX Rack → "mở VST GUI"); runs until the user closes it.
std::string fxGuiJob;
for (int i = 1; i + 1 < argc; ++i) {
if (std::strcmp(argv[i], "--open-fx-gui") == 0) fxGuiJob = argv[i + 1];
}
if (!fxGuiJob.empty()) return run_open_fx_gui(fxGuiJob);
// 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
std::string shmName = "SonicForge_DAW_IPC";
for (int i = 1; i + 1 < argc; ++i) {