// native_bridge/src/NativeInstrumentEngine.cpp #include "NativeInstrumentEngine.h" #include #include #ifdef _WIN32 #include "Vst3Instrument.h" #include "Vst2Instrument.h" #include "SandboxVst3Host.h" #include "SandboxVst2Host.h" #endif // void* members keep fluid types out of the public header; cast here. #define FS_SYNTH (static_cast(synth)) #define FS_SETTINGS (static_cast(settings)) #ifdef _WIN32 #include #endif #include #include #include #include #include #include #include #ifdef _WIN32 #endif // ----------------------------------------------------------------- // 1. SOUNDFONT ENGINE (.SF2 / .SF3) VIA FLUIDSYNTH C API // ----------------------------------------------------------------- FluidSynthInstrument::FluidSynthInstrument() : settings(nullptr), synth(nullptr), sfontId(-1) { for (uint32_t i = 0; i < 16; ++i) bank_[i] = 0; } FluidSynthInstrument::~FluidSynthInstrument() { if (synth) delete_fluid_synth(FS_SYNTH); if (settings) delete_fluid_settings(FS_SETTINGS); } bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sampleRate) { if (synth) { delete_fluid_synth(FS_SYNTH); synth = nullptr; } if (settings) { delete_fluid_settings(FS_SETTINGS); settings = nullptr; } settings = new_fluid_settings(); fluid_settings_setnum(FS_SETTINGS, "synth.sample-rate", sampleRate); fluid_settings_setint(FS_SETTINGS, "synth.polyphony", 256); fluid_settings_setint(FS_SETTINGS, "synth.verbose", 0); synth = new_fluid_synth(FS_SETTINGS); if (!synth) return false; // Unity baseline; auto-calibration trong InstrumentEngineManager::calibrate // can bang peak ve -6 dBFS cho moi engine (VST2/VST3/SF2/SFZ). fluid_synth_set_gain(FS_SYNTH, 1.0f); sfontId = fluid_synth_sfload(FS_SYNTH, path.c_str(), 1); if (sfontId == -1) return false; // Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano) for (uint32_t ch = 0; ch < 16; ++ch) { bank_[ch] = 0; program_[ch] = 0; fluid_synth_program_select(FS_SYNTH, ch, sfontId, 0, 0); } return true; } bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) { return synth != nullptr; } void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) { if (!synth || channel >= 16) return; bank_[channel] = bank; program_[channel] = program; // SF2 fallback: nhieu soundfont khong co bank 128 (GM drum) — neu select // fail, thu lai bank 0 de khong cam (fluidsynth giu preset cu -> sai am). if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program) != FLUID_OK && bank != 0) { bank_[channel] = 0; fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program); } } void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) { if (!synth) return; int velInt = static_cast(velocity * 127.0f); fluid_synth_noteon(FS_SYNTH, channel, pitch, velInt); } void FluidSynthInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) { if (!synth) return; fluid_synth_noteoff(FS_SYNTH, channel, pitch); } void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { if (!synth || channel >= 16) return; // Bank select MSB/LSB (A12): CC0 = (bank>>7)&0x7F, CC32 = bank&0x7F if (cc == 0) bank_[channel] = (bank_[channel] & 0x7Fu) | ((value & 0x7Fu) << 7); else if (cc == 32) bank_[channel] = (bank_[channel] & ~0x7Fu) | (value & 0x7Fu); fluid_synth_cc(FS_SYNTH, channel, cc, value); } void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) { if (!synth || channel >= 16) return; // Dung bank da nhan tu CC0/CC32 — bank hardcode 0 lam preset o bank != 0 // khong duoc chon (fluid giu preset cu -> ra piano sai). program_[channel] = program; if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program) != FLUID_OK && bank_[channel] != 0) { bank_[channel] = 0; fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program); } } void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) { if (!synth) return; // fluid_synth_pitch_bend takes the raw 14-bit value (center 2097152). fluid_synth_pitch_bend(FS_SYNTH, channel, bend14); } bool FluidSynthInstrument::openGUI(void* parentWindowHandle) { return false; // SoundFont uses Web GUI Manager / Reskinned Knobs } void FluidSynthInstrument::closeGUI() {} // G2.1: expose last bank/program for the state snapshot. uint32_t FluidSynthInstrument::bankOf(uint32_t channel) const { return channel < 16 ? bank_[channel] : 0; } uint32_t FluidSynthInstrument::programOf(uint32_t channel) const { return channel < 16 ? program_[channel] : 0; } void FluidSynthInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) { if (!synth) return; fluid_synth_write_float(FS_SYNTH, numSamples, outputL, 0, 1, outputR, 0, 1); } // ----------------------------------------------------------------- // 2. SFZ ENGINE (.SFZ) VIA SFIZZ C++ API // ----------------------------------------------------------------- bool SfizzInstrument::loadSfzFile(const std::string& path, double sampleRate) { sfizzSynth.setSampleRate(sampleRate); return sfizzSynth.loadSfzFile(path); } bool SfizzInstrument::init(double sampleRate, uint32_t maxBlockSize) { sfizzSynth.setSampleRate(sampleRate); sfizzSynth.setSamplesPerBlock(maxBlockSize); return true; } void SfizzInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {} void SfizzInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) { sfizzSynth.hdNoteOn(sampleOffset, pitch, velocity); } void SfizzInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) { sfizzSynth.hdNoteOff(sampleOffset, pitch, 0.0f); } void SfizzInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { sfizzSynth.cc(0, static_cast(cc), static_cast(value)); } void SfizzInstrument::programChange(uint32_t channel, uint32_t program) { sfizzSynth.programChange(0, static_cast(program)); } void SfizzInstrument::pitchBend(uint32_t channel, uint32_t bend14) { sfizzSynth.pitchWheel(0, static_cast(bend14)); } bool SfizzInstrument::openGUI(void* parentWindowHandle) { return false; } void SfizzInstrument::closeGUI() {} void SfizzInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) { float* channels[2] = { outputL, outputR }; sfizzSynth.renderBlock(channels, numSamples, 1); // numOutputs=1 = stereo L/R pair (2 ch) } // ----------------------------------------------------------------- // 3. MULTI-CHANNEL INSTRUMENT MANAGER (A10) // ----------------------------------------------------------------- // jBridge stub detection: jBridge-wrapped VST2s (e.g. Qin_RV_x64.dll) embed // the string "Bridger64.dll". The bundled wrapper allows only ONE instance // of the same plugin per host process (2nd instance loses the shared audio // buffer -> silent), so these MUST run in a sandbox child (one process per // instance). Native VST2s (Nexus) host in-process with an embedded editor. static bool is_jbridge_dll(const std::string& path) { FILE* f = fopen(path.c_str(), "rb"); if (!f) return false; std::vector d; char tmp[16384]; size_t n; while ((n = fread(tmp, 1, sizeof(tmp), f)) > 0) d.insert(d.end(), tmp, tmp + n); fclose(f); static const char needle[] = "Bridger64.dll"; return std::search(d.begin(), d.end(), needle, needle + sizeof(needle) - 1) != d.end(); } std::unique_ptr InstrumentEngineManager::create_instrument(InstrumentType type) { switch (type) { case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique(); case InstrumentType::SFZ: return std::make_unique(); #ifdef _WIN32 case InstrumentType::VST3: // G4.2: SF_SANDBOX_VST3=1 -> host the VST3 in a child // process (plugin_host.exe). Crash kills only the child. if (std::getenv("SF_SANDBOX_VST3")) return std::make_unique(); return std::make_unique(); case InstrumentType::VST2: return std::make_unique(); #else case InstrumentType::VST3: case InstrumentType::VST2: // Linux bridge: SF2/SF3 + SFZ only (Docker render). VST3/VST2 // hosting is Windows-only (COM/HWND/SHM realtime loop). return nullptr; #endif default: return nullptr; } } bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type, const std::string& path, double sampleRate, uint32_t blockSize) { if (channel >= 16) return false; auto inst = create_instrument(type); if (!inst) return false; #ifdef _WIN32 // jBridge VST2 (Qin): sandbox in a child process - one Bridger64 // instance per process. Native VST2 stays in-process. if (type == InstrumentType::VST2 && is_jbridge_dll(path)) inst = std::make_unique(); #endif bool loaded = false; if (type == InstrumentType::SOUNDFONT_SF2_SF3) loaded = static_cast(inst.get())->loadSoundFontFile(path, sampleRate); else if (type == InstrumentType::SFZ) loaded = static_cast(inst.get())->loadSfzFile(path, sampleRate); #ifdef _WIN32 else if (type == InstrumentType::VST3) { if (auto* v = dynamic_cast(inst.get())) loaded = v->loadPlugin(path, sampleRate); 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); } else if (auto* s2 = dynamic_cast(inst.get())) { loaded = s2->loadPlugin(path, sampleRate, channel); } } if (type == InstrumentType::VST3) { if (auto* v = dynamic_cast(inst.get())) v->setChannel(channel); } #endif if (!loaded) return false; // init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile. if (!inst->init(sampleRate, blockSize)) return false; // Calibrate output level (makeup) - probe render truoc khi publish vao map // de renderAll ap dung ngay, khong block audio thread. const float mk = calibrate(inst.get(), sampleRate, blockSize); // Replacing an existing instrument drops its voices with the old engine. // Load may run on a detached thread (VST3 init is slow): only the map // write is under the mutex so renderAll on the audio loop never stalls. // CRITICAL: the OLD instrument is destroyed AFTER mu_ is released. VST3 // teardown (setProcessing(false) / terminate / view removed) can block; // under the lock it would stall renderAll -> bridge Not Responding, // transport stop hangs, notes never turn off (must kill daw_engine). std::unique_ptr oldInst; { std::lock_guard lock(mu_); auto it = channels_.find(channel); if (it != channels_.end()) oldInst = std::move(it->second); channels_[channel] = std::move(inst); paths_[channel] = path; types_[channel] = type; // Fresh instance is by construction not reloading — clear the channel // flag so real-time MIDI dispatch (which drops reloading channels) // flows to it again. G3.3: a reload also clears the crash-mute. reloadingCh_[channel] = false; crashedCh_[channel] = false; makeup_[channel] = mk; } if (oldInst) { isBypassed_.store(true); std::this_thread::sleep_for(std::chrono::milliseconds(10)); try { oldInst.reset(); } catch (...) { isBypassed_.store(false); throw; } isBypassed_.store(false); } return true; } float InstrumentEngineManager::recalibrate(uint32_t channel, double sampleRate, uint32_t blockSize) { INativeInstrument* inst = nullptr; { std::lock_guard lock(mu_); auto it = channels_.find(channel); if (it != channels_.end()) inst = it->second.get(); } if (!inst) return 1.0f; // calibrate() probe-renders (up to ~0.9s) - never hold mu_ (audio stall). // Flag the channel so renderAll skips it: the probe notes would otherwise // leak into the live mix (auto-play at startup, bug 1). { std::lock_guard lock(mu_); if (channel < 16) calibratingCh_[channel] = true; } const float mk = calibrate(inst, sampleRate, blockSize); { std::lock_guard lock(mu_); if (channel < 16) calibratingCh_[channel] = false; makeup_[channel] = mk; } return mk; } void InstrumentEngineManager::unload(uint32_t channel) { if (channel >= 16) return; // Destructor runs on the CALLER thread (VST3 terminate must run on the // channel worker). The map write is under mu_ so renderAll never stalls. std::unique_ptr oldInst; { std::lock_guard lock(mu_); auto it = channels_.find(channel); if (it != channels_.end()) oldInst = std::move(it->second); channels_.erase(channel); paths_.erase(channel); types_.erase(channel); makeup_.erase(channel); reloadingCh_[channel] = false; } if (oldInst) { isBypassed_.store(true); std::this_thread::sleep_for(std::chrono::milliseconds(10)); try { oldInst.reset(); } catch (...) { isBypassed_.store(false); throw; } isBypassed_.store(false); } } std::string InstrumentEngineManager::pathOf(uint32_t channel) { std::lock_guard lock(mu_); auto it = paths_.find(channel); return it == paths_.end() ? std::string() : it->second; } bool InstrumentEngineManager::snapshot(BridgeStateSnapshot& out) { std::lock_guard lock(mu_); out.instruments.clear(); for (auto& kv : channels_) { uint32_t ch = kv.first; if (ch < 16 && reloadingCh_[ch]) continue; // old instance dying InstrumentStateEntry e; e.channel = ch; auto tp = types_.find(ch); e.type = tp == types_.end() ? 0 : (int)tp->second; auto pp = paths_.find(ch); e.path = pp == paths_.end() ? std::string() : pp->second; if (INativeInstrument* i = kv.second.get()) { // VST3: refresh the saved buffers then serialize them. Safe // here: mu_ is held, so the audio loop cannot be inside // process() on this instance while getState runs. i->captureState(); std::string saved = i->serializeState(); e.presetBase64 = saved.size() >= 100 ? saved : std::string(); e.bank = i->bankOf(ch); e.program = i->programOf(ch); } out.instruments.push_back(std::move(e)); } return true; } INativeInstrument* InstrumentEngineManager::get(uint32_t channel) { std::lock_guard lock(mu_); auto it = channels_.find(channel); return it == channels_.end() ? nullptr : it->second.get(); } bool InstrumentEngineManager::has(uint32_t channel) { std::lock_guard lock(mu_); return channels_.count(channel) != 0; } void InstrumentEngineManager::setReloading(uint32_t channel, bool on) { std::lock_guard lock(mu_); if (channel < 16) reloadingCh_[channel] = on; auto it = channels_.find(channel); if (it != channels_.end()) it->second->setReloading(on); } bool InstrumentEngineManager::isReloading(uint32_t channel) const { std::lock_guard lock(mu_); return channel < 16 && reloadingCh_[channel]; } void InstrumentEngineManager::markCrashed(uint32_t channel) { std::lock_guard lock(mu_); if (channel < 16) crashedCh_[channel] = true; } bool InstrumentEngineManager::isCrashed(uint32_t channel) const { std::lock_guard lock(mu_); return channel < 16 && crashedCh_[channel]; } // Quiet = reloading (mid-rebuild) OR crashed (G3.3). G1.4: bo // mute-when-editor-open — editor co the mo trong luc PLAY, audio loop van // process() binh thuong. G3.3: channel crash -> mute vi trang thai plugin // khong xac dinh sau fault; xu ly tiep se fault lai. // Called with mu_ held by the real-time dispatch. bool InstrumentEngineManager::channelQuiet(uint32_t ch) const { if (ch >= 16 || reloadingCh_[ch] || crashedCh_[ch] || calibratingCh_[ch]) return true; return false; } // Real-time MIDI dispatch: hold mu_ for the WHOLE call so assign()/unload() // (map swap + old-instance destruction outside the lock) and reload()/reloadForGUI() // (state_ deleted in place on the worker) can never destroy/free the instance // while dispatch is inside a method on it — use-after-free when loading a new // VSTi while other channels keep playing. Reloading channels are skipped: their // instance is mid-teardown and must not be touched. void InstrumentEngineManager::noteOn(uint32_t channel, uint32_t pitch, float velocity) { std::lock_guard lock(mu_); if (channelQuiet(channel)) return; auto it = channels_.find(channel); if (it == channels_.end()) return; it->second->noteOn(channel, pitch, velocity, 0); } void InstrumentEngineManager::noteOff(uint32_t channel, uint32_t pitch) { std::lock_guard lock(mu_); if (channelQuiet(channel)) return; auto it = channels_.find(channel); if (it == channels_.end()) return; it->second->noteOff(channel, pitch, 0); } void InstrumentEngineManager::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { std::lock_guard lock(mu_); if (channelQuiet(channel)) return; auto it = channels_.find(channel); if (it == channels_.end()) return; it->second->controlChange(channel, cc, value); } void InstrumentEngineManager::programChange(uint32_t channel, uint32_t program) { std::lock_guard lock(mu_); if (channelQuiet(channel)) return; auto it = channels_.find(channel); if (it == channels_.end()) return; it->second->programChange(channel, program); } void InstrumentEngineManager::pitchBend(uint32_t channel, uint32_t bend14) { std::lock_guard lock(mu_); if (channelQuiet(channel)) return; auto it = channels_.find(channel); if (it == channels_.end()) return; it->second->pitchBend(channel, bend14); } void InstrumentEngineManager::allNotesOff() { std::lock_guard lock(mu_); for (auto& [ch, inst] : channels_) { if (channelQuiet(ch)) continue; // mid-rebuild / editor open: do not touch the instance for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0); } } // 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) { // 64B-aligned pointer can sit up to 63B into the allocation. const size_t kTail = 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); } // 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. #ifdef _WIN32 static DWORD g_sehCode = 0; static void* g_sehAddr = nullptr; #endif #ifdef _WIN32 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 (g_sehCode = GetExceptionCode(), g_sehAddr = GetExceptionInformation()->ExceptionRecord->ExceptionAddress, std::cerr << "[NativeBridge][G3.3][diag] SEH code=0x" << std::hex << g_sehCode << " rip=0x" << (uintptr_t)g_sehAddr << " fault=0x" << std::hex << (GetExceptionInformation()->ExceptionRecord->NumberParameters > 1 ? GetExceptionInformation()->ExceptionRecord->ExceptionInformation[1] : 0) << " isWrite=" << (GetExceptionInformation()->ExceptionRecord->NumberParameters > 0 ? GetExceptionInformation()->ExceptionRecord->ExceptionInformation[0] : 0) << std::dec << std::endl, EXCEPTION_EXECUTE_HANDLER) { *outCode = g_sehCode; return false; } } #else static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR, uint32_t n, uint32_t* outCode) { *outCode = 0; try { inst->processAudioBlock(outL, outR, n); return true; } catch (...) { *outCode = 1; return false; } } #endif // --- Auto-calibration (am luong on dinh moi engine VST2/VST3/SF2/SFZ) --- // Probe-render 3 notes (60/64/67, velocity 0.8) ngay sau khi load, do peak, // tinh makeup sao cho note don ra ~ -6 dBFS (0.5). Clamp: khong bu patch cam // qua muc, khong tat patch to. renderAll ap dung makeup + peak-safety @1.0 // (chord/transient vuot 1.0 se bi keo ve 1.0 - khong clip, khong meo). static constexpr float kCalTargetPeak = 0.5f; static constexpr float kCalMinMakeup = 0.125f; static constexpr float kCalMaxMakeup = 8.0f; float InstrumentEngineManager::calibrate(INativeInstrument* inst, double sampleRate, uint32_t blockSize) { if (!inst) return 1.0f; const uint32_t bs = blockSize ? blockSize : 512; const uint32_t body = (uint32_t)(sampleRate * 0.6); const uint32_t tail = (uint32_t)(sampleRate * 0.25); // Sandbox hosts render through a child process at consumer pace: consume // one block per real-time block (~5.8ms @256/44.1k) so the child wakes // from its idle Sleep(15), picks up the probe notes and renders steady // state. Direct engines: no pacing (probe finishes in µs). const bool realtime = inst->probeNeedsRealtime(); std::vector L(bs + 64, 0.0f), R(bs + 64, 0.0f); const int pitches[3] = { 60, 64, 67 }; float peak = 0.0f; auto renderProbe = [&](uint32_t n) { #ifdef _WIN32 DWORD code = 0; #else uint32_t code = 0; #endif if (!SafeProcessChannel(inst, L.data(), R.data(), n, &code)) return false; for (uint32_t i = 0; i < n; ++i) { float a = L[i]; if (a < 0.0f) a = -a; if (a > peak) peak = a; float b = R[i]; if (b < 0.0f) b = -b; if (b > peak) peak = b; } return true; }; for (int p : pitches) { inst->noteOn(0, (uint32_t)p, 0.8f, 0); uint32_t off = 0; while (off < body) { uint32_t n = bs; if (off + n > body) n = body - off; if (!renderProbe(n)) { inst->noteOff(0, (uint32_t)p, 0); return 1.0f; } if (realtime) Sleep((DWORD)(n * 1000.0 / sampleRate + 0.5)); off += n; } inst->noteOff(0, (uint32_t)p, 0); off = 0; while (off < tail) { uint32_t n = bs; if (off + n > tail) n = tail - off; if (!renderProbe(n)) return 1.0f; if (realtime) Sleep((DWORD)(n * 1000.0 / sampleRate + 0.5)); off += n; } } if (peak < 1e-4f) { std::cerr << "[Calibrate] silent probe peak=" << peak << " makeup=1.0" << std::endl; return 1.0f; } float g = kCalTargetPeak / peak; if (g < kCalMinMakeup) g = kCalMinMakeup; if (g > kCalMaxMakeup) g = kCalMaxMakeup; std::cerr << "[Calibrate] peak=" << peak << " makeup=" << g << std::endl; return g; } void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) { if (isBypassed_.load()) { std::memset(outputL, 0, numSamples * sizeof(float)); std::memset(outputR, 0, numSamples * sizeof(float)); return; } std::lock_guard lock(mu_); std::memset(outputL, 0, numSamples * sizeof(float)); std::memset(outputR, 0, numSamples * sizeof(float)); if (channels_.empty()) return; for (auto& [ch, inst] : channels_) { if (channelQuiet(ch)) continue; // editor open / crashed: do not process float* spL = alignF(scratchL_, numSamples); float* spR = alignF(scratchR_, numSamples); #ifdef _WIN32 DWORD sehCode = 0; #else uint32_t sehCode = 0; #endif 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; #ifdef _WIN32 { 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; } #else std::cerr << "[NativeBridge] G3.3: channel " << ch << " plugin crashed in processAudioBlock — muted until reload" << std::endl; #endif } continue; } // Auto-normalize: makeup calibrated (constant per-channel gain, no steps). auto mit = makeup_.find(ch); const float g = (mit != makeup_.end()) ? mit->second : 1.0f; for (uint32_t i = 0; i < numSamples; ++i) { spL[i] *= g; spR[i] *= g; } for (uint32_t i = 0; i < numSamples; ++i) { outputL[i] += spL[i]; outputR[i] += spR[i]; } } // Smoothed brickwall limiter on the summed mix: fast attack (~2-3 samples) // when the mix exceeds 1.0, slow release back to unity. Replaces the old // per-block hard clamp whose gain step at block boundaries caused zipper // crackle. limiterGain_ persists across renderAll calls. const float kAttack = 0.4f; const float kRelease = 0.0006f; for (uint32_t i = 0; i < numSamples; ++i) { float a = outputL[i] < 0.0f ? -outputL[i] : outputL[i]; float b = outputR[i] < 0.0f ? -outputR[i] : outputR[i]; const float m = a > b ? a : b; const float target = (m * limiterGain_ > 1.0f) ? (1.0f / (m + 1e-12f)) : 1.0f; if (target < limiterGain_) limiterGain_ += (target - limiterGain_) * kAttack; else limiterGain_ += (1.0f - limiterGain_) * kRelease; outputL[i] *= limiterGain_; outputR[i] *= limiterGain_; } } void InstrumentEngineManager::guiIdleAll() { std::lock_guard lock(mu_); for (auto& kv : channels_) { kv.second->guiIdle(); } }