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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user