Files
SonicForgeStudio/native_bridge/src/NativeInstrumentEngine.cpp
T

528 lines
21 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"
#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 <cstring>
#include <iostream>
#include <thread>
#include <chrono>
// -----------------------------------------------------------------
// 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;
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;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, 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;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program);
}
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
if (!synth) return;
// fluid_synth_pitch_bend takes the raw 14-bit value (center 8192).
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)
// -----------------------------------------------------------------
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;
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);
}
}
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;
// 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;
}
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;
}
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);
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();
e.presetBase64 = i->serializeState();
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]) 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
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<float>& v, size_t n) {
if (v.size() < n + 16) v.assign(n + 16, 0.0f);
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
}
// G3.3: SEH cannot live inside a function that needs C++ unwinding (C2712),
// so per-channel process() runs in this helper. A plugin access violation
// (0xC0000005) is caught here — the channel is muted instead of killing the
// whole bridge process. /EHa (set in CMakeLists) allows mixing with the
// outer C++ try/catch net in main.cpp.
#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,
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
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);
std::memset(spL, 0, numSamples * sizeof(float));
std::memset(spR, 0, numSamples * sizeof(float));
#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;
}
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] += spL[i];
outputR[i] += spR[i];
}
}
}