// 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 #include #include #include #else #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include // --- 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 } // 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 = DefWindowProcA; 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 CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); #endif std::unique_lock 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 CoUninitialize(); #endif }); } ~ChannelWorker() { { std::lock_guard lk(mu_); stop_ = true; } cv_.notify_all(); if (th_.joinable()) th_.join(); } void post(std::function job) { { std::lock_guard lk(mu_); jobs_.push_back(std::move(job)); } cv_.notify_all(); } private: std::thread th_; std::mutex mu_; std::condition_variable cv_; std::deque> 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 | 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; // 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> workers; // B9: native editor windows per channel — keep alive (HWND outlives the job). std::map guiWindows; // 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); }; // 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(); 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 // ponytail: per-plugin channel mapping chua co — gan GUI cho // instrument dau tien duoc load (smoke test = 1 instrument). // 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). // LOAD va OPEN_GUI duoc drain trong cung vong lap: LOAD post job // len worker (async, VST3 init co the mat giay) truoc khi type=4 // duoc xu ly — khong doi, instruments.get() con rong -> "no // instrument loaded". Poll toi da 10s cho LOAD hoan tat. uint32_t guiCh = 16; for (int tries = 0; tries < 200 && guiCh == 16; ++tries) { for (uint32_t ch = 0; ch < 16; ++ch) { if (instruments.get(ch)) { guiCh = ch; break; } } if (guiCh == 16) sleep_ms(50); } if (guiCh == 16) { std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1 << " plugin=" << c.arg2 << " (no instrument loaded)" << std::endl; } else { if (!workers[guiCh]) workers[guiCh] = std::make_unique(); workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() { 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) { // B9: bridge tự tạo native window — editor VST3 đính // vào đây; message pump bởi ChannelWorker idle loop. hwnd = create_native_vst_window(arg2.c_str()); if (!hwnd) { std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl; return; } guiWindows[guiCh] = hwnd; // keep window alive } #else (void)guiWindows; #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. 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; }