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,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()
|
||||
@@ -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
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "daw-vst-bridge",
|
||||
"version-string": "1.0.0",
|
||||
"dependencies": [
|
||||
"fluidsynth",
|
||||
"sfizz",
|
||||
"pkgconf"
|
||||
]
|
||||
}
|
||||
Submodule
+1
Submodule native_bridge/vst3sdk added at 3cdf9ca5d1
Reference in New Issue
Block a user