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,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