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,42 @@
|
||||
// native_bridge/include/INativeInstrument.h
|
||||
#ifndef I_NATIVE_INSTRUMENT_H
|
||||
#define I_NATIVE_INSTRUMENT_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
enum class InstrumentType {
|
||||
VST3,
|
||||
VST2,
|
||||
SOUNDFONT_SF2_SF3,
|
||||
SFZ
|
||||
};
|
||||
|
||||
class INativeInstrument {
|
||||
public:
|
||||
virtual ~INativeInstrument() = default;
|
||||
|
||||
// Initialize Engine with Sample Rate and Buffer Size
|
||||
virtual bool init(double sampleRate, uint32_t maxBlockSize) = 0;
|
||||
|
||||
// Select Bank and Program Change
|
||||
virtual void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) = 0;
|
||||
|
||||
// Dispatch MIDI Note On / Note Off events
|
||||
virtual void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) = 0;
|
||||
virtual void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) = 0;
|
||||
|
||||
// MIDI continuous controllers (A12): CC value, program change, 14-bit pitch bend
|
||||
virtual void controlChange(uint32_t channel, uint32_t cc, uint32_t value) = 0;
|
||||
virtual void programChange(uint32_t channel, uint32_t program) = 0;
|
||||
virtual void pitchBend(uint32_t channel, uint32_t bend14) = 0;
|
||||
|
||||
// Open/close Native GUI window
|
||||
virtual bool openGUI(void* parentWindowHandle) = 0;
|
||||
virtual void closeGUI() = 0;
|
||||
|
||||
// Real-time Audio PCM Float32 rendering loop
|
||||
virtual void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) = 0;
|
||||
};
|
||||
|
||||
#endif // I_NATIVE_INSTRUMENT_H
|
||||
@@ -0,0 +1,87 @@
|
||||
// native_bridge/include/NativeInstrumentEngine.h
|
||||
#ifndef NATIVE_INSTRUMENT_ENGINE_H
|
||||
#define NATIVE_INSTRUMENT_ENGINE_H
|
||||
|
||||
#include "INativeInstrument.h"
|
||||
|
||||
#include <sfizz.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 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;
|
||||
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
||||
|
||||
private:
|
||||
void* settings; // fluid_settings_t*
|
||||
void* synth; // fluid_synth_t*
|
||||
int sfontId;
|
||||
};
|
||||
|
||||
// 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:
|
||||
sfizz::Synth 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);
|
||||
|
||||
// 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 { return channels_.size(); }
|
||||
|
||||
private:
|
||||
static std::unique_ptr<INativeInstrument> create_instrument(InstrumentType type);
|
||||
|
||||
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
|
||||
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
|
||||
std::vector<float> scratchL_, scratchR_;
|
||||
};
|
||||
|
||||
#endif // NATIVE_INSTRUMENT_ENGINE_H
|
||||
@@ -0,0 +1,48 @@
|
||||
// native_bridge/include/SharedMemoryIPC.h
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#define AUDIO_BLOCK_SIZE 256
|
||||
|
||||
// WARNING: volatile is NOT a sync primitive. Real impl should use an
|
||||
// interlocked/index-flag pair or a Win32 event (SetEvent) signalled by the
|
||||
// writer. This struct follows the spec layout so Rust/JS mapping stays in sync.
|
||||
struct SharedAudioBufferIPC {
|
||||
// Synchronization Flags
|
||||
volatile uint32_t clientReadIndex;
|
||||
volatile uint32_t bridgeWriteIndex;
|
||||
|
||||
// PCM Float32 Audio Buffers
|
||||
float masterLeft[AUDIO_BLOCK_SIZE];
|
||||
float masterRight[AUDIO_BLOCK_SIZE];
|
||||
|
||||
// Latency probe: bridge stamps every rendered block (QueryPerformanceCounter)
|
||||
volatile uint64_t blockTimestamp;
|
||||
|
||||
// MIDI Event Exchange Queue
|
||||
struct MidiEventIPC {
|
||||
uint8_t command; // 0x9 note on / 0x8 note off / 0xB CC / 0xC program / 0xE pitch bend
|
||||
uint8_t channel;
|
||||
uint8_t pitch; // note number (0x9/0x8) or CC number (0xB)
|
||||
uint8_t velocity; // 0-127 (0x9 + vel=0 == note off)
|
||||
uint8_t data2; // CC value / program number / PB LSB
|
||||
uint8_t data3; // PB MSB (0xE only)
|
||||
uint8_t reserved[2]; // explicit padding — keeps C/Rust layout stable
|
||||
uint32_t sampleOffset;
|
||||
} midiQueue[64];
|
||||
|
||||
volatile uint32_t midiQueueCount;
|
||||
|
||||
// Transport / Control (added vs spec: Stop/Play flush, instrument switch)
|
||||
struct ControlEventIPC {
|
||||
uint32_t type; // 0 = NONE, 1 = PANIC/ALL_NOTES_OFF, 2 = LOAD_INSTRUMENT,
|
||||
// 3 = TRANSPORT, 4 = OPEN_GUI
|
||||
uint32_t arg0; // LOAD: instrument type (InstrumentType);
|
||||
// TRANSPORT: 0=STOP, 1=PLAY, 2=SET_POSITION
|
||||
uint32_t arg1; // LOAD: string length (bytes) of path;
|
||||
// TRANSPORT: playheadSamples (for timeline sync)
|
||||
uint32_t channel; // LOAD: MIDI channel to assign instrument to (A10)
|
||||
char arg2[1024]; // LOAD: UTF-8 path
|
||||
} controlQueue[8];
|
||||
volatile uint32_t controlQueueCount;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// native_bridge/include/Vst3Instrument.h
|
||||
// VST3 host instrument (A7). Compiled ALWAYS; the real vst3sdk wiring is
|
||||
// inside #ifdef HAVE_VST3SDK (set by CMake when the vst3sdk submodule is
|
||||
// present). Without the SDK the class degrades to a no-op stub so the bridge
|
||||
// still builds for SF2/SF3/SFZ.
|
||||
#ifndef VST3_INSTRUMENT_H
|
||||
#define VST3_INSTRUMENT_H
|
||||
|
||||
#include "INativeInstrument.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class Vst3Instrument : public INativeInstrument {
|
||||
public:
|
||||
Vst3Instrument();
|
||||
~Vst3Instrument() override;
|
||||
|
||||
bool loadPlugin(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:
|
||||
void* module_; // Steinberg::IPluginFactory* (owned by module)
|
||||
void* processor_; // Steinberg::Vst::IComponent*
|
||||
void* controller_; // Steinberg::Vst::IEditController*
|
||||
void* view_; // Steinberg::IPlugView*
|
||||
std::string path_;
|
||||
double sampleRate_;
|
||||
uint32_t maxBlockSize_;
|
||||
bool loaded_;
|
||||
bool guiAttached_;
|
||||
};
|
||||
|
||||
#endif // VST3_INSTRUMENT_H
|
||||
Reference in New Issue
Block a user