diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 9dc9e91..f621739 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -1248,7 +1248,7 @@ def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str]) (fname.lower() == base.lower() or os.path.splitext(fname)[0].lower() == base_noext): return os.path.join(root, fname) ext = (".vst3" if instrument_type.upper() == "VST3" else - ".vst2" if instrument_type.upper() == "VST2" else + (".dll", ".so") if instrument_type.upper() == "VST2" else ".sfz" if instrument_type.upper() == "SFZ" else None) if ext: try: @@ -1267,7 +1267,7 @@ def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str]) continue for root, _, files in os.walk(base_dir): for fn in files: - if fn.lower().endswith(ext) and \ + if (fn.lower().endswith(ext) if isinstance(ext, tuple) else fn.lower().endswith(ext)) and \ (fn.lower() == base.lower() or os.path.splitext(fn)[0].lower() == base_noext): return os.path.join(root, fn) # V9 bug 8: Windows VST3 = FOLDER "X.vst3" (chua X.vst3.dll) — @@ -1277,7 +1277,7 @@ def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str]) # base_noext. for sub in os.listdir(root): sp = os.path.join(root, sub) - if os.path.isdir(sp) and sub.lower().endswith(ext) and \ + if os.path.isdir(sp) and (sub.lower().endswith(ext) if isinstance(ext, tuple) else sub.lower().endswith(ext)) and \ (sub.lower() == base.lower() or os.path.splitext(sub)[0].lower() == base_noext): return sp return None diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index 0b3fb2d..62af676 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -470,10 +470,13 @@ class PluginManager: return presets[:256] def list_available(self) -> dict: + plugins = self._scan_plugins() return { "vst_instruments": [ - {"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD} - for k in self._scan_plugins().keys() + {"id": k, "name": k, + "type": "VST3" if p.lower().endswith(".vst3") else "VST2", + "has_native_support": HAS_PEDALBOARD} + for k, p in plugins.items() ], "soundfonts": self._scan_soundfonts() } diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 400b845..5bb361a 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -6382,9 +6382,16 @@ const loadVstToBridge = async (track) => { // V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename — // KHONG walk folder VST3 ("X.vst3/X.vst3.dll") → 404. Dung path da luu // (synth_engine.plugin_path) hoac cache tu listPlugins. + // VST2 (.dll) scan type 'VST2' - khong hardcode VST3 (se LOAD sai loai). + let btype = 'VST3'; let knownPath = track.synth_engine && track.synth_engine.plugin_path; if (!knownPath && window.__bridgePluginPaths) knownPath = window.__bridgePluginPaths[instrumentId]; - const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: knownPath || null, instrumentType: 'VST3', channel: bch }); + if (instrumentSelectorData) { + const v0 = (instrumentSelectorData.vst_instruments || []).find(v => v.id === instrumentId || v.name === instrumentId); + if (v0 && v0.path) knownPath = v0.path; + if (v0 && v0.type) btype = v0.type; + } + const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: knownPath || null, instrumentType: btype, channel: bch }); let p = r && r.path ? r.path : null; if (!p && knownPath) p = knownPath; if (!p && window.SonicAPI.listPlugins) { @@ -6393,15 +6400,16 @@ const loadVstToBridge = async (track) => { const v = ((pl && pl.vst_instruments) || []).find(x => x.id === instrumentId || x.name === instrumentId); if (v && v.path) { p = v.path; + if (v && v.type) btype = v.type; if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {}; window.__bridgePluginPaths[instrumentId] = v.path; } } catch (e2) { console.warn('[Bridge] listPlugins fallback fail:', e2); } } if (!p) return false; - const ok = await window.NativeBridgeService.loadInstrument(p, 'VST3', bch); + const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch); if (ok) window.__bridgeLoadedChannels[bch] = true; - console.log('[Bridge] auto-load track', track.id, '-> VST3 ch', bch, ok ? 'OK' : 'FAIL', p); + console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p); return !!ok; } catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); return false; } }; @@ -15753,7 +15761,9 @@ const App = () => { // (flag cũ còn true) → OPEN_GUI đến bridge khi channel chưa có instrument // → deferred mãi, nút GUI không mở gì. if (isSf && window.__bridgeLoadedChannels) delete window.__bridgeLoadedChannels[bch]; - const btype = isSf ? 'SF2' : 'VST3'; + // VST2 (.dll) scan type la 'VST2' - khong hardcode VST3 (se LOAD sai + // loai -> create_instrument nullptr -> fail im lang). + let btype = isSf ? 'SF2' : 'VST3'; let bpath = null; if (instrumentSelectorData) { if (isSf) { @@ -15763,6 +15773,7 @@ const App = () => { } else { const v0 = (instrumentSelectorData.vst_instruments || []).find(v => v.id === instrumentId || v.name === instrumentId); bpath = v0 && v0.path; + if (v0 && v0.type) btype = v0.type; } } (async () => { diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index b7a29f3..8b23825 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/install/plugin_host.exe b/install/plugin_host.exe index 684ab70..223e1d0 100644 Binary files a/install/plugin_host.exe and b/install/plugin_host.exe differ diff --git a/native_bridge/CMakeLists.txt b/native_bridge/CMakeLists.txt index f3554d5..2459a53 100644 --- a/native_bridge/CMakeLists.txt +++ b/native_bridge/CMakeLists.txt @@ -48,6 +48,7 @@ else() endif() include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include ${FLUIDSYNTH_INCLUDE_DIR} ${SFIZZ_INCLUDE_DIR} @@ -58,6 +59,7 @@ add_executable(daw_vst_bridge src/NativeInstrumentEngine.cpp src/SharedMemoryIPC.cpp src/Vst3Instrument.cpp + src/Vst2Instrument.cpp src/StateStore.cpp src/SandboxVst3Host.cpp ) @@ -88,6 +90,8 @@ add_executable(gui_probe src/NativeInstrumentEngine.cpp src/SharedMemoryIPC.cpp src/Vst3Instrument.cpp + src/Vst2Instrument.cpp + src/SandboxVst3Host.cpp ) if(VST3_SDK_TARGET) target_compile_definitions(gui_probe PRIVATE HAVE_VST3SDK=1) @@ -125,6 +129,9 @@ endif() add_executable(seh_channel_test tests/seh_channel_test.cpp src/Vst3Instrument.cpp + src/Vst2Instrument.cpp + src/SandboxVst3Host.cpp + src/SharedMemoryIPC.cpp ) if(VST3_SDK_TARGET) target_compile_definitions(seh_channel_test PRIVATE HAVE_VST3SDK=1) diff --git a/native_bridge/include/INativeInstrument.h b/native_bridge/include/INativeInstrument.h index 1d576dc..23cdf1d 100644 --- a/native_bridge/include/INativeInstrument.h +++ b/native_bridge/include/INativeInstrument.h @@ -33,6 +33,9 @@ public: // Open/close Native GUI window virtual bool openGUI(void* parentWindowHandle) = 0; + // Headless engines (VST2 without an editor, SF2/SFZ): the bridge must + // not create/reuse a window for them. + virtual bool canOpenGUI() const { return true; } virtual void closeGUI() = 0; // VST3 reopen-after-close: the bridge checks before openGUI whether the // instance must be rebuilt; while it is, the engine sets reloading=true diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index e598662..3116d0b 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -4,6 +4,7 @@ #include #include #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 InstrumentEngineManager::create_instrument(In if (std::getenv("SF_SANDBOX_VST3")) return std::make_unique(); return std::make_unique(); - // ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented. case InstrumentType::VST2: + return std::make_unique(); default: return nullptr; } } @@ -196,6 +197,12 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type, else if (auto* s = dynamic_cast(inst.get())) loaded = s->loadPlugin(path, sampleRate, channel); } + else if (type == InstrumentType::VST2) { + if (auto* v2 = dynamic_cast(inst.get())) { + v2->setChannel(channel); + loaded = v2->loadPlugin(path, sampleRate); + } + } if (type == InstrumentType::VST3) { if (auto* v = dynamic_cast(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& v, size_t n) { + if (v.size() < n + 16) v.assign(n + 16, 0.0f); + uintptr_t a = reinterpret_cast(v.data()); + return reinterpret_cast((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]; } } } diff --git a/native_bridge/src/Vst2Instrument.cpp b/native_bridge/src/Vst2Instrument.cpp new file mode 100644 index 0000000..a15c601 --- /dev/null +++ b/native_bridge/src/Vst2Instrument.cpp @@ -0,0 +1,433 @@ +// 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; + +// 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 +}; + +// 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; + 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; + std::memset(&ti, 0, sizeof(ti)); + ti.sampleRate = self ? self->sampleRate() : 44100.0; + ti.tempo = 120.0; + ti.flags = kVstTempoValid | kVstTransportPlaying; + 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) + return 1; + return 0; + } + default: return 0; + } +} + +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) { + 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; + 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 { + fx->dispatcher(fx, kEffectOpen, 0, 0, nullptr, 0); + fx->dispatcher(fx, kEffectSetSampleRate, 0, 0, nullptr, (float)sampleRate); + fx->dispatcher(fx, kEffectSetBlockSize, 0, (intptr_t)maxBlockSize, nullptr, 0); + fx->dispatcher(fx, kEffectSetProgram, 0, 0, nullptr, 0); + fx->dispatcher(fx, kEffectMainsChanged, 0, 1, nullptr, 0); + fx->dispatcher(fx, kEffectStartProcess, 0, 0, nullptr, 0); + } 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 - 8192; // 14-bit signed, center 8192 + 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::openGUI(void* parentWindowHandle) { + (void)parentWindowHandle; + return false; // headless — no editor (deferred per DESIGN_VST2_BACKWARD_COMPAT.md) +} + +void Vst2Instrument::closeGUI() {} + +bool Vst2Instrument::hasAttachedView() const { return false; } + +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* { + if (v.size() < n + 16) v.assign(n + 16, 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); + std::memset(inL, 0, numSamples * sizeof(float)); + std::memset(inR, 0, numSamples * sizeof(float)); + float* inBufs[2] = { inL, inR }; + float* outBufs[2] = { outputL, outputR }; + std::cerr << "[Vst2Instrument] processReplacing IN ch=" << channel_ << " n=" << numSamples << std::endl; + fx->processReplacing(fx, inBufs, outBufs, (int32_t)numSamples); + std::cerr << "[Vst2Instrument] processReplacing OUT ch=" << channel_ << std::endl; +} + +const void* Vst2Instrument::effect() const { + auto* s = static_cast(state_); + return s ? s->effect : nullptr; +} diff --git a/native_bridge/src/Vst2Instrument.h b/native_bridge/src/Vst2Instrument.h new file mode 100644 index 0000000..325c47f --- /dev/null +++ b/native_bridge/src/Vst2Instrument.h @@ -0,0 +1,57 @@ +// native_bridge/src/Vst2Instrument.h +// VST2 (VST 2.4, 64-bit) instrument host. Clean-room ABI header +// (native_bridge/vestige/aeffect.h) — no Steinberg SDK, no LMMS code. +// Headless: no GUI editor (openGUI returns false); preset state is kept via +// effGetChunk/effSetChunk (fxp-style chunks). +#ifndef VST2_INSTRUMENT_H +#define VST2_INSTRUMENT_H + +#include "INativeInstrument.h" + +#include +#include + +class Vst2Instrument : public INativeInstrument { +public: + Vst2Instrument(); + ~Vst2Instrument() override; + + bool loadPlugin(const std::string& path, double sampleRate); + void setChannel(uint32_t channel) { channel_ = channel; } + + bool init(double sampleRate, uint32_t maxBlockSize) override; + void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override; + void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override; + void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override; + void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override; + void programChange(uint32_t channel, uint32_t program) override; + void pitchBend(uint32_t channel, uint32_t bend14) override; + bool openGUI(void* parentWindowHandle) override; + bool canOpenGUI() const override { return false; } + void closeGUI() override; + bool hasAttachedView() const override; + void captureState() override; + void restoreState() override; + std::string serializeState() const override; + void loadSerializedState(const std::string& base64) override; + void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override; + + // Accessors used by the audioMaster callback (same translation unit's + // friend-ish helpers) without exposing AEffect* in the header. + double sampleRate() const { return sampleRate_; } + uint32_t maxBlockSize() const { return maxBlockSize_; } + const void* effect() const; // AEffect*; null until loaded + +private: + void* state_; // Vst2HostState* (pimpl — aeffect.h stays out of the header) + std::string path_; + double sampleRate_; + uint32_t maxBlockSize_; + bool loaded_; + uint32_t channel_ = 0; + // scratch buffers for silent plugin inputs + std::vector inL_; + std::vector inR_; +}; + +#endif // VST2_INSTRUMENT_H diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 94f295c..23e2e2f 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -793,6 +793,16 @@ int main(int argc, char* argv[]) { g_pendingGui[guiCh] = { (void*)arg1, pluginId }; return; } + // VST2 headless (no editor) hoac instrument load fail -> khong tao + // window, khong reuse window cua channel khac (window trong voi title + // sai plugin). Frontend hien "VST2 headless - no GUI" thay vi window. + if (instruments.withInstrument(guiCh, [](INativeInstrument* i0) { + return !i0 || !i0->canOpenGUI(); + })) { + std::cerr << "[NativeBridge] GUI skip ch=" << guiCh + << " plugin=" << pluginId << " (headless - no editor)" << std::endl; + return; + } // Dedupe OPEN_GUI spam (frontend openVstGuiRetry): view dang // attached -> GUI da mo, khong post attach job lan nua. Re-attach // tren view dang attached lam plugin loi (Nexus createView null). diff --git a/native_bridge/vestige/aeffect.h b/native_bridge/vestige/aeffect.h new file mode 100644 index 0000000..713d9c5 --- /dev/null +++ b/native_bridge/vestige/aeffect.h @@ -0,0 +1,176 @@ +// native_bridge/vestige/aeffect.h +// Clean-room VST2.4 host ABI header (64-bit Windows). +// +// Written from the public VST2.4 interface specification (structure layout, +// dispatcher opcode numbers, flag bits) — NOT copied from Steinberg's SDK or +// from LMMS's vestige (LMMS is GPL-2.0+; reusing it would re-taint the bridge +// with GPL, defeating this repo's license cleanup). Only ABI facts are +// encoded here; all prose is original. +// +// A VST2 plugin is a DLL exporting `VSTPluginMain` (modern) or `main` +// (legacy): AEffect* VSTPluginMain(audioMasterCallback host). +#ifndef VESTIGE_AEFFECT_H +#define VESTIGE_AEFFECT_H + +#include + +struct AEffect; // forward decl so the callback typedefs below can reference it + +// intptr_t: VST2 ABI uses pointer-sized integers on 64-bit hosts. +typedef intptr_t (*dispatcherProc)(AEffect* effect, int32_t opcode, + int32_t index, intptr_t value, + void* ptr, float opt); +typedef void (*processProc)(AEffect* effect, float** inputs, float** outputs, + int32_t sampleFrames); +typedef void (*setParameterProc)(AEffect* effect, int32_t index, float value); +typedef float (*getParameterProc)(AEffect* effect, int32_t index); +typedef void (*processReplacingProc)(AEffect* effect, float** inputs, + float** outputs, int32_t sampleFrames); +typedef void (*processDoubleReplacingProc)(AEffect* effect, double** inputs, + double** outputs, + int32_t sampleFrames); +typedef intptr_t (*audioMasterCallback)(AEffect* effect, int32_t opcode, + int32_t index, intptr_t value, + void* ptr, float opt); + +struct AEffect { + int32_t magic; // 'VstP' == 0x56737450 + dispatcherProc dispatcher; + processProc process; + setParameterProc setParameter; + getParameterProc getParameter; + int32_t numPrograms; + int32_t numParams; + int32_t numInputs; + int32_t numOutputs; + int32_t flags; + intptr_t resvd1; // pointer-sized per VST2.4 ABI + intptr_t resvd2; + int32_t initialDelay; + int32_t realQualities; + int32_t offQualities; + float ioRatio; + void* object; + void* user; + int32_t uniqueID; + int32_t version; + processReplacingProc processReplacing; + processDoubleReplacingProc processDoubleReplacing; + char future[56]; // reserved for future use (must stay intact) +}; + +// Plugin capability flag bits (AEffect::flags). +#define kEffectFlagsHasEditor (1 << 0) +#define kEffectFlagsHasClip (1 << 1) +#define kEffectFlagsHasVu (1 << 2) +#define kEffectFlagsCanMono (1 << 3) +#define kEffectFlagsCanReplacing (1 << 4) +#define kEffectFlagsProgramChunks (1 << 5) +#define kEffectFlagsIsSynth (1 << 8) +#define kEffectFlagsNoSoundInStop (1 << 9) +#define kEffectFlagsCanDoubleReplacing (1 << 12) + +// Dispatcher opcodes (VST 2.4 numbering). +enum { + kEffectOpen = 0, // effOpen + kEffectClose = 1, // effClose + kEffectSetProgram = 2, // effSetProgram + kEffectGetProgram = 3, // effGetProgram + kEffectSetSampleRate = 10, // effSetSampleRate + kEffectSetBlockSize = 11, // effSetBlockSize + kEffectMainsChanged = 12, // effMainsChanged (value 1 = on, 0 = off) + kEffectGetChunk = 23, // effGetChunk + kEffectSetChunk = 24, // effSetChunk + kEffectProcessEvents = 25, // effProcessEvents + kEffectGetPlugCategory = 35, + kEffectGetEffectName = 45, + kEffectGetVendorString = 47, + kEffectGetProductString = 48, + kEffectGetVendorVersion = 49, + kEffectCanDo = 51, + kEffectGetVstVersion = 58, + kEffectStartProcess = 71, + kEffectStopProcess = 72 +}; + +// audioMaster opcodes (host callback). +enum { + kAudioMasterAutomate = 0, + kAudioMasterVersion = 1, + kAudioMasterCurrentId = 2, + kAudioMasterIdle = 3, + kAudioMasterWantMidi = 6, + kAudioMasterGetTime = 7, + kAudioMasterProcessEvents = 8, + kAudioMasterGetSampleRate = 16, + kAudioMasterGetBlockSize = 17, + kAudioMasterGetCurrentProcessLevel = 23, + kAudioMasterGetVendorString = 32, + kAudioMasterGetProductString = 33, + kAudioMasterGetVendorVersion = 34, + kAudioMasterVendorSpecific = 35, + kAudioMasterCanDo = 37, + kAudioMasterGetLanguage = 38 +}; + +// MIDI event passed via effProcessEvents. The plugin receives a VstEvents +// array; each event starts with a VstEvent header, VstMidiEvent extends it. +struct VstEvent { + int32_t type; // kVstMidiType == 1 for MIDI events + int32_t byteSize; // sizeof(VstMidiEvent) + int32_t deltaFrames; + int32_t flags; +}; + +struct VstMidiEvent { + int32_t type; // kVstMidiType == 1 + int32_t byteSize; // sizeof(VstMidiEvent) + int32_t deltaFrames; + int32_t flags; + int32_t noteLength; // in sample frames, if available, else 0 + int32_t noteOffset; // offset into note, if available, else 0 + char midiData[4]; // 1-3 MIDI bytes; midiData[3] reserved (zero) + char detune; // -64..+63 cents + char noteOffVelocity; + char reserved1; + char reserved2; +}; + +struct VstEvents { + int32_t numEvents; + intptr_t reserved; + VstEvent* events[2]; +}; + +// Time info returned to kAudioMasterGetTime (host fills this). +struct VstTimeInfo { + double samplePos; + double sampleRate; + double nanoSeconds; + double ppqPos; + double tempo; + double barStartPos; + double cycleStartPos; + double cycleEndPos; + int32_t timeSigNumerator; + int32_t timeSigDenominator; + int32_t smpteOffset; + int32_t smpteFrameRate; + int32_t samplesToNextClock; + int32_t flags; +}; + +#define kVstMidiType 1 +#define kEffectMagic 0x56737450 // 'VstP' + +// VstTimeInfo::flags bits (kAudioMasterGetTime). +#define kVstTransportPlaying 1 +#define kVstTransportCycleActive 2 +#define kVstTransportChanged 4 +#define kVstTempoValid 8 + +// Host responses for the plugin's capability probes (kEffectCanDo). +#define kCanDoYes 1 +#define kCanDoNo 0 + +#endif // VESTIGE_AEFFECT_H