Files
SonicForgeStudio/native_bridge/src/NativeInstrumentEngine.cpp
T

722 lines
30 KiB
C++

// native_bridge/src/NativeInstrumentEngine.cpp
#include "NativeInstrumentEngine.h"
#include <fluidsynth.h>
#include <sfizz.hpp>
#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<fluid_synth_t*>(synth))
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
#ifdef _WIN32
#include <windows.h>
#endif
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <thread>
#include <chrono>
#include <cmath>
#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<int>(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<int>(cc), static_cast<int>(value));
}
void SfizzInstrument::programChange(uint32_t channel, uint32_t program) {
sfizzSynth.programChange(0, static_cast<int>(program));
}
void SfizzInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
sfizzSynth.pitchWheel(0, static_cast<int>(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<char> 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<INativeInstrument> InstrumentEngineManager::create_instrument(InstrumentType type) {
switch (type) {
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
case InstrumentType::SFZ: return std::make_unique<SfizzInstrument>();
#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<SandboxVst3Host>();
return std::make_unique<Vst3Instrument>();
case InstrumentType::VST2:
return std::make_unique<Vst2Instrument>();
#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<SandboxVst2Host>();
#endif
bool loaded = false;
if (type == InstrumentType::SOUNDFONT_SF2_SF3)
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
else if (type == InstrumentType::SFZ)
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
#ifdef _WIN32
else if (type == InstrumentType::VST3) {
if (auto* v = dynamic_cast<Vst3Instrument*>(inst.get()))
loaded = v->loadPlugin(path, sampleRate);
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);
} else if (auto* s2 = dynamic_cast<SandboxVst2Host*>(inst.get())) {
loaded = s2->loadPlugin(path, sampleRate, channel);
}
}
if (type == InstrumentType::VST3) {
if (auto* v = dynamic_cast<Vst3Instrument*>(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<INativeInstrument> oldInst;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(mu_);
if (channel < 16) calibratingCh_[channel] = true;
}
const float mk = calibrate(inst, sampleRate, blockSize);
{
std::lock_guard<std::mutex> 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<INativeInstrument> oldInst;
{
std::lock_guard<std::mutex> 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<std::mutex> lock(mu_);
auto it = paths_.find(channel);
return it == paths_.end() ? std::string() : it->second;
}
bool InstrumentEngineManager::snapshot(BridgeStateSnapshot& out) {
std::lock_guard<std::mutex> 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<std::mutex> lock(mu_);
auto it = channels_.find(channel);
return it == channels_.end() ? nullptr : it->second.get();
}
bool InstrumentEngineManager::has(uint32_t channel) {
std::lock_guard<std::mutex> lock(mu_);
return channels_.count(channel) != 0;
}
void InstrumentEngineManager::setReloading(uint32_t channel, bool on) {
std::lock_guard<std::mutex> 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<std::mutex> lock(mu_);
return channel < 16 && reloadingCh_[channel];
}
void InstrumentEngineManager::markCrashed(uint32_t channel) {
std::lock_guard<std::mutex> lock(mu_);
if (channel < 16) crashedCh_[channel] = true;
}
bool InstrumentEngineManager::isCrashed(uint32_t channel) const {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mu_);
for (auto& [ch, inst] : channels_) {
if (channelQuiet(ch)) continue; // mid-rebuild / editor open: do not touch the instance
inst->allNotesOff(ch);
}
}
// 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) {
// 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<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.
#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
// + RMS, tinh makeup = min(gRms, gPeak) sao cho note don ra ~ -18 dBFS RMS
// va ~ -6 dBFS peak (he so nao nho hon thang de ca hai khong vuot nguong).
// Clamp: khong bu patch cam qua muc, khong tat patch to. renderAll ap dung
// makeup + mix limiter cap -3 dBFS (nhieu track cong vao master chain nam
// trong cua so -6..-3 dBFS peak).
static constexpr float kCalTargetPeak = 0.5f; // note don: peak ~ -6 dBFS
static constexpr float kCalTargetRms = 0.1259f; // note don: RMS ~ -18 dBFS (10^(-18/20))
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<float> 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;
};
double sumSq = 0.0; // RMS chi do tren phan body (sustain), bo qua tail
uint64_t rmsN = 0;
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; }
for (uint32_t i = 0; i < n; ++i) {
sumSq += (double)L[i] * (double)L[i] + (double)R[i] * (double)R[i];
}
rmsN += n;
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;
}
}
const float rms = (float)std::sqrt(sumSq / (2.0 * (double)rmsN));
if (peak < 1e-4f) {
std::cerr << "[Calibrate] silent probe peak=" << peak << " rms=" << rms << " makeup=1.0" << std::endl;
return 1.0f;
}
float g = kCalTargetRms / rms; // de RMS chay dung -18 dBFS
const float gPeak = kCalTargetPeak / peak; // de peak khong vuot -6 dBFS
if (gPeak < g) g = gPeak; // min: ca hai khong vuot nguong
if (g < kCalMinMakeup) g = kCalMinMakeup;
if (g > kCalMaxMakeup) g = kCalMaxMakeup;
std::cerr << "[Calibrate] peak=" << peak << " rms=" << rms << " 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<std::mutex> 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];
}
}
// Mixer summing: smoothed brickwall limiter caps the summed mix at
// kCeilingPeak = -3 dBFS -> MASTER CHAIN INPUT nam trong cua so
// -6..-3 dBFS peak (1 track ~ -6 dBFS; nhieu track cong len, toi da -3 dBFS).
// Fast attack (~2-3 samples) khi mix vuot tran, slow release ve unity.
// Thay the hard clamp cu (gain step dau block gay zipper crackle).
// limiterGain_ persists across renderAll calls.
const float kCeilingPeak = 0.7071f; // -3 dBFS
// Attack 0.9 (near-instant) caused zipper crackle: nhieu track sum ~2.0,
// target 0.35 -> gain 1.0->0.42 trong 1 sample = step -7.6 dB moi transient.
// 0.1 = ~10 samples (0.2ms @48k) ve tran: smooth, van giu ceiling -3 dBFS.
const float kAttack = 0.1f;
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_ > kCeilingPeak)
? (kCeilingPeak / (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<std::mutex> lock(mu_);
for (auto& kv : channels_) {
kv.second->guiIdle();
}
}