feat(fx-gui): đưa audio vào process GUI — meter VST nhảy theo âm thật qua SHM peek

- GUI process peek ring SHM đầu vào (không consume, không tranh chấp RealtimeFxLoop)
- gọi fx->processAudio() trên instance GUI để meter/UI plugin chuyển động theo âm thật
- performEdit forward sang controller -> meter điều khiển bởi param cũng cập nhật
- bridge /shm: frontend đẩy tên session live vào mỗi GUI đang mở (start/stop/open)
- tests: 127 passed
This commit is contained in:
2026-08-22 17:06:37 +07:00
parent 6ba38286ad
commit 6dfe390bda
7 changed files with 219 additions and 24 deletions
+124 -7
View File
@@ -37,6 +37,7 @@
#endif
#include <windows.h>
#endif
#include "FxRealtimeIPC.h"
#include <algorithm>
#include <cctype>
@@ -341,7 +342,14 @@ public:
Steinberg::uint32 addRef() override { return 1; }
Steinberg::uint32 release() override { return 1; }
tresult beginEdit(Steinberg::Vst::ParamID) override { return kResultOk; }
tresult performEdit(Steinberg::Vst::ParamID, Steinberg::Vst::ParamValue) override { return kResultOk; }
// Forward automation from the plugin's native GUI into the controller so
// controller-driven meters/UI update with the edited parameter. Null until
// the controller is attached (set in vst3FxLoadInner).
Steinberg::Vst::IEditController* controller = nullptr;
tresult performEdit(Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue v) override {
if (controller) controller->setParamFromEdit(id, v);
return kResultOk;
}
tresult endEdit(Steinberg::Vst::ParamID) override { return kResultOk; }
tresult restartComponent(Steinberg::int32) override { return kResultOk; }
};
@@ -479,6 +487,7 @@ bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
s->controllerIsComponent = isSingle;
controller->setComponentHandler(&s->componentHandler);
s->componentHandler.controller = controller.get();
FUnknownPtr<IConnectionPoint> compCP(component);
FUnknownPtr<IConnectionPoint> ctrlCP(controller);
if (compCP && ctrlCP) { compCP->connect(ctrlCP); ctrlCP->connect(compCP); }
@@ -1352,9 +1361,113 @@ void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
}
// --- SHM audio feeder (GUI meters) -----------------------------------------
// The GUI process PEEKS the SAME input ring the realtime session uses (never
// consumes -> no contention with RealtimeFxLoop) and runs fx->processAudio()
// on the GUI plugin instance so its own meters/UI move with real audio.
// Meters show the PRE-chain mix (input ring peek); per-plugin position in the
// chain is approximated (ponytail: feed from a per-plugin output ring if exact
// post-FX metering is ever needed).
struct FxGuiShmCtl {
std::mutex m;
std::string name; // "" = no live session to feed
};
static std::shared_ptr<FxGuiShmCtl>& fxGuiShmCtlRef() {
static std::shared_ptr<FxGuiShmCtl> p;
return p;
}
// Called from FxGuiServer /shm route (HTTP worker thread) when the frontend
// reports which realtime session is live (or "" after it stopped/restarted).
void fxGuiSetShm(const std::string& name) {
auto p = fxGuiShmCtlRef();
if (!p) return;
std::lock_guard<std::mutex> lk(p->m);
p->name = name;
}
struct FxGuiShmView { void* map = nullptr; void* view = nullptr; };
static FxGuiShmView* fxGuiOpenShm(const std::string& name, size_t size) {
int wlen = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, nullptr, 0);
std::wstring wname(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, &wname[0], wlen);
HANDLE map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
if (!map) return nullptr;
void* view = MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size);
if (!view) { CloseHandle(map); return nullptr; }
FxGuiShmView* v = new FxGuiShmView();
v->map = map;
v->view = view;
return v;
}
static void fxGuiCloseShm(FxGuiShmView* v) {
if (!v) return;
if (v->view) UnmapViewOfFile(v->view);
if (v->map) CloseHandle((HANDLE)v->map);
delete v;
}
// SEH frame holds only raw pointers / trivials (C2712 - no unwinding locals).
static void fxGuiFeedAudioSafe(Vst3Fx* fx, const float* inL, const float* inR,
float* outL, float* outR, uint32_t n) {
#ifdef _WIN32
__try { fx->processAudio(inL, inR, outL, outR, n); }
__except (EXCEPTION_EXECUTE_HANDLER) {
std::cerr << "[FxGui] plugin crashed in audio feed" << std::endl;
}
#else
(void)fx; (void)inL; (void)inR; (void)outL; (void)outR; (void)n;
#endif
}
static void fxGuiFeedLoop(Vst3Fx* fx, HWND hwnd, std::shared_ptr<FxGuiShmCtl> ctl) {
FxGuiShmView* v = nullptr;
FxRealtimeIPC* ipc = nullptr;
std::string curName;
uint64_t lastWrite = 0; // feeder-local: newest inWrite already peeked
while (IsWindow(hwnd)) {
std::string name;
{
std::lock_guard<std::mutex> lk(ctl->m);
name = ctl->name;
}
if (name != curName) {
if (v) { fxGuiCloseShm(v); v = nullptr; ipc = nullptr; }
curName = name;
if (!name.empty()) v = fxGuiOpenShm(name, sizeof(FxRealtimeIPC));
ipc = v ? static_cast<FxRealtimeIPC*>(v->view) : nullptr;
lastWrite = 0; // session (re)start -> re-anchor
}
if (ipc && ipc->h.magic == FXRT_MAGIC &&
ipc->h.inWrite > lastWrite && ipc->h.inWrite > ipc->h.inRead) {
if (ipc->h.inSlots == FXRT_IN_SLOTS && ipc->h.blockSize == FXRT_BLOCK) {
const uint32_t slot = (ipc->h.inWrite - 1) & (ipc->h.inSlots - 1);
float L[FXRT_BLOCK], R[FXRT_BLOCK], oL[FXRT_BLOCK], oR[FXRT_BLOCK];
std::memcpy(L, ipc->inL[slot], FXRT_BLOCK * sizeof(float));
std::memcpy(R, ipc->inR[slot], FXRT_BLOCK * sizeof(float));
fxGuiFeedAudioSafe(fx, L, R, oL, oR, FXRT_BLOCK);
lastWrite = ipc->h.inWrite;
}
}
Sleep(2);
}
if (v) fxGuiCloseShm(v);
}
// Detached feeder thread. initialShm may be "" — the frontend pushes the
// live session name later via /shm once the realtime session is up.
static void fxGuiStartAudioFeeder(Vst3Fx* fx, HWND hwnd, const std::string& initialShm) {
auto p = fxGuiShmCtlRef();
if (!p) { p = std::make_shared<FxGuiShmCtl>(); fxGuiShmCtlRef() = p; }
{
std::lock_guard<std::mutex> lk(p->m);
p->name = initialShm;
}
std::thread([fx, hwnd, p]() { fxGuiFeedLoop(fx, hwnd, p); }).detach();
}
// --- 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) {
static int fxGuiParseJob(const std::string& jobPath, std::string& path, std::string& name, std::string& shm) {
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>());
@@ -1368,6 +1481,7 @@ static int fxGuiParseJob(const std::string& jobPath, std::string& path, std::str
const json_object_s* job = static_cast<const json_object_s*>(root->payload);
path = memberString(job, "path", "");
name = memberString(job, "name", "");
shm = memberString(job, "shm", "");
if (path.empty()) { std::cerr << "[FxGui] job missing path" << std::endl; return 1; }
return 0;
}
@@ -1423,9 +1537,9 @@ static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::strin
// 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) {
bool offscreen, LONG_PTR userData, std::string& shmOut) {
std::string path, name;
int rc = fxGuiParseJob(jobPath, path, name);
int rc = fxGuiParseJob(jobPath, path, name, shmOut);
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);
@@ -1442,9 +1556,11 @@ int run_open_fx_gui(const std::string& jobPath) {
Vst3Fx fx;
HWND hwnd = nullptr;
FxGuiCtx ctx;
const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/false, (LONG_PTR)&ctx);
std::string shm;
const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/false, (LONG_PTR)&ctx, shm);
if (rc) return rc;
fxGuiStartWatchdog(hwnd, fxGuiParentPid());
fxGuiStartAudioFeeder(&fx, hwnd, shm);
// 5. Pump messages until the window is destroyed.
MSG msg;
@@ -1475,13 +1591,14 @@ int run_fx_gui_server(const std::string& jobPath) {
// 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);
std::string path, name, shm;
int rc = fxGuiParseJob(jobPath, path, name, shm);
if (rc) return rc;
rc = fxGuiCreateWindow(name, /*offscreen=*/true, 0, &hwnd);
if (rc) return rc;
fxGuiStartWatchdog(hwnd, fxGuiParentPid());
SetPropA(hwnd, "FXGUI_FX", &fx); // for WM_FXGUI_RESIZE on the pump thread
fxGuiStartAudioFeeder(&fx, hwnd, shm);
int loadRc = 0;
const int srv = fxGuiServerLoop(hwnd, [&]() {