// 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 --in --out ` // processes an existing WAV through a VST3 effect chain (audio-in/audio-out, // no MIDI) plus builtin slots (gain/normalize). Also `--scan ` 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 #endif #include "FxRealtimeIPC.h" #include "BuiltinFxChain.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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(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(&L[i]), 4); f.write(reinterpret_cast(&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 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 buf((std::istreambuf_iterator(f)), std::istreambuf_iterator()); 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(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& 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 component; IPtr controller; IPtr 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 view; // editor view while GUI open Steinberg::Vst::ParameterChanges paramChanges; // live setParam queue -> inputParameterChanges std::mutex paramMutex; // setParam (loop/worker) vs processAudio std::map 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\\ // 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 component = factory.createInstance(chosen.ID()); if (!component) { std::cerr << "[RenderFx] STEP createInstance FAILED" << std::endl; err = "createInstance failed"; return false; } std::cerr << "[RenderFx] STEP createInstance OK" << std::endl; IPtr hostApp = owned(new HostApplication()); FUnknownPtr 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 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(VST3::UID(cid)); if (controller) { FUnknownPtr 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 compCP(component); FUnknownPtr 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 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(state_); if (s->component) { FUnknownPtr 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(state_); if (!s || !s->component || n == 0) return; FUnknownPtr processor(s->component); if (!processor) return; s->processData.processMode = kRealtime; s->processData.numSamples = (int32)n; s->processData.inputEvents = nullptr; { std::lock_guard 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 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 bytes; if (!base64Decode(b64, bytes) || bytes.empty()) return false; std::vector 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& 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(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(state_); if (!s) return; std::vector 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 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(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 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(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 view(rawView); if (!view) { // Some plugins expose IPlugView on the component instead. IPlugView* rawViewC = nullptr; s->component->queryInterface(IPlugView::iid, (void**)&rawViewC); view = FUnknownPtr(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(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(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(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 fx; std::unique_ptr 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(jf)), std::istreambuf_iterator()); 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(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(32, std::min(block, 4096)); const uint32_t chunk = (uint32_t)block; // 3. Build slot list. std::vector slots; const json_value_s* chainV = memberValue(job, "fx_chain"); if (chainV && chainV->type == json_type_array) { const json_array_s* chain = static_cast(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(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(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(); 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 L0(chunk), R0(chunk), L1(chunk), R1(chunk); std::vector 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(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(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 fx; std::unique_ptr bfx; bool bypass = false; }; struct Chain { std::vector 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 pending_; std::atomic paramQueued_{0}; // wake worker: param chờ apply std::mutex mutex_; // guards chain_ / retired_ std::shared_ptr chain_; std::shared_ptr retired_; // dtor deferred to the worker thread std::atomic gen_{0}; // chain generation (Phase 2.8) std::mutex qmutex_; // guards q_ / quit_ std::condition_variable qcv_; std::vector 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> cache_; std::map lastPreset_; // preset b64 da ap (tranh ap lai) std::map occ_; // so lan xuat hien cua path trong chain std::shared_ptr buildChain(const std::string& json); void workerLoop(); }; RealtimeFxChain::RealtimeFxChain() : impl_(std::make_unique()) { impl_->worker_ = std::thread([this]() { impl_->workerLoop(); }); } RealtimeFxChain::~RealtimeFxChain() { shutdown(); } void RealtimeFxChain::shutdown() { if (!impl_) return; { std::lock_guard lk(impl_->qmutex_); if (impl_->quit_) return; impl_->quit_ = true; } impl_->qcv_.notify_all(); if (impl_->worker_.joinable()) impl_->worker_.join(); { std::lock_guard 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 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 c; { std::lock_guard 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 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 RealtimeFxChain::entryLatencies() { std::vector out; if (!impl_) return out; std::shared_ptr c; { std::lock_guard 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::buildChain(const std::string& json) { auto chain = std::make_shared(); 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(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(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(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(); 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 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 next = buildChain(job); // slow: plugin load { std::lock_guard 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 toApply; { std::lock_guard lk(pmutex_); if (!pending_.empty()) { toApply.swap(pending_); paramQueued_.store(0, std::memory_order_release); } } if (!toApply.empty()) { std::shared_ptr c; { std::lock_guard 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 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 : 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=. 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 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(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(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(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(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 lk(gCapMutex()); gCapDone() = false; gCapB64().clear(); } PostMessageA(hwnd, WM_FXGUI_CAPTURE, 0, 0); { std::unique_lock 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& fxGuiShmCtlRef() { static std::shared_ptr 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 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 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 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(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(); fxGuiShmCtlRef() = p; } std::lock_guard lk(p->m); p->name = initialShm; } static void fxGuiStartFeederThread(Vst3Fx* fx, HWND hwnd) { auto p = fxGuiShmCtlRef(); if (!p) { p = std::make_shared(); 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(jf)), std::istreambuf_iterator()); 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(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& 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& 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* out; bool ok = false; }; static DWORD WINAPI scanWorker(LPVOID p) { auto* ctx = static_cast(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& 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 vst3Paths; // folders or files ending .vst3 std::vector 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 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; }