// native_bridge/src/NativeInstrumentEngine.cpp #include "NativeInstrumentEngine.h" #include #include #include "Vst3Instrument.h" // void* members keep fluid types out of the public header; cast here. #define FS_SYNTH (static_cast(synth)) #define FS_SETTINGS (static_cast(settings)) #include #include // ----------------------------------------------------------------- // 1. SOUNDFONT ENGINE (.SF2 / .SF3) VIA FLUIDSYNTH C API // ----------------------------------------------------------------- FluidSynthInstrument::FluidSynthInstrument() : settings(nullptr), synth(nullptr), sfontId(-1) {} 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) { 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) return; fluid_synth_bank_select(FS_SYNTH, channel, bank); fluid_synth_program_change(FS_SYNTH, channel, 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) return; fluid_synth_cc(FS_SYNTH, channel, cc, value); } void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) { if (!synth) return; fluid_synth_program_change(FS_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(FS_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(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) // ----------------------------------------------------------------- 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(); case InstrumentType::VST3: return std::make_unique(); // ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented. 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; 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); else if (type == InstrumentType::VST3) loaded = static_cast(inst.get())->loadPlugin(path, sampleRate); 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. { std::lock_guard lock(mu_); channels_[channel] = std::move(inst); } 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(); } void InstrumentEngineManager::allNotesOff() { std::lock_guard lock(mu_); 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::lock_guard lock(mu_); 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]; } } }