2099 lines
88 KiB
C++
2099 lines
88 KiB
C++
// native_bridge/src/RenderFxJob.cpp
|
||
// Offline audio-FX render mode (Phase 0+1 of PLAN_MASTERBUS_FX_RACK_VST.md):
|
||
// `daw_vst_bridge --render-fx <job.json> --in <input.wav> --out <output.wav>`
|
||
// processes an existing WAV through a VST3 effect chain (audio-in/audio-out,
|
||
// no MIDI) plus builtin slots (gain/normalize). Also `--scan <dir>` which
|
||
// classifies VST3 modules as instrument/effect.
|
||
//
|
||
// Same JSON/b64/WAV helpers as RenderJob.cpp (sheredom/json.h vendored with
|
||
// the VST3 SDK). The VST3 host pattern is the Vst3Instrument.cpp one minus
|
||
// MIDI/GUI — component + controller, audio buses, processData with input
|
||
// buffers copied directly (setChannelBuffers is unusable: prepare() owns them).
|
||
#include "RenderFxJob.h"
|
||
|
||
// sheredom/json.h (public domain) — vendored copy; SDK copy as fallback.
|
||
#if __has_include("sheredom_json.h")
|
||
#include "sheredom_json.h"
|
||
#elif __has_include("vst3sdk/public.sdk/source/vst/moduleinfo/json.h")
|
||
#include "vst3sdk/public.sdk/source/vst/moduleinfo/json.h"
|
||
#endif
|
||
#ifdef HAVE_VST3SDK
|
||
#include "public.sdk/source/vst/hosting/module.h"
|
||
#include "public.sdk/source/vst/hosting/hostclasses.h"
|
||
#include "public.sdk/source/vst/hosting/processdata.h"
|
||
#include "public.sdk/source/vst/hosting/parameterchanges.h"
|
||
#include "public.sdk/source/vst/vstpresetfile.h"
|
||
#include "public.sdk/source/common/memorystream.h"
|
||
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
||
#include "pluginterfaces/vst/ivstcomponent.h"
|
||
#include "pluginterfaces/vst/ivsteditcontroller.h"
|
||
#include "pluginterfaces/vst/ivstprocesscontext.h"
|
||
#include "pluginterfaces/vst/ivstmessage.h"
|
||
#include "pluginterfaces/gui/iplugview.h"
|
||
#endif
|
||
|
||
#ifdef _WIN32
|
||
#ifndef NOMINMAX
|
||
#define NOMINMAX
|
||
#endif
|
||
#include <windows.h>
|
||
#endif
|
||
#include "FxRealtimeIPC.h"
|
||
#include "BuiltinFxChain.h"
|
||
|
||
#include <algorithm>
|
||
#include <atomic>
|
||
#include <cctype>
|
||
#include <cmath>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
#include <chrono>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <iostream>
|
||
#include <condition_variable>
|
||
#include <functional>
|
||
#include <iterator>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <mutex>
|
||
#include <string>
|
||
#include <thread>
|
||
#include <vector>
|
||
|
||
namespace {
|
||
|
||
// --- sheredom/json.h helpers (same as RenderJob.cpp) ------------------------
|
||
const json_object_element_s* member(const json_object_s* o, const char* key) {
|
||
for (const json_object_element_s* e = o ? o->start : nullptr; e; e = e->next)
|
||
if (e->name && e->name->string && std::strcmp(e->name->string, key) == 0)
|
||
return e;
|
||
return nullptr;
|
||
}
|
||
|
||
const json_value_s* memberValue(const json_object_s* o, const char* key) {
|
||
const json_object_element_s* m = member(o, key);
|
||
return m ? m->value : nullptr;
|
||
}
|
||
|
||
std::string memberString(const json_object_s* o, const char* key, const std::string& def) {
|
||
const json_value_s* v = memberValue(o, key);
|
||
if (v && v->type == json_type_string) {
|
||
const auto* s = static_cast<const json_string_s*>(v->payload);
|
||
return std::string(s->string, s->string_size);
|
||
}
|
||
return def;
|
||
}
|
||
|
||
bool memberNumber(const json_object_s* o, const char* key, double& out) {
|
||
const json_value_s* v = memberValue(o, key);
|
||
if (v && v->type == json_type_number) {
|
||
out = std::atof(static_cast<const json_number_s*>(v->payload)->number);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
int64_t memberInt(const json_object_s* o, const char* key, int64_t def) {
|
||
double d;
|
||
return memberNumber(o, key, d) ? (int64_t)d : def;
|
||
}
|
||
|
||
bool memberBool(const json_object_s* o, const char* key, bool def) {
|
||
const json_value_s* v = memberValue(o, key);
|
||
if (v && v->type == json_type_true) return true;
|
||
if (v && v->type == json_type_false) return false;
|
||
return def;
|
||
}
|
||
|
||
// --- WAV writer (stdlib only, IEEE Float32 stereo, little-endian) ------------
|
||
// Lossless: keeps the full float32 render buffer bit-exact. No PCM truncation
|
||
// and no dither here — dither/quantization belongs ONLY to a final export
|
||
// stage (lossless-audio-compliance skill, Rules 4 & 7). Format tag 3.
|
||
void writeU16(std::ofstream& f, uint16_t v) {
|
||
char b[2] = { (char)(v & 0xFF), (char)((v >> 8) & 0xFF) };
|
||
f.write(b, 2);
|
||
}
|
||
void writeU32(std::ofstream& f, uint32_t v) {
|
||
char b[4] = { (char)(v & 0xFF), (char)((v >> 8) & 0xFF),
|
||
(char)((v >> 16) & 0xFF), (char)((v >> 24) & 0xFF) };
|
||
f.write(b, 4);
|
||
}
|
||
void writeWavHeader(std::ofstream& f, uint32_t sampleRate) {
|
||
f.write("RIFF", 4);
|
||
writeU32(f, 36);
|
||
f.write("WAVE", 4);
|
||
f.write("fmt ", 4);
|
||
writeU32(f, 16);
|
||
writeU16(f, 3); // IEEE float
|
||
writeU16(f, 2);
|
||
writeU32(f, sampleRate);
|
||
writeU32(f, sampleRate * 8); // byte rate = sr * 2ch * 4B
|
||
writeU16(f, 8);
|
||
writeU16(f, 32);
|
||
f.write("data", 4);
|
||
writeU32(f, 0);
|
||
}
|
||
void finishWav(std::ofstream& f, uint64_t dataBytes) {
|
||
f.seekp(4);
|
||
writeU32(f, (uint32_t)(36 + dataBytes));
|
||
f.seekp(40);
|
||
writeU32(f, (uint32_t)dataBytes);
|
||
f.flush();
|
||
}
|
||
void writeFrames(std::ofstream& f, const float* L, const float* R, uint32_t n) {
|
||
// IEEE float32 little-endian, bit-exact: no clamp, no quantization.
|
||
for (uint32_t i = 0; i < n; ++i) {
|
||
f.write(reinterpret_cast<const char*>(&L[i]), 4);
|
||
f.write(reinterpret_cast<const char*>(&R[i]), 4);
|
||
}
|
||
}
|
||
|
||
// --- WAV reader (stdlib only): PCM 16/24/32-bit + IEEE float32, mono/stereo
|
||
// → stereo float32. Returns false on any parse error (job/argument error). ---
|
||
struct WavIn {
|
||
uint32_t sampleRate = 0;
|
||
uint16_t channels = 0;
|
||
uint16_t bits = 0;
|
||
std::vector<float> L, R; // same length
|
||
};
|
||
|
||
uint32_t readU32le(const uint8_t* p) {
|
||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||
}
|
||
uint16_t readU16le(const uint8_t* p) {
|
||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||
}
|
||
|
||
bool readWav(const std::string& path, WavIn& out) {
|
||
std::ifstream f(path, std::ios::binary);
|
||
if (!f) { std::cerr << "[RenderFx] cannot read input: " << path << std::endl; return false; }
|
||
std::vector<uint8_t> buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||
if (buf.size() < 44) return false;
|
||
if (std::memcmp(buf.data(), "RIFF", 4) != 0 || std::memcmp(buf.data() + 8, "WAVE", 4) != 0)
|
||
return false;
|
||
|
||
uint16_t fmtTag = 0, channels = 0, bits = 0;
|
||
uint32_t sampleRate = 0;
|
||
size_t dataOff = 0, dataLen = 0;
|
||
size_t p = 12;
|
||
bool foundFmt = false, foundData = false;
|
||
while (p + 8 <= buf.size()) {
|
||
const char* id = reinterpret_cast<const char*>(buf.data() + p);
|
||
uint32_t sz = readU32le(buf.data() + p + 4);
|
||
size_t chunkStart = p + 8;
|
||
if (chunkStart + sz > buf.size()) break; // truncated chunk — stop
|
||
if (std::memcmp(id, "fmt ", 4) == 0 && sz >= 16) {
|
||
fmtTag = readU16le(buf.data() + chunkStart);
|
||
channels = readU16le(buf.data() + chunkStart + 2);
|
||
sampleRate = readU32le(buf.data() + chunkStart + 4);
|
||
bits = readU16le(buf.data() + chunkStart + 14);
|
||
foundFmt = true;
|
||
} else if (std::memcmp(id, "data", 4) == 0) {
|
||
dataOff = chunkStart;
|
||
dataLen = sz;
|
||
foundData = true;
|
||
break;
|
||
}
|
||
p = chunkStart + sz + (sz & 1);
|
||
}
|
||
if (!foundFmt || !foundData) return false;
|
||
// Supported: PCM 16/24/32 and IEEE float 32; mono or stereo.
|
||
const bool isFloat = (fmtTag == 3);
|
||
if (!(fmtTag == 1 || isFloat)) {
|
||
std::cerr << "[RenderFx] unsupported WAV format tag " << fmtTag << " (need PCM/float)" << std::endl;
|
||
return false;
|
||
}
|
||
if (channels < 1 || channels > 2) {
|
||
std::cerr << "[RenderFx] unsupported WAV channel count " << channels << std::endl;
|
||
return false;
|
||
}
|
||
const uint16_t bytes = (bits + 7) / 8;
|
||
if (bytes != 2 && bytes != 3 && bytes != 4) return false;
|
||
|
||
const size_t frameBytes = (size_t)channels * bytes;
|
||
const size_t nFrames = dataLen / frameBytes;
|
||
out.sampleRate = sampleRate;
|
||
out.channels = channels;
|
||
out.bits = bits;
|
||
out.L.assign(nFrames, 0.0f);
|
||
out.R.assign(nFrames, 0.0f);
|
||
const uint8_t* d = buf.data() + dataOff;
|
||
for (size_t i = 0; i < nFrames; ++i) {
|
||
float l = 0.f, r = 0.f;
|
||
for (uint16_t c = 0; c < channels; ++c) {
|
||
const uint8_t* s = d + (i * channels + c) * bytes;
|
||
float v = 0.f;
|
||
if (isFloat && bytes == 4) {
|
||
uint32_t u = readU32le(s);
|
||
float fv;
|
||
std::memcpy(&fv, &u, 4);
|
||
v = fv;
|
||
} else if (bytes == 2) {
|
||
v = (float)(int16_t)((uint16_t)s[0] | ((uint16_t)s[1] << 8)) / 32768.f;
|
||
} else if (bytes == 3) {
|
||
int32_t iv = (int32_t)s[0] | ((int32_t)s[1] << 8) | ((int32_t)s[2] << 16);
|
||
if (iv & 0x800000) iv |= ~0xFFFFFF; // sign extend
|
||
v = (float)iv / 8388608.f;
|
||
} else { // bytes == 4, PCM int32
|
||
int32_t iv = (int32_t)readU32le(s);
|
||
v = (float)iv / 2147483648.f;
|
||
}
|
||
if (c == 0) l = v; else r = v;
|
||
}
|
||
out.L[i] = l;
|
||
out.R[i] = (channels == 1) ? l : r;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// --- base64 (RFC 4648) — same helpers as RenderJob.cpp ----------------------
|
||
static const char* kBase64Tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||
static std::string base64Encode(const uint8_t* data, size_t len) {
|
||
std::string out;
|
||
out.reserve(((len + 2) / 3) * 4);
|
||
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 += kBase64Tbl[(n >> 18) & 63];
|
||
out += kBase64Tbl[(n >> 12) & 63];
|
||
out += (i + 1 < len) ? kBase64Tbl[(n >> 6) & 63] : '=';
|
||
out += (i + 2 < len) ? kBase64Tbl[n & 63] : '=';
|
||
}
|
||
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;
|
||
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||
if (c == '+') return 62;
|
||
if (c == '/') return 63;
|
||
return -1;
|
||
}
|
||
static bool base64Decode(const std::string& in, std::vector<uint8_t>& out) {
|
||
out.clear();
|
||
out.reserve((in.size() / 4) * 3);
|
||
uint32_t acc = 0;
|
||
int bits = 0;
|
||
for (unsigned char ch : in) {
|
||
if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue;
|
||
int v = b64val(ch);
|
||
if (v < 0) return false;
|
||
acc = (acc << 6) | (uint32_t)v;
|
||
bits += 6;
|
||
if (bits >= 8) {
|
||
bits -= 8;
|
||
out.push_back((uint8_t)((acc >> bits) & 0xFF));
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// --- VST3 audio-effect host (offline; no MIDI, no GUI) ----------------------
|
||
#ifdef HAVE_VST3SDK
|
||
using Steinberg::tresult;
|
||
using Steinberg::kResultOk;
|
||
using Steinberg::kResultTrue;
|
||
using Steinberg::kResultFalse;
|
||
using Steinberg::kNoInterface;
|
||
using Steinberg::FUnknownPtr;
|
||
using Steinberg::IPtr;
|
||
using Steinberg::owned;
|
||
using Steinberg::FIDString;
|
||
using Steinberg::int32;
|
||
using Steinberg::uint32;
|
||
|
||
using Steinberg::Vst::IComponent;
|
||
using Steinberg::Vst::IEditController;
|
||
using Steinberg::Vst::IAudioProcessor;
|
||
using Steinberg::Vst::IComponentHandler;
|
||
using Steinberg::Vst::IConnectionPoint;
|
||
using Steinberg::Vst::ProcessSetup;
|
||
using Steinberg::Vst::ProcessContext;
|
||
using Steinberg::Vst::HostProcessData;
|
||
using Steinberg::Vst::HostApplication;
|
||
using Steinberg::Vst::BusInfo;
|
||
using Steinberg::Vst::kAudio;
|
||
using Steinberg::Vst::kEvent;
|
||
using Steinberg::Vst::kInput;
|
||
using Steinberg::Vst::kOutput;
|
||
using Steinberg::Vst::kRealtime;
|
||
using Steinberg::Vst::kSample32;
|
||
|
||
class HostComponentHandlerFx : public IComponentHandler {
|
||
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 beginEdit(Steinberg::Vst::ParamID) 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->setParamNormalized(id, v);
|
||
return kResultOk;
|
||
}
|
||
tresult endEdit(Steinberg::Vst::ParamID) override { return kResultOk; }
|
||
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;
|
||
IPtr<IEditController> controller;
|
||
IPtr<HostApplication> hostApp;
|
||
HostProcessData processData;
|
||
ProcessContext processContext;
|
||
HostComponentHandlerFx componentHandler;
|
||
int32 inputChannels = 2;
|
||
int32 outputChannels = 2;
|
||
int32 latencySamples = 0; // PDC (REPORT_LATENCY, Phase 2.8)
|
||
bool controllerIsComponent = false;
|
||
Steinberg::IPlugFrame* plugFrame = nullptr; // owned; freed in closeEditor
|
||
IPtr<IPlugView> view; // editor view while GUI open
|
||
Steinberg::Vst::ParameterChanges paramChanges; // live setParam queue -> inputParameterChanges
|
||
std::mutex paramMutex; // setParam (loop/worker) vs processAudio
|
||
std::map<std::string, Steinberg::Vst::ParamID> paramByName; // lower(title/shortTitle) -> id
|
||
};
|
||
|
||
// Class selection for FX: any kVstAudioEffectClass whose subcategory does NOT
|
||
// contain "Instrument" (Ozone/ScalerAudio et al.). Pick the first match.
|
||
bool pickFxClass(const VST3::Hosting::PluginFactory& factory,
|
||
VST3::Hosting::ClassInfo& out) {
|
||
auto infos = factory.classInfos(); // temporary vector — copy, never keep pointer
|
||
for (const auto& ci : infos) {
|
||
if (ci.category() != kVstAudioEffectClass) continue;
|
||
if (ci.subCategoriesString().find("Instrument") != std::string::npos) continue;
|
||
out = ci;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Must live OUTSIDE any __try (C2712: no C++ unwinding inside SEH frames).
|
||
// Windows DLL search: LoadLibraryW (VST3 SDK) does NOT search the plugin's own
|
||
// directory for dependency DLLs -> ERROR_MOD_NOT_FOUND (0x7E) when a VST3 needs
|
||
// helper DLLs next to it (bundle Contents\x86_64-win or sidecar files). Point
|
||
// the DLL search path at the plugin directory before Module::create.
|
||
#ifdef _WIN32
|
||
static void setPluginSearchPath(const std::string& path) {
|
||
if (path.empty()) return;
|
||
std::string dir;
|
||
// Folder bundle (.vst3 dir): deps live in Contents\<arch>\
|
||
// Single file (.vst3/.dll): deps live next to it.
|
||
size_t baseSlash = path.find_last_of("/\\");
|
||
std::string base = baseSlash == std::string::npos ? "" : path.substr(baseSlash + 1);
|
||
std::string parent = baseSlash == std::string::npos ? "" : path.substr(0, baseSlash);
|
||
if (!base.empty() && base.size() > 4 &&
|
||
base.substr(base.size() - 4) == ".vst3") {
|
||
// Could be a folder bundle OR a single .vst3 file. Check folder first.
|
||
DWORD attr = GetFileAttributesA(path.c_str());
|
||
if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) {
|
||
dir = path + "\\Contents\\x86_64-win";
|
||
DWORD dAttr = GetFileAttributesA(dir.c_str());
|
||
if (dAttr == INVALID_FILE_ATTRIBUTES)
|
||
dir = path + "\\Contents";
|
||
} else {
|
||
dir = parent;
|
||
}
|
||
} else {
|
||
dir = parent;
|
||
}
|
||
if (dir.empty()) return;
|
||
// SetDllDirectoryW is process-global, fine here: bridge loads one plugin
|
||
// at a time (render slots serial; realtime chain loads on its own thread).
|
||
std::wstring wdir(dir.begin(), dir.end());
|
||
SetDllDirectoryW(wdir.c_str());
|
||
std::cerr << "[RenderFx] dll search dir: " << dir << std::endl;
|
||
}
|
||
#endif
|
||
|
||
bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
|
||
int32 maxBlockSize, std::string& err) {
|
||
using namespace VST3::Hosting;
|
||
#ifdef _WIN32
|
||
setPluginSearchPath(path);
|
||
#endif
|
||
Module::Ptr module = Module::create(path, err);
|
||
if (!module) { std::cerr << "[RenderFx] STEP module.create FAILED: " << err << std::endl; return false; }
|
||
std::cerr << "[RenderFx] STEP module.create OK" << std::endl;
|
||
const PluginFactory& factory = module->getFactory();
|
||
ClassInfo chosen;
|
||
if (!pickFxClass(factory, chosen)) {
|
||
err = "no audio-effect class in " + path;
|
||
std::cerr << "[RenderFx] STEP pickFxClass FAILED: " << err << std::endl;
|
||
return false;
|
||
}
|
||
std::cerr << "[RenderFx] STEP pickFxClass OK: " << chosen.name() << std::endl;
|
||
IPtr<IComponent> component = factory.createInstance<IComponent>(chosen.ID());
|
||
if (!component) { std::cerr << "[RenderFx] STEP createInstance FAILED" << std::endl; err = "createInstance<IComponent> failed"; return false; }
|
||
std::cerr << "[RenderFx] STEP createInstance OK" << std::endl;
|
||
IPtr<HostApplication> hostApp = owned(new HostApplication());
|
||
FUnknownPtr<Steinberg::IPluginBase> plugBase(component.get());
|
||
if (!plugBase || plugBase->initialize(hostApp) != kResultOk) {
|
||
err = "component initialize failed";
|
||
std::cerr << "[RenderFx] STEP initialize FAILED" << std::endl;
|
||
return false;
|
||
}
|
||
std::cerr << "[RenderFx] STEP initialize OK" << std::endl;
|
||
IPtr<IEditController> controller;
|
||
bool isSingle = false;
|
||
if (component->queryInterface(IEditController::iid, (void**)&controller) == kResultTrue) {
|
||
isSingle = true;
|
||
} else {
|
||
Steinberg::TUID cid = {};
|
||
tresult cidRes = component->getControllerClassId(cid);
|
||
if (cidRes == kResultTrue || cidRes == kResultOk) {
|
||
controller = factory.createInstance<IEditController>(VST3::UID(cid));
|
||
if (controller) {
|
||
FUnknownPtr<Steinberg::IPluginBase> ctrlBase(controller.get());
|
||
if (!ctrlBase || ctrlBase->initialize(hostApp) != kResultOk) controller = nullptr;
|
||
}
|
||
}
|
||
}
|
||
if (!controller) { err = "no edit controller"; std::cerr << "[RenderFx] STEP controller FAILED" << std::endl; return false; }
|
||
std::cerr << "[RenderFx] STEP controller OK" << std::endl;
|
||
|
||
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); }
|
||
std::cerr << "[RenderFx] STEP connection points OK" << std::endl;
|
||
|
||
int32 numAudioInputs = component->getBusCount(kAudio, kInput);
|
||
for (int32 i = 0; i < numAudioInputs; ++i) component->activateBus(kAudio, kInput, i, true);
|
||
int32 numAudioOutputs = component->getBusCount(kAudio, kOutput);
|
||
for (int32 i = 0; i < numAudioOutputs; ++i) component->activateBus(kAudio, kOutput, i, true);
|
||
int32 numEventInputs = component->getBusCount(kEvent, kInput);
|
||
for (int32 i = 0; i < numEventInputs; ++i) component->activateBus(kEvent, kInput, i, true);
|
||
int32 numEventOutputs = component->getBusCount(kEvent, kOutput);
|
||
for (int32 i = 0; i < numEventOutputs; ++i) component->activateBus(kEvent, kOutput, i, true);
|
||
std::cerr << "[RenderFx] STEP buses activated (ain=" << numAudioInputs << " aout=" << numAudioOutputs << ")" << std::endl;
|
||
if (numAudioInputs < 1 || numAudioOutputs < 1) {
|
||
err = "plugin has no audio in/out (instrument?)"; return false;
|
||
}
|
||
|
||
FUnknownPtr<IAudioProcessor> processor(component);
|
||
if (!processor) { err = "no IAudioProcessor"; return false; }
|
||
ProcessSetup setup{kRealtime, kSample32, maxBlockSize, sampleRate};
|
||
std::cerr << "[RenderFx] STEP setupProcessing begin" << std::endl;
|
||
if (processor->setupProcessing(setup) != kResultOk) { err = "setupProcessing failed"; return false; }
|
||
std::cerr << "[RenderFx] STEP setupProcessing OK" << std::endl;
|
||
std::cerr << "[RenderFx] STEP setActive begin" << std::endl;
|
||
if (component->setActive(true) != kResultOk) { err = "setActive failed"; return false; }
|
||
std::cerr << "[RenderFx] STEP setActive OK" << std::endl;
|
||
processor->setProcessing(true);
|
||
std::cerr << "[RenderFx] STEP setProcessing OK" << std::endl;
|
||
s->latencySamples = processor->getLatencySamples();
|
||
std::cerr << "[RenderFx] STEP getLatencySamples = " << s->latencySamples << std::endl;
|
||
if (!s->processData.prepare(*component, maxBlockSize, kSample32)) {
|
||
err = "processData.prepare failed"; return false;
|
||
}
|
||
std::cerr << "[RenderFx] STEP processData.prepare OK" << std::endl;
|
||
|
||
BusInfo bi = {};
|
||
if (component->getBusInfo(kAudio, kInput, 0, bi) == kResultOk && bi.channelCount > 0)
|
||
s->inputChannels = bi.channelCount;
|
||
bi = {};
|
||
if (component->getBusInfo(kAudio, kOutput, 0, bi) == kResultOk && bi.channelCount > 0)
|
||
s->outputChannels = bi.channelCount;
|
||
|
||
// Live setParam: map lowercased title/shortTitle -> ParamID so the
|
||
// realtime chain can address VST3 params by name (no UI-side tags).
|
||
// Titles are UTF-16 (String128); keep the ASCII subset lowercased.
|
||
for (int32 pi = 0; pi < controller->getParameterCount(); ++pi) {
|
||
Steinberg::Vst::ParameterInfo info = {};
|
||
if (controller->getParameterInfo(pi, info) != kResultOk) continue;
|
||
auto addName = [&](const Steinberg::Vst::TChar* nm) {
|
||
if (!nm) return;
|
||
std::string k;
|
||
for (const Steinberg::Vst::TChar* p = nm; p && *p && k.size() < 64; ++p) {
|
||
if (*p < 0x80) k.push_back((char)std::tolower((unsigned char)*p));
|
||
}
|
||
if (k.empty()) return;
|
||
s->paramByName[k] = info.id;
|
||
};
|
||
addName(info.title);
|
||
addName(info.shortTitle);
|
||
}
|
||
|
||
s->module = std::move(module);
|
||
s->component = std::move(component);
|
||
s->controller = std::move(controller);
|
||
s->hostApp = std::move(hostApp);
|
||
s->processContext.sampleRate = sampleRate;
|
||
s->processContext.tempo = 120.0;
|
||
s->processContext.timeSigNumerator = 4;
|
||
s->processContext.timeSigDenominator = 4;
|
||
s->processContext.state = ProcessContext::kPlaying | ProcessContext::kTempoValid |
|
||
ProcessContext::kTimeSigValid | ProcessContext::kProjectTimeMusicValid;
|
||
std::cout << "[RenderFx] VST3 FX loaded " << path << " (" << chosen.name()
|
||
<< ") in=" << s->inputChannels << " out=" << s->outputChannels << std::endl;
|
||
return true;
|
||
}
|
||
#endif // HAVE_VST3SDK
|
||
|
||
class Vst3Fx {
|
||
public:
|
||
Vst3Fx() = default;
|
||
~Vst3Fx() {
|
||
#ifdef HAVE_VST3SDK
|
||
if (!state_) return;
|
||
auto* s = static_cast<Vst3FxState*>(state_);
|
||
if (s->component) {
|
||
FUnknownPtr<IAudioProcessor> processor(s->component);
|
||
if (processor) processor->setProcessing(false);
|
||
s->component->setActive(false);
|
||
s->component->terminate();
|
||
}
|
||
if (s->controller && !s->controllerIsComponent) s->controller->terminate();
|
||
s->processData.unprepare();
|
||
delete s;
|
||
state_ = nullptr;
|
||
#endif
|
||
}
|
||
bool load(const std::string& path, double sampleRate, int32 maxBlockSize) {
|
||
#ifndef HAVE_VST3SDK
|
||
(void)path; (void)sampleRate; (void)maxBlockSize;
|
||
return false;
|
||
#else
|
||
if (state_) return true;
|
||
auto* s = new Vst3FxState();
|
||
std::string err;
|
||
if (!vst3FxLoadInner(s, path, sampleRate, maxBlockSize, err)) {
|
||
std::cerr << "[RenderFx] VST3 FX load FAILED: " << err << std::endl;
|
||
delete s;
|
||
return false;
|
||
}
|
||
state_ = s;
|
||
return true;
|
||
#endif
|
||
}
|
||
// Audio in → audio out, one block. Must be SEH-wrapped by the caller.
|
||
void processAudio(const float* inL, const float* inR,
|
||
float* outL, float* outR, uint32_t n) {
|
||
#ifndef HAVE_VST3SDK
|
||
(void)inL; (void)inR; (void)outL; (void)outR; (void)n;
|
||
#else
|
||
auto* s = static_cast<Vst3FxState*>(state_);
|
||
if (!s || !s->component || n == 0) return;
|
||
FUnknownPtr<IAudioProcessor> processor(s->component);
|
||
if (!processor) return;
|
||
s->processData.processMode = kRealtime;
|
||
s->processData.numSamples = (int32)n;
|
||
s->processData.inputEvents = nullptr;
|
||
{
|
||
std::lock_guard<std::mutex> lk(s->paramMutex);
|
||
s->processData.inputParameterChanges = &s->paramChanges;
|
||
}
|
||
s->processData.processContext = &s->processContext;
|
||
s->processContext.projectTimeSamples += n;
|
||
s->processContext.projectTimeMusic =
|
||
(double)s->processContext.projectTimeSamples / s->processContext.sampleRate *
|
||
(s->processContext.tempo / 60.0);
|
||
// Copy input into the prepared (owned) input buffers — direct write,
|
||
// setChannelBuffers is unusable (channelBufferOwner=true after prepare).
|
||
if (s->processData.numInputs > 0) {
|
||
const Steinberg::Vst::AudioBusBuffers& ib = s->processData.inputs[0];
|
||
for (int32 c = 0; c < ib.numChannels; ++c) {
|
||
float* dst = ib.channelBuffers32[c];
|
||
if (!dst) continue;
|
||
const float* src = (c == 0) ? inL : (c == 1 ? inR : inL);
|
||
std::memcpy(dst, src, n * sizeof(float));
|
||
}
|
||
}
|
||
// Host buffers must be zeroed before process (plugins skip output
|
||
// leave garbage otherwise — same rule as Vst3Instrument).
|
||
if (s->processData.numOutputs > 0) {
|
||
for (int32 b = 0; b < s->processData.numOutputs; ++b) {
|
||
const Steinberg::Vst::AudioBusBuffers& ob = s->processData.outputs[b];
|
||
for (int32 c = 0; c < ob.numChannels; ++c)
|
||
if (ob.channelBuffers32[c])
|
||
std::memset(ob.channelBuffers32[c], 0, n * sizeof(float));
|
||
}
|
||
}
|
||
tresult pr = processor->process(s->processData);
|
||
if (pr >= 0 && s->processData.numOutputs > 0) {
|
||
const Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0];
|
||
const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
|
||
const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr;
|
||
if (buf0) std::memcpy(outL, buf0, n * sizeof(float));
|
||
else std::memset(outL, 0, n * sizeof(float));
|
||
if (buf1) std::memcpy(outR, buf1, n * sizeof(float));
|
||
else if (buf0) std::memcpy(outR, buf0, n * sizeof(float)); // mono → stereo
|
||
else std::memset(outR, 0, n * sizeof(float));
|
||
} else {
|
||
std::memset(outL, 0, n * sizeof(float));
|
||
std::memset(outR, 0, n * sizeof(float));
|
||
}
|
||
{
|
||
// Each block consumes the queue once; clear so the next setParam
|
||
// writes fresh points (adelay reads the LAST point).
|
||
std::lock_guard<std::mutex> lk(s->paramMutex);
|
||
s->paramChanges.clearQueue();
|
||
}
|
||
#endif
|
||
}
|
||
// Apply preset: base64 of a raw .vstpreset FILE (VST3 magic + chunk list)
|
||
// OR of the bridge state blob [4B BE compLen][comp][4B BE ctrlLen][ctrl].
|
||
bool applyPreset(const std::string& b64) {
|
||
#ifndef HAVE_VST3SDK
|
||
(void)b64;
|
||
return false;
|
||
#else
|
||
std::vector<uint8_t> bytes;
|
||
if (!base64Decode(b64, bytes) || bytes.empty()) return false;
|
||
std::vector<uint8_t> comp, ctrl;
|
||
if (bytes.size() >= 4 && std::memcmp(bytes.data(), "VST3", 4) == 0) {
|
||
// Raw .vstpreset file: parse with the SDK PresetFile.
|
||
Steinberg::MemoryStream stream;
|
||
if (stream.write(bytes.data(), (int32)bytes.size(), nullptr) != Steinberg::kResultOk) return false;
|
||
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
|
||
Steinberg::Vst::PresetFile pf(&stream);
|
||
if (!pf.readChunkList()) return false;
|
||
auto readChunk = [&](Steinberg::Vst::ChunkType which, std::vector<uint8_t>& outc) -> bool {
|
||
const Steinberg::Vst::PresetFile::Entry* e = pf.getEntry(which);
|
||
if (!e) return true;
|
||
bool seeked = (which == Steinberg::Vst::kComponentState)
|
||
? pf.seekToComponentState() : pf.seekToControllerState();
|
||
if (!seeked) return false;
|
||
outc.resize((size_t)e->size);
|
||
if (e->size == 0) return true;
|
||
int32 got = 0;
|
||
return stream.read(outc.data(), (int32)e->size, &got) == Steinberg::kResultOk &&
|
||
got == e->size;
|
||
};
|
||
if (!readChunk(Steinberg::Vst::kComponentState, comp) ||
|
||
!readChunk(Steinberg::Vst::kControllerState, ctrl))
|
||
return false;
|
||
} else {
|
||
// Bridge state blob.
|
||
auto read32 = [&bytes](size_t off) -> uint32_t {
|
||
return ((uint32_t)bytes[off] << 24) | ((uint32_t)bytes[off + 1] << 16) |
|
||
((uint32_t)bytes[off + 2] << 8) | (uint32_t)bytes[off + 3];
|
||
};
|
||
if (bytes.size() < 8) return false;
|
||
size_t off = 0;
|
||
uint32_t clen = read32(off); off += 4;
|
||
if (off + clen > bytes.size()) return false;
|
||
comp.assign(bytes.begin() + off, bytes.begin() + off + clen);
|
||
off += clen;
|
||
if (off + 4 > bytes.size()) return false;
|
||
uint32_t klen = read32(off); off += 4;
|
||
if (off + klen > bytes.size()) return false;
|
||
ctrl.assign(bytes.begin() + off, bytes.begin() + off + klen);
|
||
}
|
||
auto* s = static_cast<Vst3FxState*>(state_);
|
||
if (!s) return false;
|
||
if (!comp.empty()) {
|
||
Steinberg::MemoryStream stream;
|
||
stream.write(comp.data(), (int32)comp.size(), nullptr);
|
||
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
|
||
if (!s->component || s->component->setState(&stream) != kResultOk) return false;
|
||
}
|
||
if (!ctrl.empty()) {
|
||
Steinberg::MemoryStream stream;
|
||
stream.write(ctrl.data(), (int32)ctrl.size(), nullptr);
|
||
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
|
||
if (!s->controller || s->controller->setState(&stream) != kResultOk) return false;
|
||
}
|
||
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
|
||
}
|
||
// Live param change (realtime chain, NO restart): address by lowercase
|
||
// title/shortTitle or numeric ParamID; value normalized 0..1. Delivered
|
||
// via setParamNormalized (controller/UI state) + inputParameterChanges
|
||
// queue (split plugins read the queue in process() — same rule as
|
||
// Vst3Instrument). ponytail: beginEdit/endEdit automation gestures when a
|
||
// plugin ignores setParamNormalized (fallback: restart with new preset).
|
||
bool setParam(const std::string& key, double value) {
|
||
#ifndef HAVE_VST3SDK
|
||
(void)key; (void)value;
|
||
return false;
|
||
#else
|
||
auto* s = static_cast<Vst3FxState*>(state_);
|
||
if (!s || !s->controller) return false;
|
||
Steinberg::Vst::ParamID tag = 0;
|
||
bool found = false;
|
||
try { // numeric ParamID passes through
|
||
size_t consumed = 0;
|
||
unsigned long num = std::stoul(key, &consumed);
|
||
if (consumed == key.size()) { tag = (Steinberg::Vst::ParamID)num; found = true; }
|
||
} catch (...) {}
|
||
if (!found) {
|
||
std::string k = key;
|
||
std::transform(k.begin(), k.end(), k.begin(),
|
||
[](unsigned char c) { return (char)std::tolower(c); });
|
||
auto it = s->paramByName.find(k);
|
||
if (it == s->paramByName.end()) {
|
||
std::cerr << "[RealtimeFx] vst3 setParam unknown key: " << key << std::endl;
|
||
return false;
|
||
}
|
||
tag = it->second;
|
||
}
|
||
Steinberg::Vst::ParamValue v =
|
||
(Steinberg::Vst::ParamValue)std::max(0.0, std::min(1.0, value));
|
||
{
|
||
std::lock_guard<std::mutex> lk(s->paramMutex);
|
||
s->controller->setParamNormalized(tag, v);
|
||
int32 idx = 0;
|
||
if (Steinberg::Vst::IParamValueQueue* q =
|
||
s->paramChanges.addParameterData(tag, idx))
|
||
q->addPoint(0, v, idx);
|
||
}
|
||
std::cout << "[RealtimeFx] vst3 setParam '" << key << "' -> tag " << tag
|
||
<< " = " << v << 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 };
|
||
// Size to the window's own style: borderless (WS_POPUP) needs no
|
||
// caption/border adjustment.
|
||
AdjustWindowRect(&r, GetWindowLongPtrA(hwnd, GWL_STYLE), 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
|
||
}
|
||
// Resize the editor view to w x h (VST3 host resize flow: checkSizeConstraint
|
||
// lets the plugin clamp to a supported size, host resizes the window, then
|
||
// onSize tells the plugin -- it moves/resizes its own child window). Runs on
|
||
// the GUI pump thread (WM_FXGUI_RESIZE) -- plugin calls must not come from
|
||
// the HTTP worker thread.
|
||
bool resizeEditor(void* parentWindowHandle, int w, int h) {
|
||
#ifndef HAVE_VST3SDK
|
||
(void)parentWindowHandle; (void)w; (void)h;
|
||
return false;
|
||
#else
|
||
auto* s = static_cast<Vst3FxState*>(state_);
|
||
if (!s || !s->view || w <= 0 || h <= 0) return false;
|
||
Steinberg::ViewRect vr = {0, 0, w, h};
|
||
s->view->checkSizeConstraint(&vr); // plugin clamps to a supported size
|
||
const int cw = vr.right - vr.left;
|
||
const int ch = vr.bottom - vr.top;
|
||
if (cw <= 0 || ch <= 0) return false;
|
||
#ifdef _WIN32
|
||
RECT r = {0, 0, cw, ch};
|
||
AdjustWindowRectEx(&r,
|
||
(DWORD)GetWindowLongPtrA((HWND)parentWindowHandle, GWL_STYLE),
|
||
FALSE,
|
||
(DWORD)GetWindowLongPtrA((HWND)parentWindowHandle, GWL_EXSTYLE));
|
||
SetWindowPos((HWND)parentWindowHandle, nullptr, 0, 0,
|
||
r.right - r.left, r.bottom - r.top,
|
||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||
#endif
|
||
s->view->onSize(&vr);
|
||
return true;
|
||
#endif
|
||
}
|
||
bool loaded() const { return state_ != nullptr; }
|
||
// Plugin-reported latency samples (PDC) — 0 nếu plugin không báo.
|
||
int32 latencySamples() const {
|
||
#ifndef HAVE_VST3SDK
|
||
return 0;
|
||
#else
|
||
return state_ ? static_cast<Vst3FxState*>(state_)->latencySamples : 0;
|
||
#endif
|
||
}
|
||
|
||
private:
|
||
void* state_ = nullptr;
|
||
};
|
||
|
||
// One FX slot from the job. builtin id: "gain" | "normalize" (legacy) hoặc 8
|
||
// DSP (PLAN_DAW_A Phase 2) — id ∈ eq|eqpro|imager|maximizer|compressor|limiter
|
||
// |exciter|rebalance → bfx (BuiltinFxChain).
|
||
struct FxSlot {
|
||
bool vst = false; // true = VST3, false = builtin
|
||
std::string path; // vst only
|
||
std::string presetB64; // vst only
|
||
bool bypass = false;
|
||
std::string builtinId; // builtin only
|
||
double db = 0.0; // gain param
|
||
double peak = 0.95; // normalize param
|
||
std::unique_ptr<Vst3Fx> fx;
|
||
std::unique_ptr<BuiltinFx> bfx; // 8 DSP builtin (Phase 2)
|
||
};
|
||
|
||
} // namespace
|
||
|
||
// SEH-guarded single-slot process call. Kept OUT of run_render_fx_job's frame
|
||
// (C2712: __try cannot live in a function whose C++ objects need unwinding).
|
||
// This frame holds only references/raw pointers — nothing to unwind.
|
||
static bool runFxSlotSafe(const FxSlot& sl, const float* inL, const float* inR,
|
||
float* outL, float* outR, uint32_t n, uint32_t* crashCode) {
|
||
#ifdef _WIN32
|
||
__try {
|
||
sl.fx->processAudio(inL, inR, outL, outR, n);
|
||
return true;
|
||
} __except (*crashCode = (uint32_t)GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER) {
|
||
return false;
|
||
}
|
||
#else
|
||
sl.fx->processAudio(inL, inR, outL, outR, n);
|
||
return true;
|
||
#endif
|
||
}
|
||
|
||
// SEH-guarded builtin DSP slot (in-place). Frame chỉ raw pointers.
|
||
static bool runBuiltinSlotSafe(BuiltinFx* fx, float* L, float* R, uint32_t n,
|
||
uint32_t* crashCode) {
|
||
#ifdef _WIN32
|
||
__try {
|
||
fx->process(L, R, n);
|
||
return true;
|
||
} __except (*crashCode = (uint32_t)GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER) {
|
||
return false;
|
||
}
|
||
#else
|
||
fx->process(L, R, n);
|
||
return true;
|
||
#endif
|
||
}
|
||
|
||
int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
|
||
const std::string& outPath) {
|
||
#ifdef _WIN32
|
||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
|
||
#endif
|
||
|
||
int rc = 1;
|
||
std::ofstream wav;
|
||
auto fail = [&](const std::string& msg) {
|
||
std::cerr << "[RenderFx] " << msg << std::endl;
|
||
if (wav.is_open()) wav.close();
|
||
std::remove(outPath.c_str());
|
||
};
|
||
|
||
// 1. Read input WAV.
|
||
WavIn in;
|
||
if (!readWav(inPath, in)) { fail("cannot read input WAV: " + inPath); return rc; }
|
||
if (in.L.empty()) { fail("input WAV is empty"); return rc; }
|
||
|
||
// 2. Parse job.
|
||
std::ifstream jf(jobPath, std::ios::binary);
|
||
if (!jf) { fail("cannot read job file: " + jobPath); return rc; }
|
||
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) { fail("job JSON parse error (code " + std::to_string(pres.error) + ")"); return rc; }
|
||
struct RootHolder { json_value_s* p = nullptr; ~RootHolder() { if (p) std::free(p); } } rootH;
|
||
rootH.p = root;
|
||
if (root->type != json_type_object) { fail("job root must be a JSON object"); return rc; }
|
||
const json_object_s* job = static_cast<const json_object_s*>(root->payload);
|
||
|
||
double srD = (double)in.sampleRate;
|
||
memberNumber(job, "sample_rate", srD);
|
||
if (srD < 8000.0 || srD > 192000.0) { fail("bad sample_rate: " + std::to_string(srD)); return rc; }
|
||
const uint32_t sampleRate = (uint32_t)srD;
|
||
if (in.sampleRate != sampleRate) {
|
||
fail("input WAV sr (" + std::to_string(in.sampleRate) + ") != job sample_rate ("
|
||
+ std::to_string(sampleRate) + ")"); return rc;
|
||
}
|
||
int64_t block = memberInt(job, "block_size", 512);
|
||
block = std::max<int64_t>(32, std::min<int64_t>(block, 4096));
|
||
const uint32_t chunk = (uint32_t)block;
|
||
|
||
// 3. Build slot list.
|
||
std::vector<FxSlot> slots;
|
||
const json_value_s* chainV = memberValue(job, "fx_chain");
|
||
if (chainV && chainV->type == json_type_array) {
|
||
const json_array_s* chain = static_cast<const json_array_s*>(chainV->payload);
|
||
for (const json_array_element_s* el = chain->start; el; el = el->next) {
|
||
const json_value_s* v = el->value;
|
||
if (!v || v->type != json_type_object) continue;
|
||
const json_object_s* o = static_cast<const json_object_s*>(v->payload);
|
||
const std::string type = memberString(o, "type", "");
|
||
FxSlot sl;
|
||
sl.bypass = memberBool(o, "bypass", false);
|
||
if (type == "vst3" || type == "vst") {
|
||
sl.vst = true;
|
||
sl.path = memberString(o, "path", "");
|
||
sl.presetB64 = memberString(o, "preset_b64", "");
|
||
} else if (type == "builtin") {
|
||
sl.vst = false;
|
||
sl.builtinId = memberString(o, "id", "");
|
||
const json_object_s* po = nullptr;
|
||
const json_value_s* pv = memberValue(o, "params");
|
||
if (pv && pv->type == json_type_object)
|
||
po = static_cast<const json_object_s*>(pv->payload);
|
||
double d = sl.db; if (po) memberNumber(po, "db", d); sl.db = d;
|
||
double pk = sl.peak; if (po) memberNumber(po, "peak", pk); sl.peak = pk;
|
||
if (sl.builtinId != "gain" && sl.builtinId != "normalize") {
|
||
// 8 DSP (PLAN_DAW_A Phase 2) — khớp Python _apply_builtin_fx_chain.
|
||
sl.bfx = createBuiltinFx(sl.builtinId, po, srD);
|
||
if (!sl.bfx) {
|
||
fail("unknown builtin id: " + sl.builtinId); return rc;
|
||
}
|
||
}
|
||
} else {
|
||
fail("unknown fx_chain slot type: " + type); return rc;
|
||
}
|
||
slots.push_back(std::move(sl));
|
||
}
|
||
}
|
||
if (slots.empty()) { fail("fx_chain is empty — nothing to render"); return rc; }
|
||
|
||
// 4. Load VST3 slots (bypassed slots stay unloaded → passthrough).
|
||
for (auto& sl : slots) {
|
||
if (!sl.vst || sl.bypass) continue;
|
||
if (sl.path.empty()) { fail("vst3 slot missing path"); return rc; }
|
||
auto fx = std::make_unique<Vst3Fx>();
|
||
if (!fx->load(sl.path, sampleRate, (int32)chunk)) { rc = 2; fail("VST3 FX load failed: " + sl.path); return rc; }
|
||
if (!sl.presetB64.empty() && !fx->applyPreset(sl.presetB64)) {
|
||
fail("preset import FAILED for " + sl.path); return rc;
|
||
}
|
||
sl.fx = std::move(fx);
|
||
}
|
||
|
||
// 4b. Per-slot latency (PDC, Phase 3): vst3 = getLatencySamples() (0 nếu
|
||
// chưa load/bypass), builtin = 0. 1 dòng stdout để native_render parse →
|
||
// render_engine align track (cùng thứ tự mảng slots).
|
||
std::cout << "[RenderFx] FX_LATENCIES";
|
||
for (const auto& sl : slots) {
|
||
uint32_t lat = 0;
|
||
if (sl.vst && !sl.bypass && sl.fx) lat = (uint32_t)sl.fx->latencySamples();
|
||
std::cout << ' ' << lat;
|
||
}
|
||
std::cout << std::endl;
|
||
|
||
// 5. Process chain block-wise. Double-buffered: cur ← slot → nxt, swap.
|
||
std::vector<float> L0(chunk), R0(chunk), L1(chunk), R1(chunk);
|
||
std::vector<float> curL(chunk), curR(chunk);
|
||
bool normalizePending = false;
|
||
double normalizePeak = 0.95;
|
||
uint32_t crashCode = 0;
|
||
for (const auto& sl : slots) {
|
||
if (sl.bypass) continue;
|
||
if (sl.vst && !sl.fx) continue; // defensive: bypassed/unloaded
|
||
if (!sl.vst && sl.builtinId == "normalize") {
|
||
normalizePending = true;
|
||
normalizePeak = std::max(0.01, std::min(1.0, sl.peak));
|
||
continue; // global post-pass
|
||
}
|
||
const bool isVst = sl.vst;
|
||
const double gainLin = sl.vst ? 1.0 : std::pow(10.0, sl.db / 20.0);
|
||
for (uint64_t pos = 0; pos < in.L.size(); pos += chunk) {
|
||
const uint32_t n = (uint32_t)std::min<uint64_t>(chunk, in.L.size() - pos);
|
||
std::memcpy(curL.data(), in.L.data() + pos, n * sizeof(float));
|
||
std::memcpy(curR.data(), in.R.data() + pos, n * sizeof(float));
|
||
if (isVst) {
|
||
bool ok = runFxSlotSafe(sl, curL.data(), curR.data(), L0.data(), R0.data(),
|
||
n, &crashCode);
|
||
if (!ok) { rc = 3; fail("VST3 FX crashed inside processAudio (SEH code=0x"
|
||
+ std::to_string(crashCode) + ")"); return rc; }
|
||
std::memcpy(in.L.data() + pos, L0.data(), n * sizeof(float));
|
||
std::memcpy(in.R.data() + pos, R0.data(), n * sizeof(float));
|
||
} else if (sl.bfx) {
|
||
// Builtin DSP (8 loại) — in-place, xen kẽ VST3 đúng thứ tự UI.
|
||
bool ok = runBuiltinSlotSafe(sl.bfx.get(), in.L.data() + pos,
|
||
in.R.data() + pos, n, &crashCode);
|
||
if (!ok) {
|
||
rc = 3; fail("builtin DSP crashed (SEH code=0x"
|
||
+ std::to_string(crashCode) + ")"); return rc;
|
||
}
|
||
} else {
|
||
for (uint32_t i = 0; i < n; ++i) {
|
||
in.L[pos + i] = curL[i] * (float)gainLin;
|
||
in.R[pos + i] = curR[i] * (float)gainLin;
|
||
}
|
||
}
|
||
}
|
||
if (isVst) std::cout << "[RenderFx] slot processed: " << sl.path << std::endl;
|
||
else std::cout << "[RenderFx] slot processed: builtin " << sl.builtinId << std::endl;
|
||
}
|
||
|
||
// 6. Global normalize pass (only reduces when peak exceeds target — mirrors
|
||
// render_project's clip guard; the final mastering slot by convention).
|
||
if (normalizePending) {
|
||
float peak = 0.f;
|
||
for (float v : in.L) peak = std::max(peak, std::fabs(v));
|
||
for (float v : in.R) peak = std::max(peak, std::fabs(v));
|
||
if (peak > (float)normalizePeak && peak > 1e-9f) {
|
||
const float k = (float)normalizePeak / peak;
|
||
for (float& v : in.L) v *= k;
|
||
for (float& v : in.R) v *= k;
|
||
std::cout << "[RenderFx] normalize: peak " << peak << " → " << normalizePeak << std::endl;
|
||
} else {
|
||
std::cout << "[RenderFx] normalize: peak " << peak << " ≤ " << normalizePeak << " (no-op)" << std::endl;
|
||
}
|
||
}
|
||
|
||
// 7. Write output WAV.
|
||
wav.open(outPath, std::ios::binary);
|
||
if (!wav) { fail("cannot open output: " + outPath); return rc; }
|
||
writeWavHeader(wav, sampleRate);
|
||
uint64_t dataBytes = 0;
|
||
for (uint64_t pos = 0; pos < in.L.size(); pos += chunk) {
|
||
const uint32_t n = (uint32_t)std::min<uint64_t>(chunk, in.L.size() - pos);
|
||
writeFrames(wav, in.L.data() + pos, in.R.data() + pos, n);
|
||
dataBytes += (uint64_t)n * 8; // stereo float32: 2ch * 4B
|
||
}
|
||
finishWav(wav, dataBytes);
|
||
wav.close();
|
||
std::cout << "[RenderFx] wrote " << outPath << " (" << in.L.size() << " frames)" << std::endl;
|
||
rc = 0;
|
||
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** vst, BuiltinFx** bfx, 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 (bypass && bypass[i]) continue;
|
||
if (vst[i]) vst[i]->processAudio(inL, inR, inL, inR, n); // in-place safe
|
||
else if (bfx[i]) bfx[i]->process(inL, inR, n); // builtin in-place
|
||
}
|
||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||
std::cerr << "[RealtimeFx] chain crashed in process — slot bypassed" << std::endl;
|
||
}
|
||
#else
|
||
(void)vst; (void)bfx; (void)bypass; (void)count; (void)inL; (void)inR; (void)n;
|
||
#endif
|
||
}
|
||
|
||
} // namespace
|
||
|
||
struct RealtimeFxChain::Impl {
|
||
// VST3 (fx) hoặc builtin DSP (bfx) — 1 trong 2; xen kẽ đúng thứ tự UI.
|
||
struct Entry {
|
||
std::shared_ptr<Vst3Fx> fx;
|
||
std::unique_ptr<BuiltinFx> bfx;
|
||
bool bypass = false;
|
||
};
|
||
struct Chain { std::vector<Entry> entries; };
|
||
|
||
// SET_PARAM chờ chain swap (setParam tới trước setChain): worker áp sau
|
||
// khi swap — không mất param race lúc khởi động.
|
||
struct PendingParam { int slot; std::string key; double value; };
|
||
std::mutex pmutex_; // guards pending_
|
||
std::vector<PendingParam> pending_;
|
||
std::atomic<int> paramQueued_{0}; // wake worker: param chờ apply
|
||
|
||
std::mutex mutex_; // guards chain_ / retired_
|
||
std::shared_ptr<Chain> chain_;
|
||
std::shared_ptr<Chain> retired_; // dtor deferred to the worker thread
|
||
std::atomic<uint64_t> gen_{0}; // chain generation (Phase 2.8)
|
||
|
||
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;
|
||
|
||
// VST3 instance cache: giữ plugin đã load qua các lần buildChain để toggle
|
||
// bypass không phải load lại từ disk (tránh delay dry->wet). Key = path + #occ.
|
||
std::map<std::string, std::shared_ptr<Vst3Fx>> cache_;
|
||
std::map<std::string, std::string> lastPreset_; // preset b64 da ap (tranh ap lai)
|
||
std::map<std::string, int> occ_; // so lan xuat hien cua path trong chain
|
||
|
||
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* vst[kMaxSlots];
|
||
BuiltinFx* bfx[kMaxSlots];
|
||
bool bypass[kMaxSlots];
|
||
uint32_t count = 0;
|
||
for (const auto& e : c->entries) {
|
||
if (count >= kMaxSlots) break;
|
||
vst[count] = e.fx.get();
|
||
bfx[count] = e.bfx.get();
|
||
bypass[count] = e.bypass;
|
||
++count;
|
||
}
|
||
realtimeRunChain(vst, bfx, bypass, count, inL, inR, n);
|
||
}
|
||
|
||
void RealtimeFxChain::setParam(int slot, const std::string& key, double value) {
|
||
// SET_PARAM (SHM control ring, Phase 2.8): KHÔNG gọi VST API trên audio
|
||
// thread (chain.setParam chạy từ RealtimeFxLoop drain ctrl ring) —
|
||
// controller->setParamNormalized là COM call cross-thread với GUI pump,
|
||
// block ms→chục ms → out ring cạn → worklet underrun → crackle. Mọi
|
||
// param defer vào pending_, worker thread áp (builtin nhanh, VST3 an
|
||
// toàn) — audio thread chỉ push queue + notify.
|
||
if (!impl_) return;
|
||
{
|
||
std::lock_guard<std::mutex> lk(impl_->pmutex_);
|
||
impl_->pending_.push_back({slot, key, value});
|
||
}
|
||
impl_->paramQueued_.fetch_add(1, std::memory_order_release);
|
||
impl_->qcv_.notify_all();
|
||
}
|
||
|
||
uint64_t RealtimeFxChain::chainGen() {
|
||
return impl_ ? impl_->gen_.load(std::memory_order_acquire) : 0;
|
||
}
|
||
|
||
std::vector<uint32_t> RealtimeFxChain::entryLatencies() {
|
||
std::vector<uint32_t> out;
|
||
if (!impl_) return out;
|
||
std::shared_ptr<Impl::Chain> c;
|
||
{ std::lock_guard<std::mutex> lk(impl_->mutex_); c = impl_->chain_; }
|
||
if (!c) return out;
|
||
out.reserve(c->entries.size());
|
||
for (const auto& e : c->entries)
|
||
out.push_back((!e.bypass && e.fx) ? (uint32_t)e.fx->latencySamples() : 0u);
|
||
return out;
|
||
}
|
||
|
||
std::shared_ptr<RealtimeFxChain::Impl::Chain> RealtimeFxChain::Impl::buildChain(const std::string& json) {
|
||
auto chain = std::make_shared<Chain>();
|
||
occ_.clear(); // key theo vị trí slot trong CHAIN NÀY — reset mỗi build
|
||
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 type = memberString(o, "type", "");
|
||
if (type == "builtin") {
|
||
// Builtin DSP (Phase 2): id ∈ 8 loại — xen kẽ VST3 đúng thứ tự.
|
||
const std::string id = memberString(o, "id", "");
|
||
if (id.empty()) continue;
|
||
const json_object_s* po = nullptr;
|
||
const json_value_s* pv = memberValue(o, "params");
|
||
if (pv && pv->type == json_type_object)
|
||
po = static_cast<const json_object_s*>(pv->payload);
|
||
Entry ent;
|
||
ent.bypass = memberBool(o, "bypass", false);
|
||
ent.bfx = createBuiltinFx(id, po, sampleRate_);
|
||
if (!ent.bfx) {
|
||
std::cerr << "[RealtimeFx] unknown builtin id — slot skipped: " << id << std::endl;
|
||
continue;
|
||
}
|
||
chain->entries.push_back(std::move(ent));
|
||
continue;
|
||
}
|
||
const std::string path = memberString(o, "path", "");
|
||
if (path.empty()) continue;
|
||
Entry ent;
|
||
ent.bypass = memberBool(o, "bypass", false);
|
||
const std::string preset = memberString(o, "preset_b64", "");
|
||
// Key ổn định theo vị trí slot: đếm MỌI lần path xuất hiện (kể cả
|
||
// bypassed) — toggle bypass không đổi key → không reload nhầm instance.
|
||
const std::string ckey = path + "#" + std::to_string(occ_[path]++);
|
||
if (!ent.bypass) {
|
||
auto it = cache_.find(ckey);
|
||
if (it != cache_.end()) {
|
||
ent.fx = it->second; // đã load: reuse ngay, khong reload
|
||
const std::string last = lastPreset_[ckey];
|
||
if (!preset.empty() && preset != last) {
|
||
if (ent.fx->applyPreset(preset))
|
||
std::cerr << "[RealtimeFx] preset applied (cached): " << path << std::endl;
|
||
else
|
||
std::cerr << "[RealtimeFx] preset apply FAILED (default kept): " << path << std::endl;
|
||
lastPreset_[ckey] = preset;
|
||
}
|
||
if (preset.empty()) lastPreset_[ckey] = "";
|
||
}
|
||
else {
|
||
auto fx = std::make_shared<Vst3Fx>();
|
||
if (fx->load(path, sampleRate_, blockSize_)) {
|
||
if (!preset.empty()) {
|
||
if (fx->applyPreset(preset))
|
||
std::cerr << "[RealtimeFx] preset applied: " << path << std::endl;
|
||
else
|
||
std::cerr << "[RealtimeFx] preset apply FAILED (default kept): " << path << std::endl;
|
||
}
|
||
ent.fx = fx;
|
||
cache_[ckey] = fx;
|
||
lastPreset_[ckey] = preset;
|
||
}
|
||
else {
|
||
std::cerr << "[RealtimeFx] load failed — slot bypassed: " << path << std::endl;
|
||
ent.bypass = true;
|
||
occ_[path]--; // khong dem lan that bai
|
||
}
|
||
}
|
||
}
|
||
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() || paramQueued_.load(std::memory_order_acquire) > 0;
|
||
});
|
||
if (quit_) break;
|
||
if (!q_.empty()) {
|
||
job = std::move(q_.front());
|
||
q_.erase(q_.begin());
|
||
}
|
||
}
|
||
if (!job.empty()) {
|
||
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);
|
||
}
|
||
gen_.fetch_add(1, std::memory_order_release); // loop báo lại latency
|
||
}
|
||
{
|
||
// Áp param chờ trên WORKER thread (audio thread không bao giờ gọi
|
||
// VST API): queue trước swap áp vào chain mới; queue trong lúc
|
||
// chain tồn tại áp ngay, không cần đợi setChain.
|
||
std::vector<Impl::PendingParam> toApply;
|
||
{
|
||
std::lock_guard<std::mutex> lk(pmutex_);
|
||
if (!pending_.empty()) {
|
||
toApply.swap(pending_);
|
||
paramQueued_.store(0, std::memory_order_release);
|
||
}
|
||
}
|
||
if (!toApply.empty()) {
|
||
std::shared_ptr<Chain> c;
|
||
{ std::lock_guard<std::mutex> lk(mutex_); c = chain_; }
|
||
if (c) {
|
||
for (const auto& p : toApply) {
|
||
if (p.slot < 0 || (size_t)p.slot >= c->entries.size()) continue;
|
||
auto& e = c->entries[(size_t)p.slot];
|
||
if (e.bfx) e.bfx->setParam(p.key, p.value);
|
||
else if (e.fx) e.fx->setParam(p.key, p.value);
|
||
}
|
||
} else {
|
||
// Chain chưa tồn tại — stash lại, áp sau swap.
|
||
std::lock_guard<std::mutex> lk(pmutex_);
|
||
pending_.insert(pending_.begin(), toApply.begin(), toApply.end());
|
||
paramQueued_.fetch_add((int)toApply.size(), std::memory_order_release);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#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
|
||
// 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 {
|
||
|
||
const char* kFxGuiWndClass = "SonicForge_FX_GUI_Class";
|
||
|
||
struct FxGuiCtx {
|
||
bool closing = false;
|
||
};
|
||
// Parent watchdog: engine spawn fx-gui voi SF_PARENT_PID=<engine pid>. Neu
|
||
// engine chet (crash/quit) ma bridge fx-gui khong tu thoat -> cua so topmost
|
||
// van lo lung, khong tat duoc. Watchdog thread: parent mat -> WM_CLOSE.
|
||
static uint32_t fxGuiParentPid() {
|
||
const char* e = std::getenv("SF_PARENT_PID");
|
||
return e ? (uint32_t)std::atoi(e) : 0;
|
||
}
|
||
static bool fxGuiParentAlive(uint32_t pid) {
|
||
if (pid == 0) return true;
|
||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||
if (!h) return false;
|
||
CloseHandle(h);
|
||
return true;
|
||
}
|
||
static void fxGuiStartWatchdog(HWND hwnd, uint32_t parentPid) {
|
||
if (parentPid == 0) return;
|
||
std::thread([hwnd, parentPid]() {
|
||
while (IsWindow(hwnd)) {
|
||
Sleep(2000);
|
||
if (!fxGuiParentAlive(parentPid)) {
|
||
PostMessageA(hwnd, WM_CLOSE, 0, 0); // pump exits -> process returns
|
||
return;
|
||
}
|
||
}
|
||
}).detach();
|
||
}
|
||
|
||
// Worker (HTTP) thread -> pump thread editor-resize request. The plugin's
|
||
// 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) {
|
||
case WM_CLOSE:
|
||
DestroyWindow(hwnd);
|
||
return 0;
|
||
case WM_DESTROY:
|
||
if (ctx) ctx->closing = true;
|
||
return 0;
|
||
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);
|
||
}
|
||
}
|
||
|
||
} // 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
|
||
}
|
||
static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h) {
|
||
#ifdef HAVE_VST3SDK
|
||
__try { static_cast<Vst3Fx*>(fx)->resizeEditor(hwnd, w, h); }
|
||
__except (EXCEPTION_EXECUTE_HANDLER) {}
|
||
#else
|
||
(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();
|
||
}
|
||
}
|
||
|
||
|
||
// --- 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
|
||
auto nextTick = std::chrono::steady_clock::now();
|
||
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;
|
||
lastWrite = 0; // session (re)start -> re-anchor
|
||
}
|
||
if (!v && !name.empty()) { // retry open each pass: mapping may appear late
|
||
v = fxGuiOpenShm(name, sizeof(FxRealtimeIPC));
|
||
ipc = v ? static_cast<FxRealtimeIPC*>(v->view) : nullptr;
|
||
}
|
||
if (ipc && ipc->h.magic == FXRT_MAGIC &&
|
||
ipc->h.inWrite > lastWrite) {
|
||
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;
|
||
}
|
||
}
|
||
// Pace the feeder to realtime (one FXRT_BLOCK per its audio duration,
|
||
// 44.1k fallback) instead of Sleep(2) busy-spin (~3x realtime CPU).
|
||
// Less CPU in the GUI process = less contention with the pump thread,
|
||
// which was stalling /input (dead clicks) and /preset (crackle reloads).
|
||
const uint32_t sr = (ipc && ipc->h.sampleRate > 0) ? ipc->h.sampleRate : 44100u;
|
||
nextTick += std::chrono::microseconds(1000000ULL * FXRT_BLOCK / sr);
|
||
const auto now = std::chrono::steady_clock::now();
|
||
if (now > nextTick + std::chrono::milliseconds(50))
|
||
nextTick = now; // fell behind (plugin hiccup): resync, don't burst
|
||
std::this_thread::sleep_until(nextTick);
|
||
}
|
||
if (v) fxGuiCloseShm(v);
|
||
}
|
||
|
||
// Split feeder setup: fxGuiShmSetName runs EARLY (before the server loop) so
|
||
// /shm pushes from the frontend are not lost, while the detached thread starts
|
||
// only AFTER load+attach succeeded — a feeder calling processAudio() mid-load
|
||
// can crash/corrupt a heavy plugin and break the editor attach.
|
||
// ponytail: feeder feeds the raw live pre-chain mix; add a separate audio path
|
||
// if GUI processing of the full insert chain is wanted later.
|
||
static void fxGuiShmSetName(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;
|
||
}
|
||
static void fxGuiStartFeederThread(Vst3Fx* fx, HWND hwnd) {
|
||
auto p = fxGuiShmCtlRef();
|
||
if (!p) { p = std::make_shared<FxGuiShmCtl>(); fxGuiShmCtlRef() = p; }
|
||
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, std::string& shm, std::string& presetB64) {
|
||
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);
|
||
path = memberString(job, "path", "");
|
||
name = memberString(job, "name", "");
|
||
shm = memberString(job, "shm", "");
|
||
presetB64 = memberString(job, "preset_b64", "");
|
||
if (path.empty()) { std::cerr << "[FxGui] job missing path" << std::endl; return 1; }
|
||
return 0;
|
||
}
|
||
|
||
// 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);
|
||
wc.lpszClassName = kFxGuiWndClass;
|
||
RegisterClassA(&wc);
|
||
const int x = offscreen ? -20000 : CW_USEDEFAULT;
|
||
const int y = offscreen ? -20000 : CW_USEDEFAULT;
|
||
const DWORD exStyle = offscreen ? (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) : 0;
|
||
// Borderless (WS_POPUP): GetWindowRect == client rect, so anchored
|
||
// placement over the embed panel is exact. A captioned/bordered window
|
||
// (WS_OVERLAPPEDWINDOW) put the client area ~31px lower/right of the
|
||
// requested rect -> cursor jumped on hover and clicks landed on the webview.
|
||
const DWORD style = (offscreen ? WS_POPUP : WS_OVERLAPPEDWINDOW)
|
||
| WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
|
||
HWND hwnd = CreateWindowExA(exStyle, kFxGuiWndClass,
|
||
(name.empty() ? "VST FX" : name).c_str(),
|
||
style,
|
||
x, y, 800, 600,
|
||
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;
|
||
}
|
||
|
||
// 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, const std::string& presetB64) {
|
||
// 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; }
|
||
// Khôi phục preset GUI capture cuối (hide/show mất settings bug): áp
|
||
// preset NGAY SAU load — trước attach editor để GUI hiện đúng cài đặt.
|
||
if (!presetB64.empty() && !fx->applyPreset(presetB64))
|
||
std::cerr << "[FxGui] preset apply FAILED (default kept): " << path << std::endl;
|
||
else if (!presetB64.empty())
|
||
std::cerr << "[FxGui] preset applied: " << path << std::endl;
|
||
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;
|
||
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& shmOut) {
|
||
std::string path, name, presetB64;
|
||
int rc = fxGuiParseJob(jobPath, path, name, shmOut, presetB64);
|
||
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, presetB64);
|
||
}
|
||
|
||
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;
|
||
|
||
Vst3Fx fx;
|
||
HWND hwnd = nullptr;
|
||
FxGuiCtx ctx;
|
||
std::string shm;
|
||
const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/false, (LONG_PTR)&ctx, shm);
|
||
if (rc) return rc;
|
||
fxGuiStartWatchdog(hwnd, fxGuiParentPid());
|
||
fxGuiShmSetName(shm);
|
||
fxGuiStartFeederThread(&fx, hwnd);
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 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, const std::function<void()>& onStarted);
|
||
|
||
int run_fx_gui_server(const std::string& jobPath) {
|
||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
|
||
|
||
Vst3Fx fx;
|
||
HWND hwnd = nullptr;
|
||
// 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, shm, presetB64;
|
||
int rc = fxGuiParseJob(jobPath, path, name, shm, presetB64);
|
||
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
|
||
fxGuiShmSetName(shm); // EARLY — frontend may /shm push before load finishes
|
||
|
||
int loadRc = 0;
|
||
const int srv = fxGuiServerLoop(hwnd, [&]() {
|
||
loadRc = fxGuiLoadAttach(&fx, path, name, hwnd, presetB64);
|
||
if (loadRc == 0) fxGuiStartFeederThread(&fx, hwnd); // after load+attach only
|
||
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 loadRc ? loadRc : srv;
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
int run_fx_gui_server(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 {
|
||
|
||
std::string jsonEscape(const std::string& s) {
|
||
std::string o;
|
||
o.reserve(s.size() + 8);
|
||
for (char ch : s) {
|
||
switch (ch) {
|
||
case '"': o += "\\\""; break;
|
||
case '\\': o += "\\\\"; break;
|
||
case '\n': o += "\\n"; break;
|
||
case '\r': o += "\\r"; break;
|
||
case '\t': o += "\\t"; break;
|
||
default: o += ch;
|
||
}
|
||
}
|
||
return o;
|
||
}
|
||
|
||
#ifdef HAVE_VST3SDK
|
||
struct ScanEntry {
|
||
std::string path;
|
||
std::string name;
|
||
bool isFx = false;
|
||
bool isInstrument = false;
|
||
};
|
||
|
||
// C++ locals live here (no __try in this frame) — a crashing module loader
|
||
// is caught by safeScanModule's SEH and skipped, not fatal to the whole scan.
|
||
bool scanModuleInner(const std::string& path, std::vector<ScanEntry>& out) {
|
||
using namespace VST3::Hosting;
|
||
#ifdef _WIN32
|
||
setPluginSearchPath(path); // deps cạnh plugin (0x7E fix) — scan cũng load module
|
||
#endif
|
||
std::string err;
|
||
Module::Ptr module = Module::create(path, err);
|
||
if (!module) { std::cerr << "[Scan] skip (module load failed): " << path << " — " << err << std::endl; return false; }
|
||
const PluginFactory& factory = module->getFactory();
|
||
auto infos = factory.classInfos();
|
||
int added = 0;
|
||
for (const auto& ci : infos) {
|
||
if (ci.category() != kVstAudioEffectClass) continue;
|
||
const bool isInstr = ci.subCategoriesString().find("Instrument") != std::string::npos;
|
||
ScanEntry e;
|
||
e.path = path;
|
||
e.name = ci.name();
|
||
e.isFx = !isInstr;
|
||
e.isInstrument = isInstr;
|
||
out.push_back(std::move(e));
|
||
++added;
|
||
}
|
||
if (added == 0)
|
||
std::cerr << "[Scan] " << path << ": no audio classes" << std::endl;
|
||
return added > 0;
|
||
}
|
||
|
||
// Per-module scan context for the timeout worker thread.
|
||
struct ScanCtx {
|
||
const std::string* path;
|
||
std::vector<ScanEntry>* out;
|
||
bool ok = false;
|
||
};
|
||
static DWORD WINAPI scanWorker(LPVOID p) {
|
||
auto* ctx = static_cast<ScanCtx*>(p);
|
||
__try { ctx->ok = scanModuleInner(*ctx->path, *ctx->out); }
|
||
__except (EXCEPTION_EXECUTE_HANDLER) {
|
||
std::cerr << "[Scan] skip (crashed module): " << *ctx->path << std::endl;
|
||
ctx->ok = false;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
bool safeScanModule(const std::string& path, std::vector<ScanEntry>& out) {
|
||
#ifdef _WIN32
|
||
// Run the VST3 loader on a dedicated thread so a HANGING module (e.g.
|
||
// Ozone 11 Clarity.vst3 — Module::create blocks forever inside the SDK)
|
||
// cannot stall the whole --scan. SEH only catches crashes, not hangs, so
|
||
// a watchdog timeout + TerminateThread is required. Safe here: --scan is
|
||
// a one-shot process; the worker only touches the VST3 SDK loader and the
|
||
// process exits right after the scan completes.
|
||
ScanCtx ctx{&path, &out};
|
||
HANDLE hThread = CreateThread(nullptr, 0, scanWorker, &ctx, 0, nullptr);
|
||
if (!hThread) { std::cerr << "[Scan] skip (thread fail): " << path << std::endl; return false; }
|
||
const DWORD timeoutMs = 4000; // 4s/module — module hợp lệ load 1-3s (đo
|
||
// thực tế); module treo thì treo vĩnh viễn nên 4s đủ phân biệt. Trước đây
|
||
// 12s × ~15 module Ozone 11 treo = 180s+ vượt timeout Python → mất CẢ dir
|
||
// → MASTERING PANEL rỗng.
|
||
DWORD wait = WaitForSingleObject(hThread, timeoutMs);
|
||
if (wait == WAIT_TIMEOUT) {
|
||
TerminateThread(hThread, 0);
|
||
CloseHandle(hThread);
|
||
std::cerr << "[Scan] skip (timeout 12s): " << path << std::endl;
|
||
return false;
|
||
}
|
||
CloseHandle(hThread);
|
||
return ctx.ok;
|
||
#else
|
||
return scanModuleInner(path, out);
|
||
#endif
|
||
}
|
||
#endif // HAVE_VST3SDK
|
||
|
||
} // namespace
|
||
|
||
int run_scan_dir(const std::string& dirPath) {
|
||
#ifdef _WIN32
|
||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
|
||
#endif
|
||
std::vector<std::string> vst3Paths; // folders or files ending .vst3
|
||
std::vector<std::string> vst2Paths; // .dll / .so (cannot introspect)
|
||
|
||
namespace fs = std::filesystem;
|
||
auto isExt = [](const std::string& p, const char* ext) {
|
||
const size_t n = std::strlen(ext);
|
||
return p.size() >= n && p.compare(p.size() - n, n, ext) == 0;
|
||
};
|
||
try {
|
||
fs::recursive_directory_iterator it(dirPath), end;
|
||
for (; it != end; ++it) {
|
||
const std::string p = it->path().string();
|
||
std::string low = p;
|
||
for (auto& c : low) c = (char)std::tolower((unsigned char)c);
|
||
if (it->is_directory()) {
|
||
if (isExt(low, ".vst3")) {
|
||
vst3Paths.push_back(p);
|
||
it.disable_recursion_pending(); // bundle: don't walk Contents/
|
||
}
|
||
} else if (it->is_regular_file()) {
|
||
if (isExt(low, ".vst3")) vst3Paths.push_back(p);
|
||
else if (isExt(low, ".dll") || isExt(low, ".so")) vst2Paths.push_back(p);
|
||
}
|
||
}
|
||
} catch (...) { std::cerr << "[Scan] walk failed: " << dirPath << std::endl; }
|
||
|
||
std::cout << "{\"plugins\":[";
|
||
bool first = true;
|
||
auto emit = [&](const std::string& path, const std::string& name,
|
||
bool isFx, bool isInstr, const char* type) {
|
||
if (!first) std::cout << ",";
|
||
first = false;
|
||
std::cout << "{\"path\":\"" << jsonEscape(path)
|
||
<< "\",\"name\":\"" << jsonEscape(name)
|
||
<< "\",\"is_fx\":" << (isFx ? "true" : "false")
|
||
<< ",\"is_instrument\":" << (isInstr ? "true" : "false")
|
||
<< ",\"type\":\"" << type << "\"}";
|
||
};
|
||
#ifdef HAVE_VST3SDK
|
||
for (const auto& p : vst3Paths) {
|
||
std::vector<ScanEntry> entries;
|
||
if (!safeScanModule(p, entries)) {
|
||
// Module unreadable/crashed/hung (e.g. Ozone 11 Clarity): emit
|
||
// entry UNKNOWN (is_fx=false, is_instrument=false) — trước đây
|
||
// emit is_instrument=true làm VST FX lọt vào danh sách instrument
|
||
// (nút Synth). Backend loại entry unknown khỏi vst_instruments:
|
||
// không xác định được loại → không xếp bừa vào instrument.
|
||
emit(p, p.substr(p.find_last_of("/\\") + 1), false, false, "VST3");
|
||
continue;
|
||
}
|
||
for (const auto& e : entries) emit(e.path, e.name, e.isFx, e.isInstrument, "VST3");
|
||
}
|
||
#else
|
||
for (const auto& p : vst3Paths) {
|
||
std::string name = p.substr(p.find_last_of("/\\") + 1);
|
||
emit(p, name, false, true, "VST3");
|
||
}
|
||
#endif
|
||
for (const auto& p : vst2Paths) {
|
||
std::string name = p.substr(p.find_last_of("/\\") + 1);
|
||
emit(p, name, false, true, "VST2");
|
||
}
|
||
std::cout << "]}" << std::endl;
|
||
return 0;
|
||
}
|