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:
locpham
2026-08-11 23:37:29 +07:00
parent aae0b05473
commit c3368d0a91
37 changed files with 2970 additions and 80 deletions
+59
View File
@@ -0,0 +1,59 @@
cmake_minimum_required(VERSION 3.20)
project(daw_vst_bridge LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/CMakeLists.txt")
add_subdirectory(vst3sdk EXCLUDE_FROM_ALL)
set(VST3_SDK_TARGET sdk)
else()
message(WARNING "vst3sdk submodule missing — VST3 host disabled; SF2/SF3/SFZ still build")
set(VST3_SDK_TARGET "")
endif()
# ── 2. FluidSynth + sfizz ──
# Windows: vcpkg toolchain (-DCMAKE_TOOLCHAIN_FILE=.../vcpkg.cmake) cung cap
# headers/libs; pkg_check_modules khong co san tren MSVC.
# Linux/macOS: pkg-config duoc dung (spec goc).
if(WIN32)
find_path(FLUIDSYNTH_INCLUDE_DIR fluidsynth.h)
find_library(FLUIDSYNTH_LIBRARY NAMES fluidsynth libfluidsynth)
if(NOT FLUIDSYNTH_INCLUDE_DIR OR NOT FLUIDSYNTH_LIBRARY)
message(FATAL_ERROR "fluidsynth not found — cai qua vcpkg: vcpkg install fluidsynth")
endif()
# sfizz: header sfizz.hpp + lib sfizz (vcpkg export target sfizz::sfizz neu co)
find_path(SFIZZ_INCLUDE_DIR sfizz.hpp)
find_library(SFIZZ_LIBRARY NAMES sfizz)
if(NOT SFIZZ_INCLUDE_DIR OR NOT SFIZZ_LIBRARY)
message(FATAL_ERROR "sfizz not found — cai qua vcpkg: vcpkg install sfizz")
endif()
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(FLUIDSYNTH REQUIRED fluidsynth)
pkg_check_modules(SFIZZ REQUIRED sfizz)
endif()
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/include
${FLUIDSYNTH_INCLUDE_DIRS}
${SFIZZ_INCLUDE_DIRS}
)
add_executable(daw_vst_bridge
src/main.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
)
if(VST3_SDK_TARGET)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET})
endif()
if(WIN32)
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
# Required Windows libs
target_link_libraries(daw_vst_bridge PRIVATE winmm)
else()
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
endif()
+42
View File
@@ -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
+48
View File
@@ -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;
};
+43
View File
@@ -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
@@ -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];
}
}
}
+108
View File
@@ -0,0 +1,108 @@
// native_bridge/src/SharedMemoryIPC.cpp
#include "SharedMemoryIPC.h"
#include <cstring>
#include <string>
// ── Platform shared-memory helpers (used by main.cpp on Windows and by the
// self-check test on POSIX). The DAW (Rust) creates the mapping; the bridge
// opens it. Layout must match the Rust struct (see src-tauri/src/shm.rs).
#ifdef _WIN32
#include <windows.h>
struct ShmHandle {
HANDLE map = nullptr;
void* view = nullptr;
};
ShmHandle* shm_open(const char* name) {
ShmHandle* h = new ShmHandle();
h->map = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, name);
if (!h->map) { delete h; return nullptr; }
h->view = MapViewOfFile(h->map, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
return h;
}
void shm_close(ShmHandle* h) {
if (!h) return;
if (h->view) UnmapViewOfFile(h->view);
if (h->map) CloseHandle(h->map);
delete h;
}
#else
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
struct ShmHandle {
int fd = -1;
void* view = nullptr;
std::string name;
};
ShmHandle* shm_open(const char* name) {
size_t sz = sizeof(SharedAudioBufferIPC);
int fd = ::shm_open(name, O_CREAT | O_RDWR, 0666);
if (fd < 0) return nullptr;
if (ftruncate(fd, (off_t)sz) != 0) { ::close(fd); return nullptr; }
void* view = mmap(nullptr, sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (view == MAP_FAILED) { ::close(fd); return nullptr; }
ShmHandle* h = new ShmHandle();
h->fd = fd;
h->view = view;
h->name = name;
return h;
}
void shm_close(ShmHandle* h) {
if (!h) return;
if (h->view) munmap(h->view, sizeof(SharedAudioBufferIPC));
if (h->fd >= 0) ::close(h->fd);
shm_unlink(h->name.c_str());
delete h;
}
#endif
ShmHandle* shm_open_default() { return shm_open("SonicForge_DAW_IPC"); }
SharedAudioBufferIPC* shm_ptr(ShmHandle* h) {
return h ? static_cast<SharedAudioBufferIPC*>(h->view) : nullptr;
}
bool shm_write_midi(ShmHandle* h, uint8_t cmd, uint8_t channel, uint8_t pitch,
uint8_t velocity, uint32_t sampleOffset, uint8_t data2 = 0, uint8_t data3 = 0) {
SharedAudioBufferIPC* ipc = shm_ptr(h);
if (!ipc) return false;
// Ring overwrite guard: keep at most queue capacity pending events.
if (ipc->midiQueueCount >= 64) return false;
uint32_t i = ipc->midiQueueCount++;
SharedAudioBufferIPC::MidiEventIPC& e = ipc->midiQueue[i];
e.command = cmd;
e.channel = channel;
e.pitch = pitch;
e.velocity = velocity;
e.data2 = data2;
e.data3 = data3;
e.sampleOffset = sampleOffset;
return true;
}
bool shm_write_control(ShmHandle* h, uint32_t type, uint32_t arg0, uint32_t arg1,
uint32_t channel, const char* path) {
SharedAudioBufferIPC* ipc = shm_ptr(h);
if (!ipc) return false;
if (ipc->controlQueueCount >= 8) return false;
uint32_t i = ipc->controlQueueCount++;
SharedAudioBufferIPC::ControlEventIPC& c = ipc->controlQueue[i];
c.type = type;
c.arg0 = arg0;
c.channel = channel;
std::memset(c.arg2, 0, sizeof(c.arg2));
if (path && type == 2 /*LOAD*/) {
c.arg1 = (uint32_t)std::strlen(path); // path length in arg1
if (c.arg1 < sizeof(c.arg2)) std::memcpy(c.arg2, path, c.arg1);
} else {
c.arg1 = arg1; // TRANSPORT playhead / OPEN_GUI hwnd etc.
}
return true;
}
+199
View File
@@ -0,0 +1,199 @@
// native_bridge/src/Vst3Instrument.cpp
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
//
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
// method is a no-op so the bridge still builds for SF2/SF3/SFZ only.
//
// With the SDK: load the .vst3 module, create the component + edit controller,
// connect them, process MIDI events + audio blocks, and attach the editor
// view to a native HWND (openGUI) for the floating GUI (B9).
#include "Vst3Instrument.h"
#ifdef HAVE_VST3SDK
#include "public.sdk/source/main/pluginfactory.h"
#include "pluginterfaces/base/ibstream.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstmidicontrollers.h"
#include "pluginterfaces/gui/iplugview.h"
#include "public.sdk/source/common/pluginview.h"
#include <cstring>
#include <vector>
#endif
// ---------------------------------------------------------------------------
// With-SDK implementation
// ---------------------------------------------------------------------------
#ifdef HAVE_VST3SDK
#include "public.sdk/source/main/module.h"
namespace {
using Steinberg::Vst::IComponent;
using Steinberg::Vst::IEditController;
using Steinberg::Vst::IAudioProcessor;
using Steinberg::Vst::IComponentHandler;
using Steinberg::Vst::IParameterChanges;
using Steinberg::Vst::IEventList;
using Steinberg::Vst::Event;
using Steinberg::Vst::NoteOnEvent;
using Steinberg::Vst::NoteOffEvent;
using Steinberg::Vst::DataEvent;
using Steinberg::Vst::kMidiCC;
using Steinberg::Vst::kMidiPitchBend;
using Steinberg::Vst::kMidiProgramChange;
using Steinberg::IPlugView;
using Steinberg::tresult;
using Steinberg::kResultOk;
using Steinberg::kResultFalse;
// Minimal IComponentHandler so the plugin can inform the host of param edits.
class HostComponentHandler : public IComponentHandler {
public:
Steinberg::tresult queryInterface(const Steinberg::TUID&, void** v) override {
*v = nullptr;
return Steinberg::kNoInterface;
}
Steinberg::uint32 addRef() override { return 1; }
Steinberg::uint32 release() override { return 1; }
Steinberg::tresult beginEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
Steinberg::tresult performEdit(Steinberg::Vst::ParamID, Steinberg::Vst::ParamValue) override {
return Steinberg::kResultOk;
}
Steinberg::tresult endEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
Steinberg::tresult restartComponent(Steinberg::int32) override { return Steinberg::kResultOk; }
};
// Bundle both components in one factory entry so module.load() gives us the
// component; controller is created via IComponent::createController.
} // namespace
#endif
Vst3Instrument::Vst3Instrument()
: module_(nullptr),
processor_(nullptr),
controller_(nullptr),
view_(nullptr),
sampleRate_(44100.0),
maxBlockSize_(256),
loaded_(false),
guiAttached_(false) {}
Vst3Instrument::~Vst3Instrument() { closeGUI(); }
bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
#ifndef HAVE_VST3SDK
(void)path; (void)sampleRate;
return false; // vst3sdk submodule missing — VST3 disabled
#else
if (loaded_) return true;
// ponytail: vst3sdk module loading is platform-specific
// (Module::create on Windows .vst3 bundle / macOS .vst3 framework). Keep
// the classic pluginfactory-based load for Windows bundles:
// void* handle = Steinberg::Vst::Module::create(...)
// and use module->getFactory().createInstance<...>(cid).
// Verified on Windows in A6 (G3 Vital.vst3). Until then VST3 returns false
// so SF2/SFZ keep working.
(void)path; (void)sampleRate;
return false;
#endif
}
bool Vst3Instrument::init(double sampleRate, uint32_t maxBlockSize) {
sampleRate_ = sampleRate;
maxBlockSize_ = maxBlockSize;
return loaded_;
}
void Vst3Instrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
(void)channel; (void)bank; (void)program;
// ponytail: needs IEditController::setParamNormalized on program list params
}
void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
(void)channel; (void)pitch; (void)velocity; (void)sampleOffset;
#ifndef HAVE_VST3SDK
return;
#else
if (!processor_) return;
// TODO(A6): queue NoteOnEvent into the per-block event list; see
// processAudioBlock. Wiring requires IAudioProcessor::process with
// ProcessData — implemented together with loadPlugin on Windows.
#endif
}
void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
(void)channel; (void)pitch; (void)sampleOffset;
#ifndef HAVE_VST3SDK
return;
#else
if (!processor_) return;
// TODO(A6): queue NoteOffEvent (see noteOn)
#endif
}
void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
(void)channel; (void)cc; (void)value;
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): controller_->setParamNormalized(kMidiCC | cc, value/127.0)
#endif
}
void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
(void)channel; (void)program;
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): setParamNormalized(kMidiProgramChange | program, ...)
#endif
}
void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
(void)channel; (void)bend14;
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): setParamNormalized(kMidiPitchBend, bend14/16383.0)
#endif
}
bool Vst3Instrument::openGUI(void* parentWindowHandle) {
#ifndef HAVE_VST3SDK
(void)parentWindowHandle;
return false;
#else
if (!processor_ || !controller_ || !parentWindowHandle) return false;
if (guiAttached_) return true;
// TODO(A6): FUnknownPtr<IPlugView> view(controller_);
// view->setFrame(parentWindowHandle); view->attached(...); view_ = view;
return false;
#endif
}
void Vst3Instrument::closeGUI() {
#ifndef HAVE_VST3SDK
return;
#else
if (view_ && guiAttached_) {
// view_->removed(); view_->setFrame(nullptr);
}
view_ = nullptr;
guiAttached_ = false;
#endif
}
void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
(void)outputL; (void)outputR; (void)numSamples;
#ifndef HAVE_VST3SDK
return;
#else
// TODO(A6): ProcessData with 2 output buffers + event list; called by
// InstrumentEngineManager::renderAll via processAudioBlock.
#endif
}
+204
View File
@@ -0,0 +1,204 @@
// native_bridge/src/main.cpp
// Entry point of daw_vst_bridge.exe — opens shared memory created by the DAW
// (Rust/Tauri side), runs the real-time MIDI->audio loop, watches the parent.
#include "INativeInstrument.h"
#include "SharedMemoryIPC.h"
#include "NativeInstrumentEngine.h"
#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
#include <process.h>
#else
#include <unistd.h>
#include <cstdlib>
#include <thread>
#include <chrono>
#endif
#include <algorithm>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
// --- platform helpers -------------------------------------------------------
static bool parent_alive(uint32_t pid) {
#ifdef _WIN32
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!h) return false;
CloseHandle(h);
return true;
#else
return pid == 0 || (kill(pid, 0) == 0);
#endif
}
static void sleep_ms(uint32_t ms) {
#ifdef _WIN32
Sleep(ms);
#else
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
#endif
}
int main(int argc, char* argv[]) {
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
// 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
std::string shmName = "SonicForge_DAW_IPC";
for (int i = 1; i + 1 < argc; ++i) {
if (std::strcmp(argv[i], "--shm") == 0) shmName = argv[i + 1];
}
if (const char* e = std::getenv("SF_SHM_NAME")) shmName = e;
// Parent watchdog PID (set by Tauri sidecar spawner)
uint32_t parentPid = 0;
if (const char* e = std::getenv("SF_PARENT_PID")) parentPid = (uint32_t)std::atoi(e);
#ifdef _WIN32
// Real-time-ish timing: 1ms scheduler resolution
timeBeginPeriod(1);
HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName.c_str());
if (!hMapFile) {
std::cerr << "[NativeBridge] Failed to open Shared Memory mapping: " << shmName << std::endl;
return 1;
}
auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
if (!shmIPC) { CloseHandle(hMapFile); return 1; }
#else
(void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows
auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC));
if (!shmIPC) return 1;
#endif
InstrumentEngineManager instruments;
// B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).
// Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE
// is accepted but must match, otherwise warned and ignored.
double sampleRate = 44100.0;
if (const char* e = std::getenv("SF_SAMPLE_RATE")) {
double sr = (double)std::atoi(e);
if (sr > 0) sampleRate = sr;
}
if (const char* e = std::getenv("SF_BLOCK_SIZE")) {
uint32_t b = (uint32_t)std::atoi(e);
if (b != AUDIO_BLOCK_SIZE)
std::cerr << "[NativeBridge] SHM block size fixed at " << AUDIO_BLOCK_SIZE
<< " (SF_BLOCK_SIZE=" << b << " ignored)" << std::endl;
}
const uint32_t block = AUDIO_BLOCK_SIZE;
uint64_t playheadSamples = 0;
auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) {
auto* inst = instruments.get(evt.channel);
if (!inst) return; // channel chưa gán instrument → silent (A10)
switch (evt.command) {
case 0x9:
if (evt.velocity > 0)
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, evt.sampleOffset);
else
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
break;
case 0x8:
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
break;
case 0xB: // CC: controller number in pitch, value in data2
inst->controlChange(evt.channel, evt.pitch, evt.data2);
break;
case 0xC: // program change: program in data2
inst->programChange(evt.channel, evt.data2);
break;
case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB
inst->pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7));
break;
default: break;
}
};
// Render only [from, to) of the block — used by sample-accurate splitting.
auto renderSegment = [&](uint32_t from, uint32_t to) {
if (to <= from) return;
instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from);
};
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
while (true) {
// A. Control events — non-rt safe, drained first
for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) {
const auto& c = shmIPC->controlQueue[i];
if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel)
// Robust path length: do not trust arg1 (Rust passes 0 for LOAD).
size_t plen = 0;
while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen;
std::string path(c.arg2, plen);
InstrumentType t = (InstrumentType)c.arg0;
uint32_t ch = c.channel & 0xF;
if (instruments.assign(ch, t, path, sampleRate, block)) {
std::cout << "[NativeBridge] instrument loaded ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
} else {
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
}
} else if (c.type == 1) { // PANIC
instruments.allNotesOff();
std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
} else if (c.type == 3) { // TRANSPORT (A13)
if (c.arg0 == 0) { // STOP → flush every note immediately
instruments.allNotesOff();
std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl;
} else if (c.arg0 == 1) { // PLAY
playheadSamples = c.arg1;
std::cout << "[NativeBridge] transport PLAY playhead=" << playheadSamples << std::endl;
} else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
playheadSamples = c.arg1;
}
}
}
shmIPC->controlQueueCount = 0;
// B. Snapshot queued MIDI events (bounded copy, queue reset immediately)
uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount;
SharedAudioBufferIPC::MidiEventIPC evts[64];
for (uint32_t i = 0; i < nEvents; ++i) evts[i] = shmIPC->midiQueue[i];
shmIPC->midiQueueCount = 0;
// A11 sample-accurate: sort by sampleOffset, dispatch at each boundary,
// render the sub-block before the boundary. Events with offset 0 (live
// keyboard, current JS) are dispatched first and shape the whole block.
std::stable_sort(evts, evts + nEvents,
[](const SharedAudioBufferIPC::MidiEventIPC& a,
const SharedAudioBufferIPC::MidiEventIPC& b) {
return a.sampleOffset < b.sampleOffset;
});
uint32_t cursor = 0;
uint32_t ei = 0;
while (ei < nEvents && evts[ei].sampleOffset <= cursor) { dispatch(evts[ei]); ++ei; }
for (; ei < nEvents; ++ei) {
uint32_t off = evts[ei].sampleOffset < block ? evts[ei].sampleOffset : block;
if (off > cursor) { renderSegment(cursor, off); cursor = off; }
dispatch(evts[ei]);
}
if (cursor < block) renderSegment(cursor, block);
shmIPC->bridgeWriteIndex++;
// D. Parent died / window closed -> exit (no orphan process)
if (parentPid != 0 && !parent_alive(parentPid)) {
std::cout << "[NativeBridge] parent gone — exiting." << std::endl;
break;
}
// E. Sleep briefly for the next frame tick (block@44100 ≈ 5.8ms)
sleep_ms(1);
}
#ifdef _WIN32
UnmapViewOfFile(shmIPC);
CloseHandle(hMapFile);
timeEndPeriod(1);
#else
std::free(shmIPC);
#endif
return 0;
}
+67
View File
@@ -0,0 +1,67 @@
// native_bridge/tests/shm_selfcheck.cpp
// Self-check for SharedMemoryIPC helpers (assert-based, no framework).
// POSIX path runs on Linux/macOS; Win32 path guarded (runs on Windows build).
#include "../include/SharedMemoryIPC.h"
#include "../src/SharedMemoryIPC.cpp"
#include <cassert>
#include <cstdio>
#include <cstring>
int main() {
// Use a test-only name to avoid clashing with a live DAW bridge.
ShmHandle* h = shm_open("SonicForge_SelfCheck_IPC");
assert(h != nullptr);
SharedAudioBufferIPC* ipc = shm_ptr(h);
assert(ipc != nullptr);
// Reset state (POSIX shm may survive an aborted previous run).
std::memset(ipc, 0, sizeof(SharedAudioBufferIPC));
// 1. MIDI event write + read back (A12: data2/data3 for CC/program/pitch bend)
assert(shm_write_midi(h, 0x9, 2, 60, 100, 37));
assert(ipc->midiQueueCount == 1);
assert(ipc->midiQueue[0].command == 0x9);
assert(ipc->midiQueue[0].channel == 2);
assert(ipc->midiQueue[0].pitch == 60);
assert(ipc->midiQueue[0].velocity == 100);
assert(ipc->midiQueue[0].sampleOffset == 37);
assert(ipc->midiQueue[0].data2 == 0 && ipc->midiQueue[0].data3 == 0);
assert(shm_write_midi(h, 0xB, 3, 64, 0, 0, 127)); // CC64 sustain = 127
assert(shm_write_midi(h, 0xE, 3, 0, 0, 0, 0x00, 0x40)); // pitch bend MSB
assert(ipc->midiQueue[1].data2 == 127);
assert(ipc->midiQueue[2].data3 == 0x40);
assert(ipc->midiQueueCount == 3);
// 2. Control event with path + channel (A10)
assert(shm_write_control(h, 2, 1 /*SOUNDFONT_SF2_SF3*/, 0, 4, "C:\\sf\\SGM.sf2"));
assert(ipc->controlQueueCount == 1);
assert(ipc->controlQueue[0].type == 2);
assert(ipc->controlQueue[0].arg0 == 1);
assert(ipc->controlQueue[0].channel == 4);
assert(std::strcmp(ipc->controlQueue[0].arg2, "C:\\sf\\SGM.sf2") == 0);
// TRANSPORT (A13): STOP with playhead arg
assert(shm_write_control(h, 3, 0, 44100, 0, ""));
assert(ipc->controlQueue[1].type == 3 && ipc->controlQueue[1].arg0 == 0
&& ipc->controlQueue[1].arg1 == 44100);
// 3. Queue capacity guard
for (int i = 0; i < 70; ++i) shm_write_midi(h, 0x8, 0, 40, 0, 0);
assert(ipc->midiQueueCount == 64); // capped, not overflowed
// 4. Audio block round-trip
for (uint32_t i = 0; i < AUDIO_BLOCK_SIZE; ++i) ipc->masterLeft[i] = (float)i;
ipc->bridgeWriteIndex++;
ipc->blockTimestamp = 123456;
assert(ipc->masterLeft[255] == 255.0f);
assert(ipc->bridgeWriteIndex == 1);
assert(ipc->blockTimestamp == 123456);
// 5. Layout stability — MUST match Rust src-tauri/src/shm.rs
assert(sizeof(SharedAudioBufferIPC::MidiEventIPC) == 12);
assert(sizeof(SharedAudioBufferIPC::ControlEventIPC) == 1040);
std::printf("SHM self-check OK (sizeof struct = %zu bytes)\n", sizeof(SharedAudioBufferIPC));
shm_close(h);
return 0;
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "daw-vst-bridge",
"version-string": "1.0.0",
"dependencies": [
"fluidsynth",
"sfizz",
"pkgconf"
]
}