722 lines
30 KiB
C++
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();
|
|
}
|
|
} |