3f59c2c4c2
- NativeInstrumentEngine: track GM bank per channel (CC0/CC32), use bank in programChange - app.jsx: send CC0/CC32+PROGRAM before notes via __ensureBridgeProgram, dedupe, clear dedupe after async LOAD - audioRoutingEngine/bridgeAudioNode: idempotent connect (no disconnect-flush on re-connect), fixes note cut & multi-track stuck - main.cpp: remove 10s poll in OPEN_GUI control job (blocked realtime loop, watchdog race), VstWindowProc stores channel not inst pointer, cleanup gui maps on WM_DESTROY
452 lines
20 KiB
C++
452 lines
20 KiB
C++
// 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>
|
|
#include <thread>
|
|
#else
|
|
#include <unistd.h>
|
|
#include <cstdlib>
|
|
#include <thread>
|
|
#endif
|
|
|
|
#include <chrono>
|
|
#include <algorithm>
|
|
#include <condition_variable>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#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
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// Native VST editor windows registry — global de WM_DESTROY (chay tren worker
|
|
// thread cua channel tao window) co the don map. USERDATA luu channel+1 (KHONG
|
|
// luu con tro inst truc tiep: assign() thay inst moi moi lan load — con tro cu
|
|
// bi huy → WM_DESTROY tren con tro dangling → crash/hang bridge).
|
|
static InstrumentEngineManager* g_engine = nullptr;
|
|
static std::mutex g_guiMutex;
|
|
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
|
|
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
|
|
|
|
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
|
if (uMsg == WM_DESTROY) {
|
|
INativeInstrument* instToClose = nullptr;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_hwndToCh.find(hwnd);
|
|
if (it != g_hwndToCh.end()) {
|
|
uint32_t ch = it->second;
|
|
g_hwndToCh.erase(it);
|
|
g_guiWindows.erase(ch);
|
|
if (g_engine) instToClose = g_engine->get(ch);
|
|
}
|
|
}
|
|
if (instToClose) instToClose->closeGUI();
|
|
}
|
|
return DefWindowProcA(hwnd, uMsg, wParam, lParam);
|
|
}
|
|
#endif
|
|
|
|
// B9: native Win32 window for the VST editor (replaces the WebView2 surface —
|
|
// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the
|
|
// ChannelWorker thread so the worker's idle message pump services its messages.
|
|
static void* create_native_vst_window(const char* title) {
|
|
#ifdef _WIN32
|
|
static const char* kWndClass = "SonicForge_Native_VST3_Class";
|
|
static bool registered = false;
|
|
if (!registered) {
|
|
WNDCLASSA wc = {};
|
|
wc.lpfnWndProc = VstWindowProc;
|
|
wc.hInstance = GetModuleHandleA(nullptr);
|
|
wc.lpszClassName = kWndClass;
|
|
RegisterClassA(&wc);
|
|
registered = true;
|
|
}
|
|
HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST",
|
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
|
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
|
|
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
|
|
return (void*)hwnd;
|
|
#else
|
|
(void)title;
|
|
return nullptr;
|
|
#endif
|
|
}
|
|
|
|
// Per-channel persistent worker: ONE thread owns the COM STA apartment for that
|
|
// channel's instrument for its whole lifetime. loadPlugin and openGUI MUST run
|
|
// on the same thread — if the loading thread exits, its apartment dies and
|
|
// VST3 plugins that marshal internally (Nexus) hang forever in
|
|
// view->attached(). Verified with gui_probe: `bridge_like` (load thread exits,
|
|
// openGUI on another) hangs; `same_thread` (load+openGUI on one alive thread)
|
|
// returns attached=OK.
|
|
class ChannelWorker {
|
|
public:
|
|
ChannelWorker() {
|
|
th_ = std::thread([this] {
|
|
#ifdef _WIN32
|
|
OleInitialize(nullptr);
|
|
#endif
|
|
std::unique_lock<std::mutex> lk(mu_);
|
|
for (;;) {
|
|
if (stop_ && jobs_.empty()) break;
|
|
if (!jobs_.empty()) {
|
|
auto job = std::move(jobs_.front());
|
|
jobs_.pop_front();
|
|
lk.unlock();
|
|
job();
|
|
lk.lock();
|
|
continue;
|
|
}
|
|
cv_.wait_for(lk, std::chrono::milliseconds(5));
|
|
// Pump THIS thread's message queue while idle — VST editor
|
|
// windows are created on this thread, their messages must be
|
|
// dispatched here or the editor freezes after attach.
|
|
lk.unlock();
|
|
MSG msg;
|
|
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
lk.lock();
|
|
}
|
|
#ifdef _WIN32
|
|
OleUninitialize();
|
|
#endif
|
|
});
|
|
}
|
|
~ChannelWorker() {
|
|
{
|
|
std::lock_guard<std::mutex> lk(mu_);
|
|
stop_ = true;
|
|
}
|
|
cv_.notify_all();
|
|
if (th_.joinable()) th_.join();
|
|
}
|
|
void post(std::function<void()> job) {
|
|
{
|
|
std::lock_guard<std::mutex> lk(mu_);
|
|
jobs_.push_back(std::move(job));
|
|
}
|
|
cv_.notify_all();
|
|
}
|
|
|
|
private:
|
|
std::thread th_;
|
|
std::mutex mu_;
|
|
std::condition_variable cv_;
|
|
std::deque<std::function<void()>> jobs_;
|
|
bool stop_ = false;
|
|
};
|
|
|
|
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;
|
|
#ifdef _WIN32
|
|
g_engine = &instruments;
|
|
#endif
|
|
// Per-channel persistent workers: loadPlugin + openGUI run on the SAME
|
|
// thread whose COM STA apartment stays alive for the channel's lifetime
|
|
// (see ChannelWorker comment — a dead apartment hangs Nexus attached()).
|
|
std::map<uint32_t, std::unique_ptr<ChannelWorker>> workers;
|
|
// B9: native editor windows per channel — keep alive (HWND outlives the job).
|
|
// Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc.
|
|
// 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:
|
|
// sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay
|
|
// truoc segment chua no (A11 splitting), nen offset tuong doi la 0.
|
|
// Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre.
|
|
if (evt.velocity > 0)
|
|
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, 0);
|
|
else
|
|
inst->noteOff(evt.channel, evt.pitch, 0);
|
|
break;
|
|
case 0x8:
|
|
inst->noteOff(evt.channel, evt.pitch, 0);
|
|
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);
|
|
};
|
|
|
|
double blockDurationMs = (double)block / sampleRate * 1000.0;
|
|
auto startTime = std::chrono::steady_clock::now();
|
|
uint64_t blockCount = 0;
|
|
|
|
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
|
|
while (true) {
|
|
#ifdef _WIN32
|
|
// Message pump: VST editors (Nexus, JUCE-based...) block inside
|
|
// view->attached() until the host dispatches messages — openGUI runs
|
|
// on a worker thread, so THIS loop must pump concurrently (verified
|
|
// with gui_probe: worker-thread openGUI + concurrent pump → attached
|
|
// returns kResultOk; without it → hangs forever).
|
|
MSG msg;
|
|
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
#endif
|
|
// 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).
|
|
// Chay tren persistent worker thread cua channel: VST3 init
|
|
// (loadPlugin) co the mat giay — chay dong bo tren audio loop
|
|
// lam writeIndex stall > 3s -> Rust tuong bridge chet va restart
|
|
// nham (2 bridge cung map SHM -> race control queue / double
|
|
// load). assign() chi giu mutex khi ghi map nen renderAll khong
|
|
// bao gio stall. openGUI sau nay chay tren CUNG thread nay
|
|
// (ChannelWorker) — thread khong bao gio exit nen COM STA
|
|
// apartment cua plugin con song (xem ChannelWorker comment).
|
|
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 (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
|
|
workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
|
|
std::cerr << "[dbg] load thread start ch=" << ch
|
|
<< " type=" << (int)t << " path=" << path << std::endl;
|
|
bool ok = instruments.assign(ch, t, path, sampleRate, block);
|
|
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
|
|
if (ok) {
|
|
std::cout << "[NativeBridge] instrument loaded ch=" << ch
|
|
<< " type=" << (int)t << " " << path << std::endl;
|
|
} else {
|
|
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
|
|
<< " type=" << (int)t << " " << 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;
|
|
}
|
|
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
|
|
// Chay tren CUNG ChannelWorker da load instrument: thread rieng
|
|
// cho openGUI lai tao COM apartment moi, con plugin thi song o
|
|
// apartment cu da chet (load thread exit) -> Nexus attached()
|
|
// hang (gui_probe: bridge_like treo, same_thread OK).
|
|
uint32_t guiCh = c.channel;
|
|
if (guiCh >= 16) guiCh = 0;
|
|
// Bo poll 10s tren real-time loop (writeIndex stall > 3s -> Rust
|
|
// tuong bridge chet va restart -> 2 bridge cung map SHM -> race).
|
|
// LOAD (type=2) post truoc OPEN_GUI tren CUNG ChannelWorker (FIFO)
|
|
// -> job openGUI chay sau khi LOAD xong -> kiem tra inst trong job.
|
|
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
|
|
workers[guiCh]->post([&instruments, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() {
|
|
if (!instruments.get(guiCh)) {
|
|
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << arg1
|
|
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
|
|
return;
|
|
}
|
|
std::cerr << "[dbg] openGUI thread start hwnd=" << arg1
|
|
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
|
void* hwnd = (void*)(uintptr_t)arg1;
|
|
#ifdef _WIN32
|
|
if (arg1 == 0) {
|
|
HWND existingHwnd = nullptr;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_guiWindows.find(guiCh);
|
|
if (it != g_guiWindows.end()) existingHwnd = (HWND)it->second;
|
|
}
|
|
if (existingHwnd && IsWindow(existingHwnd)) {
|
|
hwnd = existingHwnd;
|
|
SetWindowTextA((HWND)hwnd, arg2.c_str());
|
|
ShowWindow((HWND)hwnd, SW_SHOW);
|
|
SetForegroundWindow((HWND)hwnd);
|
|
// Reuse: cap nhat USERDATA (channel+1) — inst CU
|
|
// da bi thay the boi assign() -> WM_DESTROY sau
|
|
// nay lookup inst MOI, khong dung con tro dangling.
|
|
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
|
|
} else {
|
|
hwnd = create_native_vst_window(arg2.c_str());
|
|
if (!hwnd) {
|
|
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
|
|
return;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_guiWindows[guiCh] = hwnd; // keep window alive
|
|
g_hwndToCh[(HWND)hwnd] = guiCh; // WM_DESTROY cleanup
|
|
}
|
|
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
|
|
}
|
|
}
|
|
#else
|
|
(void)0;
|
|
#endif
|
|
if (auto* inst = instruments.get(guiCh)) {
|
|
if (inst->openGUI(hwnd))
|
|
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
|
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
|
else
|
|
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
|
<< " plugin=" << arg2 << std::endl;
|
|
}
|
|
// Editor windows song tren thread nay — ChannelWorker
|
|
// pump message queue khi idle (xem class comment).
|
|
});
|
|
}
|
|
}
|
|
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. Synchronize with real-time audio playback
|
|
blockCount++;
|
|
auto targetTime = startTime + std::chrono::microseconds(static_cast<int64_t>(blockCount * blockDurationMs * 1000.0));
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (now < targetTime) {
|
|
auto diff = std::chrono::duration_cast<std::chrono::microseconds>(targetTime - now).count();
|
|
if (diff > 1000) {
|
|
sleep_ms(diff / 1000);
|
|
}
|
|
while (std::chrono::steady_clock::now() < targetTime) {
|
|
std::this_thread::yield();
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
UnmapViewOfFile(shmIPC);
|
|
CloseHandle(hMapFile);
|
|
timeEndPeriod(1);
|
|
#else
|
|
std::free(shmIPC);
|
|
#endif
|
|
return 0;
|
|
}
|