Add VST2 hosting to native bridge; fix CC7 data2 routing verified via SHM test

VST2 (Steinberg 2.4 ABI) host via Vst2Instrument: aeffect.h, dispatcher,
process events/replacing, kEffectStartProcess, headless GUI skip.
- create_instrument returns Vst2Instrument for InstrumentType::VST2
- 64B-aligned scratch buffers for SIMD plugins; SEH crash capture module+stack
- plugins.py: VST2 resolves .dll/.so; vst_engine.py reports VST2 type
- app.jsx: derive instrumentType from scan (VST3 vs VST2), not hardcoded
- Install exes refreshed from Release build

SF2 FluidSynth audio verified end-to-end through SHM ring (energy 1.33):
silence was a test-harness bug (CC7 value written to velocity field, bridge
reads data2) — app sends data2 correctly. All temporary diagnostics removed.
This commit is contained in:
2026-08-17 07:55:46 +07:00
parent 9d9b4b3686
commit 4111995a5c
12 changed files with 768 additions and 24 deletions
+59 -15
View File
@@ -4,6 +4,7 @@
#include <fluidsynth.h>
#include <sfizz.hpp>
#include "Vst3Instrument.h"
#include "Vst2Instrument.h"
#include "SandboxVst3Host.h"
// void* members keep fluid types out of the public header; cast here.
@@ -173,8 +174,8 @@ std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(In
if (std::getenv("SF_SANDBOX_VST3"))
return std::make_unique<SandboxVst3Host>();
return std::make_unique<Vst3Instrument>();
// ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented.
case InstrumentType::VST2:
return std::make_unique<Vst2Instrument>();
default: return nullptr;
}
}
@@ -196,6 +197,12 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
else if (auto* s = dynamic_cast<SandboxVst3Host*>(inst.get()))
loaded = s->loadPlugin(path, sampleRate, channel);
}
else if (type == InstrumentType::VST2) {
if (auto* v2 = dynamic_cast<Vst2Instrument*>(inst.get())) {
v2->setChannel(channel);
loaded = v2->loadPlugin(path, sampleRate);
}
}
if (type == InstrumentType::VST3) {
if (auto* v = dynamic_cast<Vst3Instrument*>(inst.get()))
v->setChannel(channel);
@@ -394,16 +401,31 @@ void InstrumentEngineManager::allNotesOff() {
}
}
// Some commercial plugins (DUNE 3) use aligned SIMD loads/stores on their
// audio buffers; plain std::vector is only 16B-aligned. Over-allocate and
// align the pointer handed to plugins to 64B.
static float* alignF(std::vector<float>& v, size_t n) {
if (v.size() < n + 16) v.assign(n + 16, 0.0f);
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
}
// G3.3: SEH cannot live inside a function that needs C++ unwinding (C2712),
// so per-channel process() runs in this helper. A plugin access violation
// (0xC0000005) is caught here — the channel is muted instead of killing the
// whole bridge process. /EHa (set in CMakeLists) allows mixing with the
// outer C++ try/catch net in main.cpp.
static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR, uint32_t n) {
static DWORD g_sehCode = 0;
static void* g_sehAddr = nullptr;
static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR, uint32_t n, DWORD* outCode) {
*outCode = 0;
__try {
inst->processAudioBlock(outL, outR, n);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
} __except (g_sehCode = GetExceptionCode(),
g_sehAddr = GetExceptionInformation()->ExceptionRecord->ExceptionAddress,
EXCEPTION_EXECUTE_HANDLER) {
*outCode = g_sehCode;
return false;
}
}
@@ -418,27 +440,49 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
std::memset(outputL, 0, numSamples * sizeof(float));
std::memset(outputR, 0, numSamples * sizeof(float));
if (channels_.empty()) return;
if (scratchL_.size() < numSamples) {
scratchL_.resize(numSamples);
scratchR_.resize(numSamples);
}
for (auto& [ch, inst] : channels_) {
if (channelQuiet(ch)) continue; // editor open / crashed: do not process
std::memset(scratchL_.data(), 0, numSamples * sizeof(float));
std::memset(scratchR_.data(), 0, numSamples * sizeof(float));
if (!SafeProcessChannel(inst.get(), scratchL_.data(), scratchR_.data(), numSamples)) {
float* spL = alignF(scratchL_, numSamples);
float* spR = alignF(scratchR_, numSamples);
std::memset(spL, 0, numSamples * sizeof(float));
std::memset(spR, 0, numSamples * sizeof(float));
DWORD sehCode = 0;
if (!SafeProcessChannel(inst.get(), spL, spR, numSamples, &sehCode)) {
// G3.3: plugin fault — mute this channel only, keep the bridge up.
if (!crashedCh_[ch]) {
crashedCh_[ch] = true;
std::cerr << "[NativeBridge] G3.3: channel " << ch
<< " plugin crashed in processAudioBlock (SEH) — muted until reload"
<< std::endl;
{
HMODULE m = nullptr;
char mod[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)g_sehAddr, &m))
GetModuleFileNameA(m, mod, MAX_PATH);
std::cerr << "[NativeBridge] G3.3: channel " << ch
<< " plugin crashed in processAudioBlock (SEH code=0x" << std::hex << sehCode
<< " addr=0x" << (uintptr_t)g_sehAddr
<< std::dec << " mod=" << mod
<< ") — muted until reload"
<< std::endl;
void* bt[16];
USHORT nf = RtlCaptureStackBackTrace(0, 16, bt, nullptr);
std::cerr << "[NativeBridge] stack:";
for (USHORT fi = 0; fi < nf; ++fi) {
HMODULE bm = nullptr;
char bmod[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)bt[fi], &bm))
GetModuleFileNameA(bm, bmod, MAX_PATH);
std::cerr << " " << std::hex << (uintptr_t)bt[fi] << std::dec
<< "(" << bmod << ")";
}
std::cerr << std::endl;
}
}
continue;
}
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] += scratchL_[i];
outputR[i] += scratchR_[i];
outputL[i] += spL[i];
outputR[i] += spR[i];
}
}
}