Files
SonicForgeStudio/native_bridge/src/Vst2Instrument.cpp
T
admin 6efef85e47 fix(bridge): VST2 host dung ABI SDK — crash outs>2, NaN input, editOpen(NULL), opcodes; docs A14 MIDI-out + phan tich Ozone -Inf
- aeffect.h: bo sung opcodes 4-22, 53, 55 + audioMaster 15/41-44 + ERect (dung chuan VST2.4 SDK, DUNE load OK khong crash)
- Vst2Instrument: cap du pointer arrays theo numInputs/numOutputs (fix AV 0xC0000005 Qin outs=32), input fill 0.0f (het NaN poison effect), bo probe effEditOpen(NULL) (crash WaveObserver), GetDirectory + SizeWindow hook, canDo sendVstEvents/sendVstTimeInfo
- TASKS.md: A14 MIDI-out tu plugin (Scaler2/Instacomposer/MelodicFlow xuat MIDI khong phai audio)
- PLAN_MASTERBUS_FX_RACK_VST.md: phan tich Ozone -Inf — chain day du, nguyen nhan = audio DAW khong chay qua bridge master ring
2026-08-19 20:57:16 +07:00

746 lines
31 KiB
C++

// 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 <windows.h>
#include <cstring>
#include <iostream>
#include <mutex>
#include <unordered_map>
namespace {
// Registry so the plugin's audioMaster callback can find its host instance.
std::mutex g_v2Mutex;
std::unordered_map<const void*, Vst2Instrument*> 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<const uint8_t*>(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<uint8_t>& out) {
out.clear();
out.reserve((in.size() / 4) * 3);
uint32_t acc = 0;
int bits = 0;
for (unsigned char ch : in) {
if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue;
int v = b64Val(ch);
if (v < 0) return false;
acc = (acc << 6) | (uint32_t)v;
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back((uint8_t)((acc >> bits) & 0xFF));
}
}
return true;
}
} // namespace
// VST2HostState pimpl: everything aeffect.h needs to stay out of the header.
struct Vst2HostState {
HMODULE hModule = nullptr;
AEffect* effect = nullptr;
std::vector<uint8_t> 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<std::mutex> 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<const AEffect*>(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<const char*>(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("<bad>");
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<Vst2HostState*>(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<std::mutex> 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<std::mutex> 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<Vst2HostState*>(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<VstEvent*>(&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<Vst2HostState*>(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<VstEvent*>(&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<Vst2HostState*>(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<VstEvent*>(&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<Vst2HostState*>(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<VstEvent*>(&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<Vst2HostState*>(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<VstEvent*>(&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<Vst2HostState*>(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<VstEvent*>(&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<Vst2HostState*>(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<Vst2HostState*>(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<Vst2HostState*>(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<Vst2HostState*>(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<uint8_t*>(chunkPtr),
static_cast<uint8_t*>(chunkPtr) + size);
}
void Vst2Instrument::restoreState() {
auto* s = static_cast<Vst2HostState*>(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<Vst2HostState*>(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<Vst2HostState*>(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<Vst2HostState*>(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<float>& 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<uintptr_t>(v.data());
return reinterpret_cast<float*>((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<float*> inBufs(nIn > 0 ? nIn : 1, nullptr);
std::vector<float*> 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<Vst2HostState*>(state_);
return s ? s->effect : nullptr;
}