// native_bridge/include/NativeInstrumentEngine.h #ifndef NATIVE_INSTRUMENT_ENGINE_H #define NATIVE_INSTRUMENT_ENGINE_H #include "INativeInstrument.h" #include "StateStore.h" #include #include #include #include #include #include #include #include // FluidSynth (.sf2 / .sf3) class FluidSynthInstrument : public INativeInstrument { public: FluidSynthInstrument(); ~FluidSynthInstrument() override; bool loadSoundFontFile(const std::string& path, double sampleRate); bool init(double sampleRate, uint32_t maxBlockSize) override; void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override; void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override; void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override; void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override; void programChange(uint32_t channel, uint32_t program) override; void pitchBend(uint32_t channel, uint32_t bend14) override; bool openGUI(void* parentWindowHandle) override; void closeGUI() override; // G2.1: expose last bank/program for the state snapshot. uint32_t bankOf(uint32_t channel) const override; uint32_t programOf(uint32_t channel) const override; void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override; private: void* settings; // fluid_settings_t* void* synth; // fluid_synth_t* int sfontId; // Per-channel bank select (CC0<<7 | CC32) — programChange phai dung bank // that (truoc day hardcode bank 0 -> preset o bank != 0 khong chon duoc). uint32_t bank_[16]; uint32_t program_[16]; }; // sfizz (.sfz) class SfizzInstrument : public INativeInstrument { public: bool loadSfzFile(const std::string& path, double sampleRate); bool init(double sampleRate, uint32_t maxBlockSize) override; void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override; void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override; void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override; void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override; void programChange(uint32_t channel, uint32_t program) override; void pitchBend(uint32_t channel, uint32_t bend14) override; bool openGUI(void* parentWindowHandle) override; void closeGUI() override; void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override; private: sfz::Sfizz sfizzSynth; }; // Multi-instrument host (A10): one engine instance per MIDI channel, so two // tracks can play two different soundfonts/VSTs simultaneously. All channels // render into the same output block (mixed), keyed by evt.channel. class InstrumentEngineManager { public: // Create (or replace) the instrument on `channel` for `path`. // Returns false if the type is unsupported or the file fails to load. bool assign(uint32_t channel, InstrumentType type, const std::string& path, double sampleRate, uint32_t blockSize); INativeInstrument* get(uint32_t channel); // F-MEM-5 (audit 5.2): get() releases mu_ before the caller derefs, so a // worker thread's assign()/unload() can destroy the instance mid-use. // has() is a safe existence check; withInstrument() runs code against the // instance while mu_ is still held. Use these from non-Ui threads. bool has(uint32_t channel); template auto withInstrument(uint32_t channel, F&& fn) -> decltype(fn((INativeInstrument*)nullptr)) { std::lock_guard lock(mu_); auto it = channels_.find(channel); return fn(it == channels_.end() ? nullptr : it->second.get()); } // Real-time MIDI dispatch (audio loop thread). Each call holds mu_ for the // WHOLE call — lookup + reloading check + instrument call under one lock — // so a worker thread's assign()/unload() can never swap the map and destroy // the old instance while dispatch is inside a method on it (use-after-free // when loading a new VSTi while others play). Events for unassigned or // reloading channels are dropped (a mid-reload instance must not be touched // while its worker rebuilds state_ in place). void noteOn(uint32_t channel, uint32_t pitch, float velocity); void noteOff(uint32_t channel, uint32_t pitch); void controlChange(uint32_t channel, uint32_t cc, uint32_t value); void programChange(uint32_t channel, uint32_t program); void pitchBend(uint32_t channel, uint32_t bend14); // Remove and destroy the instrument on `channel` (its destructor may call // VST terminate — MUST run on the channel worker thread, caller's duty). // Used by Option B: two live instances of the same plugin DLL (Nexus) // hang createView on the second — unload the other channel's copy. void unload(uint32_t channel); // Path last assigned to a channel (empty if none). Windows paths are // case-insensitive — caller must lowercase before comparing. std::string pathOf(uint32_t channel); // G2.1: copy every assigned channel into a state snapshot (channel, // type, path, VST3 preset blob -> base64, SF2/SF3 bank/program). // MUST run on the channel worker (captureState enters the plugin); // takes mu_ for the whole call so the audio loop cannot be inside // process() on the same instance while its state is captured. // Channels mid-reload are skipped (their old instance is dying). bool snapshot(BridgeStateSnapshot& out); // Mark a channel as rebuilding its VST instance (GUI reopen): the // real-time loop then skips processAudioBlock for that channel so VST // teardown never races the audio thread. void setReloading(uint32_t channel, bool on); // True while a channel is being rebuilt (instance teardown/reload on its // worker thread): real-time code drops MIDI for it. Channel-level flag — // survives the instance swap (the per-instance flag dies with the object). bool isReloading(uint32_t channel) const; // G3.3: mark a channel whose plugin crashed inside processAudioBlock // (SEH access violation). The channel is muted (real-time dispatch + // renderAll skip it) until the next assign() — the plugin's state is // undefined after a fault, so re-processing would fault again. void markCrashed(uint32_t channel); bool isCrashed(uint32_t channel) const; // Editor-open predicate: while any VST editor is attached, channels whose // plugin DLL path has an open editor count as quiet (same-plugin channels // too -- the worker pumps the editor's window proc inside the DLL while the // audio loop process()es other instances of the same DLL). // Flush every sounding note on every assigned channel. void allNotesOff(); // Zero L/R then sum each assigned instrument into it. void renderAll(float* outputL, float* outputR, uint32_t numSamples); size_t count() const { std::lock_guard lock(mu_); return channels_.size(); } void setBypassed(bool on) { isBypassed_.store(on); } bool isBypassed() const { return isBypassed_.load(); } private: static std::unique_ptr create_instrument(InstrumentType type); // Probe-render 3 notes sau khi load, do peak, tinh makeup de note don ra // ~ -6 dBFS (0.5) - can bang am luong moi engine (VST2/VST3/SF2/SFZ). static float calibrate(INativeInstrument* inst, double sampleRate, uint32_t blockSize); mutable std::mutex mu_; std::map> channels_; std::map paths_; // last assigned path per channel std::map types_; // last assigned type per channel (G2.1) std::map makeup_; // calibrated per-channel output gain (renderAll ap dung) // Per-instrument scratch so engines that overwrite (not mix) stay additive. std::vector scratchL_, scratchR_; // Channel-level reload flag (see isReloading) — indexed by MIDI channel. bool reloadingCh_[16] = {}; // G3.3: channel muted after a plugin fault inside processAudioBlock. bool crashedCh_[16] = {}; float limiterGain_ = 1.0f; // smoothed mix limiter state (renderAll) // True when the channel's plugin DLL has an attached editor (or its reload // is in progress): real-time code drops MIDI and skips rendering. Lowercased // compare inside -- Windows plugin paths are case-insensitive. bool channelQuiet(uint32_t ch) const; std::atomic isBypassed_{false}; }; #endif // NATIVE_INSTRUMENT_ENGINE_H