Fix master volume + VST3FX GUI state restore on save/reload

- serializeProjectToSchema now writes master volume_db and mastering gui_open_path/name
- deserialize restores masterVolume + masteringSettings.gui_open_path/name
- track vst_gui_open_path/name fallback to live state (not just _persistFxGui*)
- MasteringModal/FXRackModal reopen effects react to isOpen + restored gui path
- openMasterVstGui/openVstGui store gui name for reopen
- bridge: /preset capture endpoint + RenderFxJob.captureState for GUI knob state
This commit is contained in:
2026-08-19 14:44:40 +07:00
parent 8874e42596
commit 2f5c70ac29
6 changed files with 308 additions and 102 deletions
+104
View File
@@ -44,6 +44,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
@@ -258,6 +259,24 @@ static std::string base64Encode(const uint8_t* data, size_t len) {
}
return out;
}
// Append-only variant (no return temporary) so it can run inside an SEH
// __try frame (C2712: __try functions cannot have destructor temporaries).
static void base64EncodeTo(const uint8_t* data, size_t len, std::string& out) {
static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t cap = ((len + 2) / 3) * 4;
out.clear();
out.reserve(cap);
for (size_t i = 0; i < len; i += 3) {
uint32_t n = (uint32_t)data[i] << 16;
if (i + 1 < len) n |= (uint32_t)data[i + 1] << 8;
if (i + 2 < len) n |= (uint32_t)data[i + 2];
out += tbl[(n >> 18) & 63];
out += tbl[(n >> 12) & 63];
out += (i + 1 < len) ? tbl[(n >> 6) & 63] : '=';
out += (i + 2 < len) ? tbl[n & 63] : '=';
}
}
static int b64val(unsigned char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
@@ -619,6 +638,43 @@ public:
std::cout << "[RenderFx] preset applied component=" << comp.size()
<< " controller=" << ctrl.size() << std::endl;
return true;
#endif
}
// Capture the plugin's CURRENT state (component + edit controller) as the
// bridge blob [4B BE compLen][comp][4B BE ctrlLen][ctrl] base64 — same
// format applyPreset accepts. Lets the GUI bridge report knob tweaks so a
// saved project can restore them. Must run on the thread owning the plugin.
void captureState(std::string& out) {
#ifndef HAVE_VST3SDK
(void)out;
return;
#else
auto* s = static_cast<Vst3FxState*>(state_);
if (!s) return;
std::vector<uint8_t> comp, ctrl;
{
Steinberg::MemoryStream stream;
if (s->component && s->component->getState(&stream) == kResultOk && stream.getSize() > 0)
comp.assign(stream.getData(), stream.getData() + stream.getSize());
}
if (!s->controllerIsComponent) {
Steinberg::MemoryStream stream;
if (s->controller && s->controller->getState(&stream) == kResultOk && stream.getSize() > 0)
ctrl.assign(stream.getData(), stream.getData() + stream.getSize());
}
if (comp.empty() && ctrl.empty()) return;
std::vector<uint8_t> blob;
auto append32 = [&blob](uint32_t v) {
blob.push_back((uint8_t)(v >> 24));
blob.push_back((uint8_t)(v >> 16));
blob.push_back((uint8_t)(v >> 8));
blob.push_back((uint8_t)v);
};
append32((uint32_t)comp.size());
blob.insert(blob.end(), comp.begin(), comp.end());
append32((uint32_t)ctrl.size());
blob.insert(blob.end(), ctrl.begin(), ctrl.end());
base64EncodeTo(blob.data(), blob.size(), out);
#endif
}
// Native editor: attach the plugin's IPlugView to a host window (must run
@@ -1103,6 +1159,7 @@ void RealtimeFxChain::Impl::workerLoop() {
// Worker (HTTP) thread -> pump thread editor-resize request. Declared
// outside the anonymous namespace: the definition sits after the namespace.
static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h);
static void fxGuiCaptureSafe(void* fx, std::string& out);
namespace {
@@ -1116,6 +1173,16 @@ struct FxGuiCtx {
// checkSizeConstraint/onSize must run on the thread that owns the editor.
#define WM_FXGUI_RESIZE (WM_APP + 0x51)
// --- preset capture (GUI state -> project save) ------------------------
// getState must run on the pump thread that owns the plugin, so the HTTP
// worker posts WM_FXGUI_CAPTURE and waits (up to timeoutMs) for the pump
// thread to park the base64 blob. Empty outB64 = timeout or no state.
#define WM_FXGUI_CAPTURE (WM_APP + 0x52)
static std::mutex& gCapMutex() { static std::mutex m; return m; }
static std::condition_variable& gCapCv() { static std::condition_variable cv; return cv; }
static std::string& gCapB64() { static std::string s; return s; }
static bool& gCapDone() { static bool b = false; return b; }
LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
FxGuiCtx* ctx = (FxGuiCtx*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
switch (msg) {
@@ -1128,6 +1195,17 @@ LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
case WM_FXGUI_RESIZE:
fxGuiResizeSafe((void*)GetPropA(hwnd, "FXGUI_FX"), hwnd, (int)wp, (int)lp);
return 0;
case WM_FXGUI_CAPTURE: {
std::string b64;
fxGuiCaptureSafe((void*)GetPropA(hwnd, "FXGUI_FX"), b64);
{
std::lock_guard<std::mutex> lk(gCapMutex());
gCapB64() = std::move(b64);
gCapDone() = true;
}
gCapCv().notify_one();
return 0;
}
default:
return DefWindowProcA(hwnd, msg, wp, lp);
}
@@ -1161,11 +1239,37 @@ static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h) {
(void)fx; (void)hwnd; (void)w; (void)h;
#endif
}
static void fxGuiCaptureSafe(void* fx, std::string& out) {
#ifdef HAVE_VST3SDK
__try { static_cast<Vst3Fx*>(fx)->captureState(out); }
__except (EXCEPTION_EXECUTE_HANDLER) {}
#else
(void)fx; (void)out;
#endif
}
// Worker (HTTP) thread asks the GUI pump thread to resize the plugin view.
void fxGuiRequestResize(HWND hwnd, int w, int h) {
PostMessageA(hwnd, WM_FXGUI_RESIZE, (WPARAM)w, (LPARAM)h);
}
// Capture the GUI plugin state on the pump thread; blocks up to timeoutMs.
void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
HWND hwnd = (HWND)hwndV;
{
std::lock_guard<std::mutex> lk(gCapMutex());
gCapDone() = false;
gCapB64().clear();
}
PostMessageA(hwnd, WM_FXGUI_CAPTURE, 0, 0);
{
std::unique_lock<std::mutex> lk(gCapMutex());
if (!gCapCv().wait_for(lk, std::chrono::milliseconds(timeoutMs), [] { return gCapDone(); }))
return; // timeout - outB64 stays empty
outB64 = gCapB64();
}
}
// --- 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) {