// 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 #endif #include #include #include #include #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 } #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). class ChannelWorker; // fwd — WM_DESTROY posts closeGUI() to the channel worker static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker static std::string lower_plugin_path(uint32_t ch); // defined after ChannelWorker static void unmute_if_not_closing(uint32_t y, const char* why); // defined after ChannelWorker static InstrumentEngineManager* g_engine = nullptr; static std::mutex g_guiMutex; static std::map g_guiWindows; // channel -> HWND (keep window alive) static std::map g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup) static std::map>* g_workers = nullptr; // Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus) // crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/ // createView is running on some worker — a same-path close job must not unmute // its channel mid-attach (the attach job owns the silence state until // attachView finishes). g_closeInFlight = channels whose close job // (closeGUI + destroy children + reload) is still inside createInstance — // unmuting them then would race the plugin teardown. static std::mutex g_attachMutex; static std::vector g_attachPaths; static bool g_closeInFlight[16] = { false }; // Editor-open predicate state: while a channel's VST editor is attached, EVERY // channel assigned the same plugin DLL path stays quiet (the worker pumps the // editor's window proc inside the DLL while the audio loop process()es other // instances of the same DLL - 2 threads in one DLL crashes Nexus on GUI clicks). // Count keyed by lowercased plugin path. The predicate runs on the audio thread // under the engine's mu_ - lock order mu_ -> g_editorMutex (never inverted). static std::mutex g_editorMutex; static std::map g_editorPathCount; static std::string g_editorOpenPath[16]; static bool g_editorOpen[16] = { false }; // GUI attach state (CRASH FIX 0xc000041d): attachView MUST run on the MAIN // thread so the editor children created inside view->attached() are owned by // the same thread as the parent window (main pump). g_guiAttachPending = one // attach in flight per channel (dedupe OPEN_GUI); g_attachSilenced = the // same-path channels silenced by the worker job, restored by the kMsgAttach // handler after attach; g_editorDestroyAck = kMsgDestroy handshake for the // LOAD job (children are main-owned -> DestroyWindow must run on main). static bool g_guiAttachPending[16] = { false }; static std::map> g_attachSilenced; static bool g_editorDestroyAck[16] = { false }; static const UINT kMsgAttach = WM_APP + 1; static const UINT kMsgDestroy = WM_APP + 2; static std::mutex g_loadMutex; // moved up: VstWindowProc handlers need it static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_CLOSE) { uint32_t ch = UINT32_MAX; { std::lock_guard lock(g_guiMutex); auto it = g_hwndToCh.find(hwnd); if (it != g_hwndToCh.end()) ch = it->second; } if (ch != UINT32_MAX) { // CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION / // 0xc0000005 trong USER32, Event Log 9:47/10:20/10:22): // KHONG de DefWindowProc DestroyWindow o day. Plugin editor la // child cua window nay, tao TREN worker thread (view->attached() // chay trong attach job) - DestroyWindow tren main thread pha huy // child cross-thread -> crash USER32. Detach view tren worker // (closeGUI), an window, GIU window trong registry de reuse. post_close_gui(ch, hwnd); ShowWindow(hwnd, SW_HIDE); return 0; } // Window khong thuoc registry (chua dang ky) - de DefWindowProc huy. } else if (uMsg == WM_DESTROY) { // Chi xay ra khi window thuc su bi huy (khong con path chu dong nao // DestroyWindow khi view dang attached). Xoa registry + detach view. uint32_t ch = UINT32_MAX; { std::lock_guard lock(g_guiMutex); auto it = g_hwndToCh.find(hwnd); if (it != g_hwndToCh.end()) ch = it->second; g_hwndToCh.erase(hwnd); for (auto it = g_guiWindows.begin(); it != g_guiWindows.end();) { if (it->second == hwnd) it = g_guiWindows.erase(it); else ++it; } } if (ch != UINT32_MAX) post_close_gui(ch, hwnd); } else if (uMsg == kMsgAttach) { // Attach the editor view on the MAIN thread: view->attached() creates // the editor children windows, and they must be owned by the SAME // thread as the parent (this main pump). Attaching on the worker made // children worker-owned while the parent was main-owned -> cross- // thread parent/child -> USER32 0xc000041d (verified with gui_probe). uint32_t ch = (uint32_t)wParam; { std::lock_guard lock(g_guiMutex); auto it = g_guiWindows.find(ch); if (it != g_guiWindows.end() && it->second != hwnd) { // Window replaced by a newer OPEN_GUI - stale attach, drop. std::cerr << "[dbg] kMsgAttach ch=" << ch << " skipped (window replaced)" << std::endl; g_guiAttachPending[ch] = false; return 0; } } auto* inst = g_engine ? g_engine->get(ch) : nullptr; if (!inst || inst->hasAttachedView()) { std::cerr << "[dbg] kMsgAttach ch=" << ch << " skipped (no instrument / view attached)" << std::endl; std::lock_guard lock(g_guiMutex); g_guiAttachPending[ch] = false; return 0; } std::vector samePathSilenced; { std::lock_guard lock(g_guiMutex); auto it = g_attachSilenced.find(ch); if (it != g_attachSilenced.end()) { samePathSilenced = it->second; g_attachSilenced.erase(it); } } std::string gp = lower_plugin_path(ch); bool ok = false; { // Serialize ALL plugin-DLL entry (createInstance AND createView) // with other loads: two threads inside one DLL (Nexus) crash. std::lock_guard lg(g_loadMutex); ok = inst->attachView(hwnd); } { std::lock_guard lock(g_guiMutex); g_guiAttachPending[ch] = false; } if (!gp.empty()) { std::lock_guard lk(g_attachMutex); auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp); if (it != g_attachPaths.end()) g_attachPaths.erase(it); } if (ok) { // Editor is attached on this channel: register it globally so // EVERY channel assigned the same plugin DLL path stays quiet // for the whole editor-open duration (g_editorPathCount). { std::lock_guard lk(g_editorMutex); if (!gp.empty()) { g_editorOpen[ch] = true; g_editorOpenPath[ch] = gp; ++g_editorPathCount[gp]; } } std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd << " plugin=" << gp << " ch=" << ch << " (channel muted while editor open)" << std::endl; } else { // Attach failed -> no editor running -> safe to process again. g_engine->setReloading(ch, false); std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd << " plugin=" << gp << std::endl; PostMessageA(hwnd, WM_CLOSE, 0, 0); } // Restore same-path silence. Channels whose own close job is still // inside createInstance stay muted - that job owns their unmute. for (uint32_t y : samePathSilenced) unmute_if_not_closing(y, "openGUI"); return 0; } else if (uMsg == kMsgDestroy) { // LOAD job handshake: the editor children are main-owned - destroy // them on THIS (main) thread, then acknowledge so the worker can // assign() the new instrument. uint32_t ch = (uint32_t)wParam; if (auto* i = g_engine ? g_engine->get(ch) : nullptr) { std::lock_guard lg(g_loadMutex); i->closeGUI(); while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) DestroyWindow(c); ShowWindow(hwnd, SW_HIDE); } { std::lock_guard lock(g_guiMutex); g_editorDestroyAck[ch] = true; } return 0; } return DefWindowProcA(hwnd, uMsg, wParam, lParam); } #endif // Serialize plugin loads across channels: concurrent VST3 createInstance on // separate worker threads (Nexus) deadlocks inside the SDK factory - all // stuck forever (observed: 3 concurrent Nexus loads, zero assign returned). // One load at a time; SF2/SFZ loads are fast, contention negligible. #ifndef _WIN32 static std::mutex g_loadMutex; #endif // Pending OPEN_GUI requests whose channel instrument was not loaded yet. // The main loop fulfils them as soon as the load job completes (assign ok), // so the GUI button works regardless of JS retry timing. static std::mutex g_pendingGuiMutex; static std::map> g_pendingGui; // 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 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; // Cap the idle pump: a busy editor (Nexus posts timer/paint // messages continuously) otherwise starves this worker's job // queue forever — post_close_gui never runs, Option B's 5s // close wait times out, two editors stay alive (deadlock). for (int pumped = 0; pumped < 16; ++pumped) { if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; TranslateMessage(&msg); DispatchMessageW(&msg); } lk.lock(); } #ifdef _WIN32 OleUninitialize(); #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; }; // --- same-plugin-DLL silence helpers ---------------------------------------- // Two threads inside one VST3 DLL (Nexus) crash/deadlock: the audio loop's // process() on one instance must not race createInstance/createView/terminate // on another. Windows plugin paths are case-insensitive — compare lowercased. static std::string lower_path_str(std::string p) { std::transform(p.begin(), p.end(), p.begin(), [](unsigned char c) { return (char)::tolower(c); }); return p; } static std::string lower_plugin_path(uint32_t ch) { return g_engine ? lower_path_str(g_engine->pathOf(ch)) : std::string(); } // Channels (other than ch) currently assigned the same plugin DLL path. static std::vector same_plugin_channels(uint32_t ch) { std::vector out; std::string p = lower_plugin_path(ch); if (p.empty()) return out; for (uint32_t y = 0; y < 16; ++y) { if (y == ch) continue; if (!g_engine->get(y)) continue; if (lower_plugin_path(y) == p) out.push_back(y); } return out; } static bool close_in_flight(uint32_t ch) { std::lock_guard lk(g_attachMutex); return g_closeInFlight[ch]; } // Unmute y unless its own close job is still inside createInstance — that job // performs the unmute once its fresh instance is loaded (or, if an attach for // the same plugin is in flight, the attach job's restore does it). static void unmute_if_not_closing(uint32_t y, const char* why) { if (close_in_flight(y)) { std::cerr << "[dbg] " << why << ": ch=" << y << " close job still in flight - close job unmutes" << std::endl; return; } g_engine->setReloading(y, false); std::cerr << "[dbg] " << why << ": restored ch=" << y << std::endl; } // closeGUI() MUST run on the channel worker thread (its COM STA apartment) — // the view was attached there. Calling view->removed() from the main thread // (WM_DESTROY handler) is a cross-apartment COM call that corrupts the plugin; // Nexus then hangs on the NEXT view->attached(). Erase the registry inside the // job so Option B (closing another editor before attach) waits for closeGUI to // actually finish. static void post_close_gui(uint32_t ch, HWND hwnd) { // Runs on the MAIN thread (VstWindowProc WM_CLOSE/WM_DESTROY). The // editor children are main-owned now (attachView runs on the main pump), // so closeGUI + child destroy + hide all happen here on the owning // thread; only the fresh-instance reload (createInstance ~2s, must live // in the channel COM STA apartment) is posted to the channel worker. { std::lock_guard lock(g_guiMutex); auto it = g_guiWindows.find(ch); if (it != g_guiWindows.end() && it->second != hwnd) { std::cerr << "[dbg] closeGUI ch=" << ch << " skipped (window replaced)" << std::endl; return; } } std::cerr << "[dbg] closeGUI ch=" << ch << " start" << std::endl; g_engine->setReloading(ch, true); // Close-in-flight: until the fresh instance is loaded, no other job may // unmute this channel (its plugin is being torn down). { std::lock_guard lk(g_attachMutex); g_closeInFlight[ch] = true; } // Silence same-plugin channels: no second thread may enter this DLL // while closeGUI/createInstance runs (2 threads in one DLL -> Nexus // exits silently, observed in probes). std::vector sp = same_plugin_channels(ch); for (uint32_t y : sp) { g_engine->setReloading(y, true); std::cerr << "[dbg] closeGUI: silenced same-plugin ch=" << y << " during reload ch=" << ch << std::endl; } if (auto* i = g_engine->get(ch)) { { // closeGUI nulls the view WITHOUT view->removed() (Nexus removed() // deadlocks). Destroying the editor windows runs the plugin // window proc on this (main) thread; it must not race another // thread createInstance/createView of the same DLL. std::lock_guard lg(g_loadMutex); i->closeGUI(); #ifdef _WIN32 if (hwnd && IsWindow(hwnd)) { while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) DestroyWindow(c); } ShowWindow(hwnd, SW_HIDE); #endif } // Rebuild a fresh instance on the channel worker (its COM STA // apartment is alive); reload() == terminate + loadPlugin // (createInstance). Serialized with other loads by g_loadMutex. ChannelWorker* w = nullptr; if (g_workers) { auto wit = g_workers->find(ch); if (wit != g_workers->end()) w = wit->second.get(); } if (w) { w->post([ch, sp]() { bool reloadOk = false; { std::lock_guard lg(g_loadMutex); if (auto* i = g_engine->get(ch)) reloadOk = i->reload(); } std::cerr << "[dbg] closeGUI ch=" << ch << " reload=" << (reloadOk ? 1 : 0) << std::endl; // Editor is closed: drop the global editor-open state so the // same-plugin channels render again. (This channel is still // silenced by its reloading flag until the restore below.) { std::lock_guard lk(g_editorMutex); if (g_editorOpen[ch]) { g_editorOpen[ch] = false; std::string pth = g_editorOpenPath[ch]; g_editorOpenPath[ch].clear(); if (!pth.empty()) { auto it = g_editorPathCount.find(pth); if (it != g_editorPathCount.end() && --it->second <= 0) g_editorPathCount.erase(it); } } } // createInstance is done: unmuting is safe again. But if an // attach for the SAME plugin path is still running (its // createView must not race process() on any same-path // channel), leave the silence in place - the attach job // restore unmutes everything once attachView finished. { std::lock_guard lk(g_attachMutex); g_closeInFlight[ch] = false; } bool pathBusy = false; { std::lock_guard lk(g_attachMutex); std::string p = lower_plugin_path(ch); pathBusy = !p.empty() && std::find(g_attachPaths.begin(), g_attachPaths.end(), p) != g_attachPaths.end(); } if (pathBusy) { std::cerr << "[dbg] closeGUI ch=" << ch << ": attach in flight for same plugin - leaving silenced (attach restore unmutes)" << std::endl; } else { for (uint32_t y : sp) unmute_if_not_closing(y, "closeGUI"); g_engine->setReloading(ch, false); } }); return; } } // Instrument already gone / no worker - nothing to rebuild: drop the // editor-open state and unmute. { std::lock_guard lk(g_editorMutex); if (g_editorOpen[ch]) { g_editorOpen[ch] = false; std::string pth = g_editorOpenPath[ch]; g_editorOpenPath[ch].clear(); if (!pth.empty()) { auto it = g_editorPathCount.find(pth); if (it != g_editorPathCount.end() && --it->second <= 0) g_editorPathCount.erase(it); } } } for (uint32_t y : sp) unmute_if_not_closing(y, "closeGUI"); g_engine->setReloading(ch, false); { std::lock_guard lk(g_attachMutex); g_closeInFlight[ch] = 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); // V8 bug 2: watchdog THREAD rieng - main loop chi check parent_alive giua // cac block; neu VST process() chan loop thi khong bao gio thoat -> orphan. // Thread nay chay doc lap, parent chet -> TerminateProcess ngay. Kem // start-time check chong PID reuse (OpenProcess tra handle cua process // khac chiem lai PID -> tuong parent con song mai). if (parentPid != 0) { std::thread([parentPid]() { auto proc_birth = [](HANDLE h) -> uint64_t { FILETIME c, e, k, u; if (GetProcessTimes(h, &c, &e, &k, &u)) return (uint64_t(c.dwHighDateTime) << 32) | c.dwLowDateTime; return 0; }; uint64_t birth = 0; { HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid); if (h) { birth = proc_birth(h); CloseHandle(h); } } for (;;) { Sleep(2000); HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid); if (!h) { TerminateProcess(GetCurrentProcess(), 0); return; } uint64_t nowBirth = proc_birth(h); CloseHandle(h); if (birth != 0 && nowBirth != 0 && nowBirth != birth) { TerminateProcess(GetCurrentProcess(), 0); return; } } }).detach(); } #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; // Editor-open predicate: channels whose plugin DLL has an attached editor // stay quiet (channelQuiet in the engine) for the whole editor-open // duration -- derived from the global editor count, so the restore loops // below that unmute after attach/load become harmless no-ops. instruments.setEditorOpenPredicate([](const std::string& lp) { std::lock_guard lk(g_editorMutex); auto it = g_editorPathCount.find(lp); return it != g_editorPathCount.end() && it->second > 0; }); #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> workers; g_workers = &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; // V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS // note-on timer co the bay toi sau STOP (guardPlay tre do React re-render) // -> retrigger note -> VST loop am. Chi PLAY moi nhan note-on lai. bool transportStopped = false; auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) { // CRASH FIX: go through the manager, which holds mu_ for the WHOLE call // (lookup + reloading check + instrument call). Direct get() then // lock-free method calls raced assign() — the worker swapped the map and // destroyed the old instance right after releasing mu_, so a playing // channel loading another VSTi crashed the bridge (use-after-free). // Unassigned and reloading channels are dropped inside the manager. 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) { if (transportStopped) return; // V8 bug 3: drop note-on sau STOP instruments.noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f); } else instruments.noteOff(evt.channel, evt.pitch); break; case 0x8: instruments.noteOff(evt.channel, evt.pitch); break; case 0xB: // CC: controller number in pitch, value in data2 // V9 bug 4: sau STOP, drop sustain-down (CC64>0) — JS co the // gui CC64 xuong sau STOP (note-on/CC timer tre); VSTi giu // note khi pedal down -> am treo loop. CC64=0 (sustain-up) // van cho qua. if (transportStopped && evt.pitch == 64 && evt.data2 > 0) return; instruments.controlChange(evt.channel, evt.pitch, evt.data2); break; case 0xC: // program change: program in data2 instruments.programChange(evt.channel, evt.data2); break; case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB instruments.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; // OPEN_GUI handling - shared by the control queue and the pending-GUI // sweep. Gate on the MAIN thread before creating a window: instrument // load is ASYNC (worker thread); OPEN_GUI too early would attach to an // empty channel and leave a blank window. Not loaded yet => remember the // request; the sweep retries once the load job completes. Window MUST // belong to the MAIN thread (audio loop pump dispatches its messages); // a window on a worker whose pump is idle during jobs hangs // view->attached() (gui_probe: two_workers_close TIMEOUT). The attach // job runs on the channel worker (COM STA apartment stays alive). auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) { if (!instruments.get(guiCh)) { std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh << " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl; std::lock_guard lock(g_pendingGuiMutex); g_pendingGui[guiCh] = { (void*)arg1, pluginId }; return; } // Dedupe OPEN_GUI spam (frontend openVstGuiRetry): view dang // attached -> GUI da mo, khong post attach job lan nua. Re-attach // tren view dang attached lam plugin loi (Nexus createView null). if (auto* i0 = instruments.get(guiCh)) { if (i0->hasAttachedView()) { std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh << " — skip" << std::endl; return; } } if (!workers[guiCh]) workers[guiCh] = std::make_unique(); void* hwnd = (void*)arg1; #ifdef _WIN32 // Window PHAI thuoc MAIN thread (audio loop pump nay dispatch // messages cua no moi vong lap). Tao/cap nhat window ngay tai day. HWND nativeHwnd = nullptr; { std::lock_guard lock(g_guiMutex); auto it = g_guiWindows.find(guiCh); if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second; } if (hwnd == 0) { if (nativeHwnd && IsWindow(nativeHwnd)) { hwnd = (void*)nativeHwnd; SetWindowTextA(nativeHwnd, pluginId.c_str()); ShowWindow(nativeHwnd, SW_SHOW); SetForegroundWindow(nativeHwnd); // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay // the boi assign() -> WM_DESTROY sau nay lookup inst MOI. SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1)); } else { nativeHwnd = (HWND)create_native_vst_window(pluginId.c_str()); if (!nativeHwnd) { std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << pluginId << std::endl; return; } { std::lock_guard lock(g_guiMutex); g_guiWindows[guiCh] = nativeHwnd; // keep window alive g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup } SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1)); hwnd = (void*)nativeHwnd; } } #endif #ifdef _WIN32 // Dedupe: one attach in flight per channel (frontend openVstGuiRetry // spam). Cleared by the worker job early returns and by the // main-thread kMsgAttach handler. { std::lock_guard lock(g_guiMutex); if (g_guiAttachPending[guiCh]) { std::cerr << "[dbg] openGUI: attach already pending ch=" << guiCh << " - skip" << std::endl; return; } g_guiAttachPending[guiCh] = true; } #endif workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() { if (!instruments.get(guiCh)) { std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd << " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl; #ifdef _WIN32 { std::lock_guard lock(g_guiMutex); g_guiAttachPending[guiCh] = false; } #endif return; } std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd << " plugin=" << arg2 << " ch=" << guiCh << std::endl; if (auto* inst0 = instruments.get(guiCh)) { if (inst0->hasAttachedView()) { std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh << " — skip" << std::endl; #ifdef _WIN32 { std::lock_guard lock(g_guiMutex); g_guiAttachPending[guiCh] = false; } #endif return; } } #ifdef _WIN32 // Option B: chi 1 editor VST mo tai 1 thoi diem toan // bridge. Instance thu 2 cua CUNG plugin (Nexus) attach // view o apartment/worker khac -> treo. Dong editor cua // channel khac TRUOC khi attach: WM_CLOSE -> main pump // destroy window -> WM_DESTROY -> closeGUI() + xoa registry. // Chay tren worker job de khong stall writeIndex cua real-time // loop. CRASH FIX: silence moi channel TRUOC khi dong editor // cua no (view->removed() tren worker cung luc process() tren // audio loop = 2 thread trong 1 plugin -> crash USER32). { std::vector others; bool abortAttach = false; { std::lock_guard lock(g_guiMutex); for (const auto& kv : g_guiWindows) if (kv.first != guiCh) others.push_back(kv.first); } for (uint32_t y : others) { // Skip channels whose editor already detached (close job // finished): do not WM_CLOSE a hidden reused window, do // not re-silence a channel that is already unmuted. if (auto* yi0 = instruments.get(y)) { if (!yi0->hasAttachedView()) { std::cerr << "[dbg] openGUI: ch=" << y << " editor already detached - skip" << std::endl; continue; } } HWND yHwnd = nullptr; { std::lock_guard lock(g_guiMutex); auto it = g_guiWindows.find(y); if (it != g_guiWindows.end()) yHwnd = (HWND)it->second; } if (!yHwnd || !IsWindow(yHwnd)) continue; if (instruments.get(y)) { instruments.setReloading(y, true); std::cerr << "[dbg] openGUI: silenced ch=" << y << " while closing its editor (before attach ch=" << guiCh << ")" << std::endl; } PostMessage(yHwnd, WM_CLOSE, 0, 0); std::cerr << "[dbg] openGUI: closing editor ch=" << y << " before attach ch=" << guiCh << std::endl; // WM_CLOSE -> VstWindowProc -> post_close_gui -> closeGUI() // tren worker cua channel y. Doi cho view da detach (window // duoc giu lai de reuse, khong doi registry erase nhu cu). bool closed = false; // The close job = closeGUI + destroy children + fresh // reload (createInstance, ~2s for Nexus). hasAttachedView // flips false after closeGUI only - ALSO wait for the // close job to finish entirely (close_in_flight): the // attach must not run createView while another thread is // still inside this plugin DLL (2 threads in one DLL -> // Nexus exits silently, observed in probes). for (int i = 0; i < 500; ++i) { // 5s budget auto* yi = instruments.get(y); bool detached = !yi || !yi->hasAttachedView(); if (detached && !close_in_flight(y)) { closed = true; break; } Sleep(10); } if (closed) { // Editor detached and its close job finished; the // close job unmutes the channel itself. Do NOT reset // reloading here - the close job owns the silence state. std::cerr << "[dbg] openGUI: restored ch=" << y << " after editor close" << std::endl; } else { std::cerr << "[dbg] openGUI: editor ch=" << y << " close job not finished in 5s, aborting attach ch=" << guiCh << std::endl; abortAttach = true; break; } } if (abortAttach) { // A close job is stuck; do NOT createView (would race the // plugin DLL). Unmute what we silenced (skips channels // whose close job is still in flight - that job unmutes // them when it finishes). for (uint32_t y : others) unmute_if_not_closing(y, "openGUI-abort"); std::cerr << "[dbg] openGUI: attach aborted ch=" << guiCh << std::endl; #ifdef _WIN32 { std::lock_guard lock(g_guiMutex); g_guiAttachPending[guiCh] = false; } #endif return; } } #else (void)0; #endif if (auto* inst = instruments.get(guiCh)) { // Reopen sau khi dong: reload() (terminate + loadPlugin) // PHAI chay tren worker thread nay - COM STA apartment // cua channel song o day. view->attached() cung chay o // day; window thuoc main thread nen main pump (audio // loop) dispatch messages cua no - khong can pump worker. // Chan UAF bang flag reloading_ (set/clear duoi engine // mutex; renderAll giu mutex khi process). // CRASH FIX (0xc000041d USER32 while playing): keep THIS // channel silenced for the whole time its editor is attached. // The worker pumps the plugin's editor windows -> the plugin's // window proc runs on this worker thread while the audio loop // calls processor->process() on the SAME instance (two threads // inside one DLL) -> Nexus crashes ~1-2s after attach during // playback. Muting the channel while the editor is open is the // deterministic trade-off (track silent while GUI visible); // restored when the editor closes (post_close_gui / LOAD). // STALL/CRASH FIX (Fix C, restored): silence EVERY channel // assigned the SAME plugin DLL path for the reload+attach // duration — the audio loop must not process() another // instance of this DLL while createInstance/createView runs // on this worker (2 threads in one DLL -> Nexus exits // silently, observed in probes). Iterate ALL 16 channels, not // just window-owning ones: session-restore instances without // a window are live and renderable. std::vector samePathSilenced; std::string gp = lower_plugin_path(guiCh); if (!gp.empty()) { // Flush held notes BEFORE muting: these channels stay muted // for the whole editor-open duration and the manager drops // MIDI for quiet channels -- notes held at mute time would // otherwise resume stuck after the editor closes. The // manager's dispatch holds mu_ (serialized vs the audio // loop); g_loadMutex serializes vs other workers' DLL entry. std::lock_guard lg(g_loadMutex); for (uint32_t y = 0; y < 16; ++y) { if (y == guiCh) continue; if (!instruments.get(y)) continue; if (lower_plugin_path(y) == gp) { for (uint32_t n = 0; n < 128; ++n) instruments.noteOff(y, n); instruments.setReloading(y, true); samePathSilenced.push_back(y); std::cerr << "[dbg] openGUI: silenced same-plugin ch=" << y << " during attach ch=" << guiCh << std::endl; } } } instruments.setReloading(guiCh, true); // Register attach-in-flight: a same-path close job must not // unmute its channel while createView runs here (it stays // silenced; this job's restore unmutes it at the end). { std::lock_guard lk(g_attachMutex); if (!gp.empty()) g_attachPaths.push_back(gp); } // Attach now happens on the MAIN thread (kMsgAttach): the // editor children created inside view->attached() must be // owned by the same thread as the parent window (main pump). // Attaching on the worker made children worker-owned while // the parent was main-owned -> cross-thread parent/child -> // USER32 0xc000041d (verified with gui_probe). Only // reloadForGUI stays here: it may createInstance (~2s on the // hasAttachedOnce reopen path) and must run on this worker // COM STA apartment. bool reloadOk = false; { // Serialize ALL plugin-DLL entry (createInstance AND // createView) with other loads: two threads inside one // DLL (Nexus) crash. std::lock_guard lg(g_loadMutex); reloadOk = inst->reloadForGUI(); } #ifdef _WIN32 if (!reloadOk || !IsWindow((HWND)hwnd)) { #else if (!reloadOk) { #endif // No editor running -> safe to process again. for (uint32_t y : samePathSilenced) unmute_if_not_closing(y, "openGUI"); instruments.setReloading(guiCh, false); { std::lock_guard lk(g_attachMutex); if (!gp.empty()) { auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp); if (it != g_attachPaths.end()) g_attachPaths.erase(it); } } { std::lock_guard lock(g_guiMutex); g_guiAttachPending[guiCh] = false; } std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd << " plugin=" << arg2 << std::endl; #ifdef _WIN32 // Attach khong co view (VD: SF2/SFZ hoac plugin loi). // Dong ngay cua so vo nghia de khong con window treo. PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0); #endif } else { #ifdef _WIN32 // Stash the same-path silenced set for the main-thread // kMsgAttach handler, then ask it to attach (message, not // direct call: the window proc runs on the main pump). { std::lock_guard lock(g_guiMutex); g_attachSilenced[guiCh] = samePathSilenced; } PostMessageW((HWND)hwnd, kMsgAttach, (WPARAM)guiCh, 0); #else // No native window on non-Windows: direct attach (null // parent) fails -> restore the silenced channels. if (!inst->attachView(hwnd)) { for (uint32_t y : samePathSilenced) unmute_if_not_closing(y, "openGUI"); instruments.setReloading(guiCh, false); std::lock_guard lk(g_attachMutex); if (!gp.empty()) { auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp); if (it != g_attachPaths.end()) g_attachPaths.erase(it); } } #endif } } }); }; // 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; int pumpedMain = 0; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); ++pumpedMain; } #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]() { // CRASH FIX: this job enters the plugin DLL (createInstance // in loadPlugin, old-instance terminate on replace) while // the audio loop may process() another instance of the SAME // DLL — 2 threads in one DLL crashes Nexus (observed with // session-restore instances). Silence this channel (its old // instance is being torn down / replaced) and every // same-plugin channel for the whole job. Old path covers // the old-instance teardown; the new path (computed BEFORE // assign — see spNew below) covers the freshly created // instance. CRASH FIX (SF→VSTi while playing): the new // path must be silenced BEFORE assign() — assign() itself // runs createInstance (loadPlugin) on this worker, and a // restored channel on ANOTHER track holding the SAME DLL // kept process()ing on the audio loop during that window // (old code silenced spNew only AFTER assign → crash). instruments.setReloading(ch, true); std::vector spOld = same_plugin_channels(ch); for (uint32_t y : spOld) { g_engine->setReloading(y, true); std::cerr << "[dbg] load: silenced same-plugin ch=" << y << " during load ch=" << ch << std::endl; } // Channels (other than ch) already assigned the NEW path — // silence BEFORE createInstance enters that DLL. std::vector spNew; if (t == InstrumentType::VST3) { std::string np = lower_path_str(path); if (!np.empty()) { for (uint32_t y = 0; y < 16; ++y) { if (y == ch) continue; if (!g_engine->get(y)) continue; if (std::find(spOld.begin(), spOld.end(), y) != spOld.end()) continue; if (lower_plugin_path(y) != np) continue; g_engine->setReloading(y, true); spNew.push_back(y); std::cerr << "[dbg] load: silenced same-plugin (new path) ch=" << y << " during load ch=" << ch << std::endl; } } } #ifdef _WIN32 // DONG cua so editor dang mo cua channel TRUOC khi assign(): // thay the inst (VST3 -> SF2/inst khac) ma editor con song -> // old inst destructor goi view->removed() tren HWND con hoat // dong -> plugin block -> treo bridge. // QUAN TRONG: window duoc tao tren MAIN thread (handleOpenGui // goi create_native_vst_window o main loop) — DestroyWindow tu // worker thread la cross-thread (MSDN cam), gay crash USER32 // 0xc000041d. Post WM_CLOSE -> main pump destroy window tren // DUNG thread so huu no. Registry da erase o tren nen WM_DESTROY // khong goi closeGUI tren inst cu (inst moi chua co view). HWND hToHide = nullptr; { std::lock_guard lock(g_guiMutex); auto git = g_guiWindows.find(ch); if (git != g_guiWindows.end()) hToHide = (HWND)git->second; } // CRASH FIX: detach view TRUOC khi thay inst. Plugin editor // children thuoc worker thread nay (attachView chay o day); // khong destroy parent window (main-owned) khi view con song // -> DestroyWindow cross-thread -> USER32 0xc000041d. An // window, giu trong registry de reuse (reopen ShowWindow lai). if (hToHide && IsWindow(hToHide)) { // CRASH FIX (0xc000041d USER32 while playing): silence // the channel while the old editor detaches — view->removed() // on this worker would race processor->process() on the // audio loop (same plugin instance). instruments.setReloading(ch, true); if (auto* i = instruments.get(ch)) i->closeGUI(); // The editor goes away with this load: drop the global // editor-open state (path stored at attach -- still the // old path before assign() below). { std::lock_guard lk(g_editorMutex); if (g_editorOpen[ch]) { g_editorOpen[ch] = false; std::string pth = g_editorOpenPath[ch]; g_editorOpenPath[ch].clear(); if (!pth.empty()) { auto it = g_editorPathCount.find(pth); if (it != g_editorPathCount.end() && --it->second <= 0) g_editorPathCount.erase(it); } } } // Destroy the old editor's child windows (worker-owned, // created by attachView here) so they stop pumping; // the parent window is hidden and kept for reuse. // See post_close_gui for the same pattern. // CRASH FIX (0xc000041d): the editor children are // MAIN-thread-owned now (attachView runs on the main // pump) - DestroyWindow from this worker is cross- // thread and crashes USER32. Ask the main thread to // destroy them (kMsgDestroy) and wait for the ack. { std::lock_guard lock(g_guiMutex); g_editorDestroyAck[ch] = false; } PostMessageW(hToHide, kMsgDestroy, (WPARAM)ch, 0); bool destroyAcked = false; for (int i = 0; i < 500; ++i) { { std::lock_guard lock(g_guiMutex); if (g_editorDestroyAck[ch]) { destroyAcked = true; break; } } Sleep(10); } if (!destroyAcked) std::cerr << "[dbg] load: editor destroy not acked ch=" << ch << std::endl; } #endif std::cerr << "[dbg] load thread start ch=" << ch << " type=" << (int)t << " path=" << path << std::endl; std::lock_guard lg(g_loadMutex); 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; } // Restore. Failed load keeps the OLD instance — unmute it // back (silenced at job start); a successful assign leaves // the fresh instance unmuted by construction. if (!ok) g_engine->setReloading(ch, false); for (uint32_t y : spOld) unmute_if_not_closing(y, "load"); for (uint32_t y : spNew) unmute_if_not_closing(y, "load"); }); } 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 transportStopped = true; // Release sustain pedal TRUOC (CC64=0) — nhieu VSTi giu note // khi pedal con down -> note-off cua allNotesOff bi bo qua // -> am treo loop (V8 bug 3). for (uint32_t ch = 0; ch < 16; ++ch) instruments.controlChange(ch, 64, 0); // manager drops unassigned/reloading instruments.allNotesOff(); std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl; } else if (c.arg0 == 1) { // PLAY transportStopped = false; 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 uint32_t guiCh = c.channel; if (guiCh >= 16) guiCh = 0; handleOpenGui(guiCh, (uintptr_t)c.arg1, std::string(c.arg2)); } } shmIPC->controlQueueCount = 0; // Pending OPEN_GUI requests: fulfilled as soon as the channel's // instrument becomes available (load job completed on its worker). { std::vector>> readyGui; { std::lock_guard lock(g_pendingGuiMutex); for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) { if (instruments.get(it->first)) { readyGui.push_back({ it->first, it->second }); it = g_pendingGui.erase(it); } else ++it; } } for (auto& g : readyGui) handleOpenGui(g.first, (uintptr_t)g.second.first, g.second.second); } // 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(blockCount * blockDurationMs * 1000.0)); auto now = std::chrono::steady_clock::now(); if (now < targetTime) { auto diff = std::chrono::duration_cast(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; }