// native_bridge/src/Vst2Instrument.cpp // VST2 (VST 2.4, 64-bit) host implementation. // // Clean-room: ABI facts come from the public VST2.4 specification; the // interface header (vestige/aeffect.h) is written from those facts, not // copied from Steinberg or LMMS source. No GUI editor (headless): the bridge // renders audio; preset state round-trips via effGetChunk/effSetChunk. #include "Vst2Instrument.h" #include "vestige/aeffect.h" #include #include #include #include #include namespace { // Registry so the plugin's audioMaster callback can find its host instance. std::mutex g_v2Mutex; std::unordered_map g_v2Registry; // Set only during VSTPluginMain() — before the plugin returns its AEffect* // the callback may be invoked with a null effect (some plugins query the // sample rate during construction). thread_local Vst2Instrument* t_loading = nullptr; // TEMP probe: log every audioMaster call while effEditOpen is running. thread_local bool t_editingProbe = false; // Resize hook registered by the bridge (main.cpp): plugin audioMaster // (SizeWindow) -> move the host window on the UI worker thread. static void (*g_vst2ResizeHook)(void*, int, int) = nullptr; // base64 encode/decode (RFC 4648) — local copies; Vst3Instrument.cpp keeps // its own so neither file depends on the other's statics. std::string b64Encode(const void* data, size_t len) { static const char tbl[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const uint8_t* d = static_cast(data); std::string out; out.reserve(((len + 2) / 3) * 4); for (size_t i = 0; i < len; i += 3) { uint32_t n = (uint32_t)d[i] << 16; if (i + 1 < len) n |= (uint32_t)d[i + 1] << 8; if (i + 2 < len) n |= (uint32_t)d[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] : '='; } return out; } 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; } bool b64Decode(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; } } // namespace // VST2HostState pimpl: everything aeffect.h needs to stay out of the header. struct Vst2HostState { HMODULE hModule = nullptr; AEffect* effect = nullptr; std::vector savedChunk; // preset chunk from effGetChunk std::string pluginDir; // directory of the plugin DLL (GetDirectory) }; // audioMaster callback — the plugin's channel to the host. VST2.4: return // version 2400, answer sample-rate/block-size queries, accept our MIDI. intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index, intptr_t value, void* ptr, float opt) { (void)index; (void)value; (void)ptr; (void)opt; if (t_editingProbe || true) std::cerr << "[Vst2Instrument][editprobe] audioMaster op=" << opcode << " idx=" << index << " val=" << value << " ptr=" << ptr << std::endl; Vst2Instrument* self = nullptr; if (effect) { std::lock_guard lock(g_v2Mutex); auto it = g_v2Registry.find(effect); if (it != g_v2Registry.end()) self = it->second; } if (!self) self = t_loading; if (t_loading) { // Construction-time probe dump: plugin asks the host before it returns // its AEffect. NULL from VSTPluginMain usually means we answered a // capability probe wrong — print everything during entry(). std::cerr << "[Vst2Instrument] audioMaster op=" << opcode << " idx=" << index << " during VSTPluginMain" << std::endl; } switch (opcode) { case kAudioMasterVersion: return 2400; case kAudioMasterGetSampleRate: return self ? (intptr_t)self->sampleRate() : 44100; case kAudioMasterGetBlockSize: return self ? (intptr_t)self->maxBlockSize() : 512; case kAudioMasterWantMidi: return 1; // we send MIDI events case kAudioMasterGetTime: { static VstTimeInfo ti; static double pos = 0.0; std::memset(&ti, 0, sizeof(ti)); ti.samplePos = pos; ti.sampleRate = self ? self->sampleRate() : 44100.0; ti.nanoSeconds = pos / 44100.0 * 1e9; ti.tempo = 120.0; ti.ppqPos = pos * 120.0 / 60.0 / 44100.0; ti.flags = kVstTempoValid | kVstTransportPlaying; // TEMP diag: DUNE silent - try forcing transport playing pos += (self ? self->maxBlockSize() : 512); std::cerr << "[Vst2Instrument][diag] getTime flags=" << ti.flags << " pos=" << pos << std::endl; return (intptr_t)&ti; } case kAudioMasterGetCurrentProcessLevel: return 1; // audio thread case kAudioMasterCurrentId: { const auto* fx = static_cast(self ? self->effect() : nullptr); if (fx) return (intptr_t)fx->uniqueID; // During VSTPluginMain no effect exists yet; some commercial // plugins abort when the host answers 0 (they take currentId as // a sanity check). Report a stable host signature instead. return 0x53464F52; // 'SFOR' } case kAudioMasterIdle: return 1; case kAudioMasterCanDo: { if (!ptr) return 0; const char* s = static_cast(ptr); if (std::strcmp(s, "receiveVstEvents") == 0 || std::strcmp(s, "receiveVstMidiEvent") == 0 || std::strcmp(s, "sendVstEvents") == 0 || std::strcmp(s, "sendVstMidiEvent") == 0 || std::strcmp(s, "sendVstTimeInfo") == 0 || std::strcmp(s, "sizeWindow") == 0 || std::strcmp(s, "supplyIdle") == 0) return 1; return 0; } case kAudioMasterAutomate: return 1; // param automation accepted (no-op) case kAudioMasterUpdateDisplay: return 1; case kAudioMasterBeginEdit: return 1; case kAudioMasterEndEdit: return 1; case kAudioMasterGetDirectory: { // Return the plugin's own directory. Some plugins (JUCE, skin- // loading synths) cache this during VSTPluginMain and later use // it to locate resources when the editor opens; a NULL answer // can crash effEditOpen. if (self && !self->pluginDir().empty()) return (intptr_t)self->pluginDir().c_str(); return 0; } case kAudioMasterSizeWindow: { // index = width, value = height (VST2.4 ABI). if (g_vst2ResizeHook && self) g_vst2ResizeHook(self->editorParent(), (int)index, (int)value); return 1; } default: return 0; } } static std::string v2_modName(uintptr_t a) { HMODULE m = nullptr; if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR)a, &m)) return ""; wchar_t buf[260]; DWORD n = GetModuleFileNameW(m, buf, 260); if (!n) return ""; std::wstring w(buf, n); std::string out; for (wchar_t c : w) out += (char)c; size_t slash = out.find_last_of("\/"); return slash == std::string::npos ? out : out.substr(slash + 1); } static void v2_dumpSEH(const CONTEXT& ctx) { std::cerr << "[Vst2Instrument] rip=0x" << std::hex << (uintptr_t)ctx.Rip << " rsp=0x" << (uintptr_t)ctx.Rsp << std::dec << std::endl; std::cerr << "[Vst2Instrument] rax=0x" << std::hex << (uintptr_t)ctx.Rax << " rbx=0x" << (uintptr_t)ctx.Rbx << " rcx=0x" << (uintptr_t)ctx.Rcx << " rdx=0x" << (uintptr_t)ctx.Rdx << " rsi=0x" << (uintptr_t)ctx.Rsi << " rdi=0x" << (uintptr_t)ctx.Rdi << " r8=0x" << (uintptr_t)ctx.R8 << " r9=0x" << (uintptr_t)ctx.R9 << std::dec << std::endl; auto tryStr = [](uintptr_t a) { if (!a || IsBadReadPtr((void*)a, 64)) return std::string(""); std::string out; const char* p = (const char*)a; for (int i = 0; i < 64; ++i) { char c = p[i]; if (!c) break; out += (c >= 32 && c < 127) ? c : '?'; } return out; }; std::cerr << "[Vst2Instrument] rcx=str: " << tryStr((uintptr_t)ctx.Rcx) << " | rdx=str: " << tryStr((uintptr_t)ctx.Rdx) << std::endl; std::cerr << "[Vst2Instrument] crash module: " << v2_modName((uintptr_t)ctx.Rip) << std::endl; const uintptr_t* sp = (const uintptr_t*)ctx.Rsp; for (int i = 0; i < 0x4000 / 8 && i < 128; ++i) { uintptr_t v; if (IsBadReadPtr(sp + i, sizeof(v))) break; v = sp[i]; std::string mn = v2_modName(v); if (!mn.empty()) std::cerr << " [" << i << "] 0x" << std::hex << v << " -> " << mn << std::dec << std::endl; } } // Dispatcher wrapper that catches SEH (access violations) inside the plugin // and reports code + fault address. C++ exceptions from the plugin are also // swallowed (return -1). /EHa makes catch(...) see SEH, but the code/address // are only available via __except. static intptr_t v2_dispatch(AEffect* fx, int32_t op, int32_t idx, intptr_t val, void* ptr, float opt, const char* where, uint32_t ch) { static DWORD g_sehCode = 0; static void* g_sehAddr = nullptr; static CONTEXT g_ctxCopy; __try { if (op == kEffectEditOpen) t_editingProbe = true; intptr_t r = fx->dispatcher(fx, op, idx, val, ptr, opt); if (op == kEffectEditOpen) t_editingProbe = false; return r; } __except (g_sehCode = GetExceptionCode(), g_sehAddr = GetExceptionInformation()->ExceptionRecord->ExceptionAddress, g_ctxCopy = *GetExceptionInformation()->ContextRecord, std::cerr << "[Vst2Instrument] " << where << " SEH code=0x" << std::hex << g_sehCode << std::dec << " addr=0x" << g_sehAddr << " ch=" << ch << std::endl, EXCEPTION_EXECUTE_HANDLER) { v2_dumpSEH(g_ctxCopy); return -1; } } Vst2Instrument::Vst2Instrument() : state_(nullptr), sampleRate_(44100.0), maxBlockSize_(512), loaded_(false) {} Vst2Instrument::~Vst2Instrument() { auto* s = static_cast(state_); if (!s) return; try { if (s->effect) { if (guiAttached_) s->effect->dispatcher(s->effect, kEffectEditClose, 0, 0, nullptr, 0); s->effect->dispatcher(s->effect, kEffectMainsChanged, 0, 0, nullptr, 0); s->effect->dispatcher(s->effect, kEffectStopProcess, 0, 0, nullptr, 0); s->effect->dispatcher(s->effect, kEffectClose, 0, 0, nullptr, 0); } } catch (...) {} { std::lock_guard lock(g_v2Mutex); if (s->effect) g_v2Registry.erase(s->effect); } if (s->hModule) FreeLibrary(s->hModule); delete s; state_ = nullptr; } bool Vst2Instrument::loadPlugin(const std::string& path, double sampleRate) { HMODULE h = LoadLibraryA(path.c_str()); if (!h) { std::cerr << "[Vst2Instrument] LoadLibraryA failed: " << path << " (err=" << GetLastError() << ")" << std::endl; return false; } using EntryFn = AEffect* (*)(audioMasterCallback); auto* entry = (EntryFn)GetProcAddress(h, "VSTPluginMain"); if (!entry) entry = (EntryFn)GetProcAddress(h, "main"); if (!entry) { std::cerr << "[Vst2Instrument] no VSTPluginMain/main export: " << path << std::endl; FreeLibrary(h); return false; } auto* s = new Vst2HostState(); s->hModule = h; { size_t slash = path.find_last_of("\/"); pluginDir_ = (slash == std::string::npos) ? std::string() : path.substr(0, slash); } t_loading = this; std::cerr << "[Vst2Instrument] calling VSTPluginMain..." << std::endl; AEffect* fx = entry(&audioMaster); t_loading = nullptr; std::cerr << "[Vst2Instrument] VSTPluginMain returned fx=" << (void*)fx << std::endl; if (!fx) { std::cerr << "[Vst2Instrument] VSTPluginMain returned NULL: " << path << std::endl; delete s; return false; } if (fx->magic != kEffectMagic) { std::cerr << "[Vst2Instrument] bad AEffect magic 0x" << std::hex << (uint32_t)fx->magic << std::dec << " (want 0x" << std::hex << kEffectMagic << std::dec << ") in: " << path << std::endl; try { fx->dispatcher(fx, kEffectClose, 0, 0, nullptr, 0); } catch (...) {} delete s; return false; } if (!(fx->flags & kEffectFlagsCanReplacing)) { std::cerr << "[Vst2Instrument] plugin lacks CanReplacing: " << path << std::endl; try { fx->dispatcher(fx, kEffectClose, 0, 0, nullptr, 0); } catch (...) {} delete s; return false; } s->effect = fx; state_ = s; path_ = path; sampleRate_ = sampleRate; { std::lock_guard lock(g_v2Mutex); g_v2Registry[fx] = this; } loaded_ = true; // Log identity strings — validates the ABI opcode table on real plugins. auto queryString = [&](int32_t op) { char buf[256] = {0}; fx->dispatcher(fx, op, 0, 0, buf, 0); return std::string(buf); }; std::cout << "[Vst2Instrument] loaded ch=" << channel_ << " " << queryString(kEffectGetVendorString) << " / " << queryString(kEffectGetProductString) << " / " << queryString(kEffectGetEffectName) << " v" << fx->version << " id=0x" << std::hex << (uint32_t)fx->uniqueID << std::dec << " flags=0x" << std::hex << (uint32_t)fx->flags << std::dec << " ins=" << fx->numInputs << " outs=" << fx->numOutputs << " progs=" << fx->numPrograms << " params=" << fx->numParams << " [" << path << "]" << std::endl; return true; } bool Vst2Instrument::init(double sampleRate, uint32_t maxBlockSize) { auto* s = static_cast(state_); if (!s || !s->effect) return false; sampleRate_ = sampleRate; maxBlockSize_ = maxBlockSize; AEffect* fx = s->effect; try { // TEMP diag variant B: sampleRate/blockSize BEFORE effOpen, block size // matches the actual render size (256), no StartProcess. fx->dispatcher(fx, kEffectSetSampleRate, 0, 0, nullptr, (float)sampleRate); fx->dispatcher(fx, kEffectSetBlockSize, 0, (intptr_t)maxBlockSize, nullptr, 0); fx->dispatcher(fx, kEffectOpen, 0, 0, nullptr, 0); fx->dispatcher(fx, kEffectSetProgram, 0, 0, nullptr, 0); fx->dispatcher(fx, kEffectMainsChanged, 0, 1, nullptr, 0); // TEMP diag: dump first params to see if the default patch is audible { const int pis[] = {0, 1, 10, 100, 500, 1000, 1160, 1168}; for (int pi : pis) { if (pi >= fx->numParams) continue; char nm[128] = {0}, disp[128] = {0}, lbl[128] = {0}; fx->dispatcher(fx, kEffectGetParamName, 0, pi, nm, 0); fx->dispatcher(fx, kEffectGetParamDisplay, 0, pi, disp, 0); fx->dispatcher(fx, kEffectGetParamLabel, 0, pi, lbl, 0); std::cerr << "[Vst2Instrument][diag] param " << pi << " name=" << nm << " disp=" << disp << " label=" << lbl << std::endl; } } // TEMP diag: does the plugin itself claim to receive MIDI? { const char* probes[] = {"receiveVstEvents", "receiveVstMidiEvent", "midiProgramNames", "bypass"}; for (auto pr : probes) { intptr_t r = fx->dispatcher(fx, kEffectCanDo, 0, 0, (void*)pr, 0); std::cerr << "[Vst2Instrument][diag] plugin canDo " << pr << " -> " << r << std::endl; } } // TEMP diag: send raw MIDI directly (bypass SHM path). CC7 vol max + noteOn // with kVstMidiEventIsRealtime + retriggers. { auto sendMidi = [&](int b0, int b1, int b2) { VstMidiEvent e; std::memset(&e, 0, sizeof(e)); e.type = kVstMidiType; e.byteSize = (int32_t)sizeof(e); e.flags = kVstMidiEventIsRealtime; e.midiData[0] = (char)b0; e.midiData[1] = (char)b1; e.midiData[2] = (char)b2; VstEvent* evs[1] = { reinterpret_cast(&e) }; VstEvents events; std::memset(&events, 0, sizeof(events)); events.numEvents = 1; events.events[0] = evs[0]; fx->dispatcher(fx, kEffectProcessEvents, 0, 0, &events, 0); }; std::cerr << "[Vst2Instrument][diag] raw CC7 127 + noteOn 60/100 x8" << std::endl; sendMidi(0xB0, 7, 127); for (int r = 0; r < 8; ++r) sendMidi(0x90, 60, 100); } // TEMP diag: does DUNE hold a preset chunk? dump its head as hex+ascii { void* chunkPtr = nullptr; intptr_t sz = fx->dispatcher(fx, kEffectGetChunk, 0, 1, &chunkPtr, 0); std::cerr << "[Vst2Instrument][diag] getChunk size=" << sz << " ptr=" << chunkPtr << std::endl; if (sz > 0 && chunkPtr) { const unsigned char* b = (const unsigned char*)chunkPtr; std::string ascii; for (intptr_t i = 0; i < sz && i < 512; ++i) { ascii += (b[i] >= 32 && b[i] < 127) ? (char)b[i] : '.'; } std::cerr << "[Vst2Instrument][diag] chunkhead: " << ascii << std::endl; std::cerr << "[Vst2Instrument][diag] chunkbytes: "; for (intptr_t i = 0; i < sz && i < 64; ++i) { char hx[8]; snprintf(hx, sizeof(hx), "%02x ", b[i]); std::cerr << hx; } std::cerr << std::endl; } } } catch (...) { std::cerr << "[Vst2Instrument] init dispatcher EXCEPTION ch=" << channel_ << std::endl; return false; } return true; } void Vst2Instrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) { (void)bank; programChange(channel, program); } void Vst2Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) { auto* s = static_cast(state_); if (!s || !s->effect) return; VstMidiEvent e; std::memset(&e, 0, sizeof(e)); e.type = kVstMidiType; e.byteSize = (int32_t)sizeof(e); e.deltaFrames = (int32_t)sampleOffset; e.midiData[0] = 0x90 | (int32_t)(channel & 0x0F); e.midiData[1] = (int32_t)(pitch & 0x7F); e.midiData[2] = (int32_t)(int)(velocity * 127.0f) & 0x7F; std::cerr << "[Vst2Instrument] noteOn ch=" << channel << " p=" << pitch << " v=" << (int)e.midiData[2] << std::endl; VstEvent* evs[1] = { reinterpret_cast(&e) }; VstEvents events; std::memset(&events, 0, sizeof(events)); events.numEvents = 1; events.reserved = 0; events.events[0] = evs[0]; s->effect->dispatcher(s->effect, kEffectProcessEvents, 0, 0, &events, 0); } void Vst2Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) { auto* s = static_cast(state_); if (!s || !s->effect) return; VstMidiEvent e; std::memset(&e, 0, sizeof(e)); e.type = kVstMidiType; e.byteSize = (int32_t)sizeof(e); e.deltaFrames = (int32_t)sampleOffset; e.midiData[0] = 0x80 | (int32_t)(channel & 0x0F); e.midiData[1] = (int32_t)(pitch & 0x7F); e.midiData[2] = 0; VstEvent* evs[1] = { reinterpret_cast(&e) }; VstEvents events; std::memset(&events, 0, sizeof(events)); events.numEvents = 1; events.reserved = 0; events.events[0] = evs[0]; s->effect->dispatcher(s->effect, kEffectProcessEvents, 0, 0, &events, 0); } void Vst2Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { auto* s = static_cast(state_); if (!s || !s->effect) return; VstMidiEvent e; std::memset(&e, 0, sizeof(e)); e.type = kVstMidiType; e.byteSize = (int32_t)sizeof(e); e.midiData[0] = 0xB0 | (int32_t)(channel & 0x0F); e.midiData[1] = (int32_t)(cc & 0x7F); e.midiData[2] = (int32_t)(value & 0x7F); VstEvent* evs[1] = { reinterpret_cast(&e) }; VstEvents events; std::memset(&events, 0, sizeof(events)); events.numEvents = 1; events.reserved = 0; events.events[0] = evs[0]; s->effect->dispatcher(s->effect, kEffectProcessEvents, 0, 0, &events, 0); } void Vst2Instrument::programChange(uint32_t channel, uint32_t program) { auto* s = static_cast(state_); if (!s || !s->effect) return; VstMidiEvent e; std::memset(&e, 0, sizeof(e)); e.type = kVstMidiType; e.byteSize = (int32_t)sizeof(e); e.midiData[0] = 0xC0 | (int32_t)(channel & 0x0F); e.midiData[1] = (int32_t)(program & 0x7F); VstEvent* evs[1] = { reinterpret_cast(&e) }; VstEvents events; std::memset(&events, 0, sizeof(events)); events.numEvents = 1; events.reserved = 0; events.events[0] = evs[0]; s->effect->dispatcher(s->effect, kEffectProcessEvents, 0, 0, &events, 0); } void Vst2Instrument::pitchBend(uint32_t channel, uint32_t bend14) { auto* s = static_cast(state_); if (!s || !s->effect) return; int32_t bend = (int32_t)bend14 - 2097152; // 14-bit signed, center 2097152 VstMidiEvent e; std::memset(&e, 0, sizeof(e)); e.type = kVstMidiType; e.byteSize = (int32_t)sizeof(e); e.midiData[0] = 0xE0 | (int32_t)(channel & 0x0F); e.midiData[1] = bend & 0x7F; e.midiData[2] = (bend >> 7) & 0x7F; VstEvent* evs[1] = { reinterpret_cast(&e) }; VstEvents events; std::memset(&events, 0, sizeof(events)); events.numEvents = 1; events.reserved = 0; events.events[0] = evs[0]; s->effect->dispatcher(s->effect, kEffectProcessEvents, 0, 0, &events, 0); } bool Vst2Instrument::canOpenGUI() const { auto* e = effect(); return loaded_ && e && (((const AEffect*)e)->flags & kEffectFlagsHasEditor); } bool Vst2Instrument::openGUI(void* parentWindowHandle) { return attachView(parentWindowHandle); } bool Vst2Instrument::attachView(void* parentWindowHandle) { auto* s = static_cast(state_); if (!s || !s->effect || guiAttached_) return guiAttached_; if (!(s->effect->flags & kEffectFlagsHasEditor)) return false; HWND parent = (HWND)parentWindowHandle; if (!parent || !IsWindow(parent)) { std::cerr << "[Vst2Instrument] attachView: bad parent hwnd ch=" << channel_ << std::endl; return false; } std::cerr << "[Vst2Instrument] attachView CALLING effEditOpen ch=" << channel_ << " parent=0x" << std::hex << (uintptr_t)parent << std::dec << std::endl; intptr_t rc = v2_dispatch(s->effect, kEffectEditOpen, 0, 0, parent, 0, "effEditOpen", channel_); std::cerr << "[Vst2Instrument] attachView effEditOpen returned rc=" << rc << " ch=" << channel_ << std::endl; if (rc == -1) return false; // SEH / plugin exception inside the DLL std::cout << "[Vst2Instrument] editOpen OK ch=" << channel_ << std::endl; ERect* r = nullptr; rc = v2_dispatch(s->effect, kEffectEditGetRect, 0, 0, &r, 0, "effEditGetRect", channel_); if (rc != -1 && rc && r) { int w = r->right - r->left; int h = r->bottom - r->top; std::cout << "[Vst2Instrument] editor size " << w << "x" << h << " ch=" << channel_ << std::endl; if (w > 0 && h > 0) SetWindowPos(parent, nullptr, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); } editorParent_ = parent; guiAttached_ = true; std::cout << "[Vst2Instrument] GUI attached ch=" << channel_ << std::endl; return true; } void Vst2Instrument::closeGUI() { auto* s = static_cast(state_); if (!s || !s->effect || !guiAttached_) { guiAttached_ = false; editorParent_ = nullptr; return; } try { s->effect->dispatcher(s->effect, kEffectEditClose, 0, 0, nullptr, 0); } catch (...) { std::cerr << "[Vst2Instrument] closeGUI EXCEPTION ch=" << channel_ << std::endl; } guiAttached_ = false; editorParent_ = nullptr; std::cout << "[Vst2Instrument] GUI closed ch=" << channel_ << std::endl; } bool Vst2Instrument::hasAttachedView() const { return guiAttached_; } void Vst2Instrument::guiIdle() { auto* s = static_cast(state_); if (!s || !s->effect || !guiAttached_) return; try { s->effect->dispatcher(s->effect, kEffectEditIdle, 0, 0, nullptr, 0); } catch (...) { std::cerr << "[Vst2Instrument] guiIdle EXCEPTION ch=" << channel_ << std::endl; } } void Vst2Instrument::setResizeHook(void (*fn)(void*, int, int)) { g_vst2ResizeHook = fn; } void Vst2Instrument::captureState() { auto* s = static_cast(state_); if (!s || !s->effect) return; if (!(s->effect->flags & kEffectFlagsProgramChunks)) return; void* chunkPtr = nullptr; intptr_t size = s->effect->dispatcher(s->effect, kEffectGetChunk, 0, 1, &chunkPtr, 0); if (size > 0 && chunkPtr) s->savedChunk.assign(static_cast(chunkPtr), static_cast(chunkPtr) + size); } void Vst2Instrument::restoreState() { auto* s = static_cast(state_); if (!s || !s->effect || s->savedChunk.empty()) return; s->effect->dispatcher(s->effect, kEffectSetChunk, 0, (intptr_t)s->savedChunk.size(), s->savedChunk.data(), 0); } std::string Vst2Instrument::serializeState() const { auto* s = static_cast(state_); if (!s || s->savedChunk.empty()) return std::string(); return b64Encode(s->savedChunk.data(), s->savedChunk.size()); } void Vst2Instrument::loadSerializedState(const std::string& base64) { auto* s = static_cast(state_); if (!s || !s->effect || base64.empty()) return; if (!b64Decode(base64, s->savedChunk)) { std::cerr << "[Vst2Instrument] loadSerializedState: bad base64 ch=" << channel_ << std::endl; return; } restoreState(); std::cout << "[Vst2Instrument] preset restored ch=" << channel_ << " (" << s->savedChunk.size() << "B)" << std::endl; } void Vst2Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) { std::cerr << "[Vst2Instrument] pab START n=" << numSamples << std::endl; auto* s = static_cast(state_); std::cerr << "[Vst2Instrument] pab state ok=" << (s && s->effect) << std::endl; if (!s || !s->effect) { std::memset(outputL, 0, numSamples * sizeof(float)); std::memset(outputR, 0, numSamples * sizeof(float)); return; } std::cerr << "[Vst2Instrument] pab before alignF" << std::endl; AEffect* fx = s->effect; // 64B-align inputs: DUNE 3 etc. use aligned SIMD on audio buffers. auto alignF = [](std::vector& v, size_t n) -> float* { // TEMP diag (heap-corrupt hunt): huge tail + input canary to measure // how far DUNE writes past n. const size_t kTail = 2097152 + 64; if (v.size() < n + kTail) v.assign(n + kTail, 0.0f); uintptr_t a = reinterpret_cast(v.data()); return reinterpret_cast((a + 63) & ~(uintptr_t)63); }; float* inL = alignF(inL_, numSamples); float* inR = alignF(inR_, numSamples); for (uint32_t i = 0; i < numSamples + 2097152; ++i) { inL[i] = 0.0f; // TEMP: clean input (NaN poisons effect plugins) inR[i] = 0.0f; } // Build pointer arrays covering the plugin's full ins/outs: plugins with // numOutputs > 2 (e.g. Qin_RV outs=32) read all pointers — short arrays // hand them garbage and crash. const int nIn = fx->numInputs; const int nOut = fx->numOutputs; std::vector inBufs(nIn > 0 ? nIn : 1, nullptr); std::vector outBufs(nOut > 0 ? nOut : 1, nullptr); if (nIn > 0) { inBufs[0] = inL; if (nIn > 1) inBufs[1] = inR; } if (nOut > 0) { outBufs[0] = outputL; if (nOut > 1) outBufs[1] = outputR; } if (nOut > 2) { // scratch for channels 2..nOut-1 (zeroed; host ring only keeps L/R) extraOut_.assign((size_t)nOut * numSamples, 0.0f); for (int c = 2; c < nOut; ++c) outBufs[c] = extraOut_.data() + (size_t)c * numSamples; } if (nIn == 0) inBufs[0] = nullptr; // ins=0: pass NULL input array entry std::cerr << "[Vst2Instrument] processReplacing IN ch=" << channel_ << " n=" << numSamples << " ins=" << nIn << " outs=" << nOut << " inL=0x" << std::hex << (uintptr_t)inL << " inR=0x" << (uintptr_t)inR << " outL=0x" << (uintptr_t)outputL << " outR=0x" << (uintptr_t)outputR << std::dec << std::endl; fx->processReplacing(fx, inBufs.data(), outBufs.data(), (int32_t)numSamples); std::cerr << "[Vst2Instrument] processReplacing OUT ch=" << channel_ << std::endl; // TEMP diag: does the plugin write any audio? { static int diagBlk = 0; float peakL = 0.f, peakR = 0.f; for (uint32_t i = 0; i < numSamples; ++i) { float a = outputL[i], b = outputR[i]; if (a > peakL) peakL = a; else if (-a > peakL) peakL = -a; if (b > peakR) peakR = b; else if (-b > peakR) peakR = -b; } if (peakL != 0.f || peakR != 0.f || diagBlk < 10) { std::cerr << "[Vst2Instrument][diag] PEAK blk=" << diagBlk << " n=" << numSamples << " L=" << peakL << " R=" << peakR << std::endl; diagBlk++; } } // TEMP diag: how far past n did DUNE write into the INPUT buffers? { int farL = -1, farR = -1; for (uint32_t i = 0; i < 2097152; ++i) { uint32_t bL, bR; std::memcpy(&bL, &inL[numSamples + i], 4); std::memcpy(&bR, &inR[numSamples + i], 4); if (bL != 0x7FC00000u) farL = (int)i; if (bR != 0x7FC00000u) farR = (int)i; } if (farL >= 0 || farR >= 0) std::cerr << "[Vst2Instrument][diag] INPUT OVERRUN past n: L=" << farL << " R=" << farR << std::endl; } } const void* Vst2Instrument::effect() const { auto* s = static_cast(state_); return s ? s->effect : nullptr; }