feat: native host bridge integration — C++ bridge, Rust SHM, JS routing, build scripts, docs
- native_bridge/: InstrumentEngineManager multi-channel, sample-accurate, CC/program/pitchbend, transport, Vst3Instrument stub (HAVE_VST3SDK) - src-tauri: shm.rs, bridge spawn + audio pump + health monitor, open_vst_gui, externalBin, commands - app: UnifiedMidiRouter, NativeBridgeService, bridgeAudioNode, audioRoutingEngine, Plugin Manager UI, Bridge/WASM indicator, set_position sync - build: 3 ps1 (force-added, build/ ignored), verify_bundle --check-bridge, CI workflow - docs: TASKS.md, TEST_NOTES.md (Windows verify checklist), install/report updates
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// native_bridge/src/NativeInstrumentEngine.cpp
|
||||
#include "NativeInstrumentEngine.h"
|
||||
|
||||
#include <fluidsynth.h>
|
||||
#include <sfizz.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 1. SOUNDFONT ENGINE (.SF2 / .SF3) VIA FLUIDSYNTH C API
|
||||
// -----------------------------------------------------------------
|
||||
FluidSynthInstrument::FluidSynthInstrument()
|
||||
: settings(nullptr), synth(nullptr), sfontId(-1) {}
|
||||
|
||||
FluidSynthInstrument::~FluidSynthInstrument() {
|
||||
if (synth) delete_fluid_synth(synth);
|
||||
if (settings) delete_fluid_settings(settings);
|
||||
}
|
||||
|
||||
bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sampleRate) {
|
||||
if (synth) { delete_fluid_synth(synth); synth = nullptr; }
|
||||
if (settings) { delete_fluid_settings(settings); settings = nullptr; }
|
||||
settings = new_fluid_settings();
|
||||
fluid_settings_setnum(settings, "synth.sample-rate", sampleRate);
|
||||
fluid_settings_setint(settings, "synth.polyphony", 256);
|
||||
fluid_settings_setint(settings, "synth.verbose", 0);
|
||||
synth = new_fluid_synth(settings);
|
||||
if (!synth) return false;
|
||||
sfontId = fluid_synth_sfload(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) {
|
||||
fluid_synth_program_select(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) return;
|
||||
fluid_synth_bank_select(synth, channel, bank);
|
||||
fluid_synth_program_change(synth, channel, 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(synth, channel, pitch, velInt);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
|
||||
if (!synth) return;
|
||||
fluid_synth_noteoff(synth, channel, pitch);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
|
||||
if (!synth) return;
|
||||
fluid_synth_cc(synth, channel, cc, value);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
if (!synth) return;
|
||||
fluid_synth_program_change(synth, 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(synth, channel, bend14);
|
||||
}
|
||||
|
||||
bool FluidSynthInstrument::openGUI(void* parentWindowHandle) {
|
||||
return false; // SoundFont uses Web GUI Manager / Reskinned Knobs
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::closeGUI() {}
|
||||
|
||||
void FluidSynthInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
if (!synth) return;
|
||||
fluid_synth_write_float(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) {
|
||||
// ponytail: non-delayed cc() is stable across sfizz versions; switch to
|
||||
// hdCC(cc, value) for sample-accurate CC when the installed sfizz has it.
|
||||
sfizzSynth.cc(static_cast<int>(cc), static_cast<float>(value));
|
||||
}
|
||||
|
||||
void SfizzInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
// TODO(A12): sfizz program-change API differs by version (hdProgramChange
|
||||
// in >=0.6). Rarely used by SFZ instruments — no-op until verified.
|
||||
}
|
||||
|
||||
void SfizzInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
sfizzSynth.pitchWheel(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);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 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>();
|
||||
// ponytail: VST3/VST2 host needs vst3sdk + Steinberg APIs — wired in
|
||||
// Giai doan 2 (A6-A8); returns nullptr so the bridge degrades gracefully.
|
||||
case InstrumentType::VST3:
|
||||
case InstrumentType::VST2:
|
||||
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;
|
||||
if (!inst->init(sampleRate, blockSize)) 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);
|
||||
if (!loaded) return false;
|
||||
// Replacing an existing instrument drops its voices with the old engine.
|
||||
channels_[channel] = std::move(inst);
|
||||
return true;
|
||||
}
|
||||
|
||||
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
||||
auto it = channels_.find(channel);
|
||||
return it == channels_.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
void InstrumentEngineManager::allNotesOff() {
|
||||
for (auto& [ch, inst] : channels_) {
|
||||
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
std::memset(outputL, 0, numSamples * sizeof(float));
|
||||
std::memset(outputR, 0, numSamples * sizeof(float));
|
||||
if (channels_.empty()) return;
|
||||
if (scratchL_.size() < numSamples) {
|
||||
scratchL_.resize(numSamples);
|
||||
scratchR_.resize(numSamples);
|
||||
}
|
||||
for (auto& [ch, inst] : channels_) {
|
||||
std::memset(scratchL_.data(), 0, numSamples * sizeof(float));
|
||||
std::memset(scratchR_.data(), 0, numSamples * sizeof(float));
|
||||
inst->processAudioBlock(scratchL_.data(), scratchR_.data(), numSamples);
|
||||
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||
outputL[i] += scratchL_[i];
|
||||
outputR[i] += scratchR_[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user