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