// native_bridge/src/plugin_host_main.cpp - G4.1 sandbox VST host (child process). // Runs ONE VST out-of-process. The plugin's own crash can only kill this // process, never the DAW/bridge. Audio + MIDI + heartbeat travel over a // child-owned SHM mapping reusing the SharedAudioBufferIPC layout, so the // bridge can later adopt the same struct without layout churn. // // VST3 mode (default): opens the plugin GUI at startup on its own window. // VST2 mode (--vst2, jBridge stubs like Qin_RV): the GUI is deferred - the // bridge sends control 4 (OPEN_GUI) / 5 (CLOSE_GUI) / 6 (QUIT) through the // SHM control queue; the child polls it in its main loop. Deferral matters: // loading a Qin must not pop a window for every track. // // argv: --shm --path [--vst2] [--channel ] [--sr ] [--block ] [--parent ] [--open] #include "Vst3Instrument.h" #define MIN_PRESET 100 // b64 chars; real chunks are KBs #include "Vst2Instrument.h" #include "SharedMemoryIPC.h" #ifdef _WIN32 #include #include #endif #include #include #include #include #include #include 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 } // G4.7: the child owns the host window, so the user's X click lands HERE, not // on the bridge. DefWindowProc destroys the window on WM_CLOSE while the VST2 // instrument still thinks its editor is attached (guiAttached_ stale true) - // a second OPEN_GUI then can never re-show the GUI (bridge dedupes on // hasAttachedView, and the child's openGUI guard skips too). Intercept: // WM_CLOSE -> effEditClose + hide (keep the window alive, reopen works); // WM_DESTROY (external destroy) -> forget the hwnd so the next OPEN_GUI // creates a fresh window instead of attaching to a dead one. static INativeInstrument* g_wndInst = nullptr; static HWND g_hostHwnd = nullptr; static LRESULT CALLBACK PluginHostWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CLOSE: if (g_wndInst) { try { g_wndInst->closeGUI(); } catch (...) {} } // effEditClose leaves the jBridge wrapper dialog + stale editor // children alive; re-opening on the SAME window stacks a second // dialog on top -> white background, editor never loads. Destroy // the window so the next OPEN_GUI creates a fresh one. g_hostHwnd = nullptr; DestroyWindow(hwnd); return 0; // swallow: we destroyed it ourselves case WM_DESTROY: if (g_wndInst) { try { g_wndInst->closeGUI(); } catch (...) {} } g_hostHwnd = nullptr; break; default: break; } return DefWindowProc(hwnd, msg, wParam, lParam); } static HWND make_host_window() { WNDCLASSEX wc = {}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = PluginHostWndProc; wc.hInstance = GetModuleHandle(nullptr); wc.lpszClassName = "PluginHostClass"; RegisterClassEx(&wc); return CreateWindowEx(0, "PluginHostClass", "PluginHost", WS_OVERLAPPEDWINDOW, 0, 0, 800, 600, nullptr, nullptr, wc.hInstance, nullptr); } int main(int argc, char* argv[]) { #ifdef _WIN32 // Suppress Windows loader error dialogs — plugin dependency thiếu (0x7E) // phải fail âm thầm, không MessageBox chặn user. SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX); #endif std::string shmName, path, presetFile; double sr = 44100.0; uint32_t block = AUDIO_BLOCK_SIZE; uint32_t parentPid = 0; bool isOpen = false; bool vst2 = false; uint32_t channel = 0; for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "--shm") == 0 && i + 1 < argc) shmName = argv[++i]; else if (strcmp(argv[i], "--path") == 0 && i + 1 < argc) path = argv[++i]; else if (strcmp(argv[i], "--sr") == 0 && i + 1 < argc) sr = atof(argv[++i]); else if (strcmp(argv[i], "--block") == 0 && i + 1 < argc) block = (uint32_t)atoi(argv[++i]); else if (strcmp(argv[i], "--parent") == 0 && i + 1 < argc) parentPid = (uint32_t)strtoul(argv[++i], nullptr, 0); else if (strcmp(argv[i], "--open") == 0) isOpen = true; else if (strcmp(argv[i], "--vst2") == 0) vst2 = true; else if (strcmp(argv[i], "--channel") == 0 && i + 1 < argc) channel = (uint32_t)atoi(argv[++i]); else if (strcmp(argv[i], "--presetfile") == 0 && i + 1 < argc) presetFile = argv[++i]; } if (shmName.empty() || path.empty()) { printf("usage: plugin_host --shm --path [--vst2] [--channel n] [--sr rate] [--block n] [--parent pid] [--open]\n"); return 2; } #ifdef _WIN32 CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); timeBeginPeriod(1); // Sleep(1) really sleeps ~1ms, not the 15.6ms timer tick #endif printf("[plugin_host] pid=%lu shm=%s path=%s sr=%.0f block=%u vst2=%d ch=%u\n", (unsigned long)GetCurrentProcessId(), shmName.c_str(), path.c_str(), sr, block, vst2 ? 1 : 0, channel); fflush(stdout); // --open: bridge already created the mapping (G4.2); default PoC: child owns. ShmHandle* shm = isOpen ? shm_open(shmName.c_str(), sizeof(SandboxHostIPC)) : shm_create(shmName.c_str(), sizeof(SandboxHostIPC)); if (!shm) { printf("[plugin_host] FAILED shm_create %s\n", shmName.c_str()); return 3; } SandboxHostIPC* ipc = shm_sandbox_ptr(shm); Vst3Instrument instV3; Vst2Instrument instV2; if (vst2) { instV2.setChannel(channel); if (!instV2.loadPlugin(path, sr)) { printf("[plugin_host] loadPlugin failed\n"); return 4; } instV2.init(sr, block); // loadPlugin FIRST: init() opens effect (kEffectOpen+resume); before load it is a silent no-op } else { instV3.init(sr, block); // sets sampleRate_/maxBlockSize_ used by setupProcessing if (!instV3.loadPlugin(path, sr)) { printf("[plugin_host] loadPlugin failed\n"); return 4; } } INativeInstrument& inst = vst2 ? (INativeInstrument&)instV2 : (INativeInstrument&)instV3; g_wndInst = &inst; // WM_CLOSE (user X) needs the live instrument // G4.6 auto-play fix: a loaded chunk can embed a stuck/sustained note // (factory state, seed, or a patch captured mid-note). Release everything // right after a preset apply so the channel starts silent instead of // sounding on app open (STOP later releases it too, but startup must not // play). auto panicAfterPreset = [&inst, channel]() { inst.controlChange(0, 64, 0); // sustain pedal off inst.controlChange(0, 123, 0); // all notes off for (uint32_t p = 0; p < 128; ++p) inst.noteOff(0, p, 0); printf("[plugin_host] panic after preset ch=%u\n", channel); fflush(stdout); }; // Optional preset restore: Qin_RV and other jBridge romplers render SILENT // from their factory state - they need their program/bank chunk. The bridge // forwards presetBase64 (base64 text, one line) via a temp file. if (!presetFile.empty()) { FILE* f = fopen(presetFile.c_str(), "rb"); if (f) { fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); std::string b64; b64.resize(sz > 0 ? (size_t)sz : 0); if (sz > 0 && fread(&b64[0], 1, (size_t)sz, f) == (size_t)sz) { inst.loadSerializedState(b64); printf("[plugin_host] preset restored from %s (%ldB)\n", presetFile.c_str(), sz); } fclose(f); } else { printf("[plugin_host] preset file open failed: %s\n", presetFile.c_str()); } fflush(stdout); } // VST3: GUI on the main thread at startup (editorhost pattern: attach then // pump below). VST2: deferred until control 4 (OPEN_GUI) so loading a Qin // does not pop a window for every track. g_hostHwnd = nullptr; if (!vst2) { g_hostHwnd = make_host_window(); printf("[plugin_host] ownHwnd=%p openGUI...\n", (void*)g_hostHwnd); fflush(stdout); bool gui = inst.openGUI(g_hostHwnd); printf("[plugin_host] openGUI=%d\n", gui ? 1 : 0); fflush(stdout); } // G4.5: publish the initial chunk so the bridge can persist it even // without a preset round-trip yet (factory state = the silent chunk; // the GUI or a presetIn apply replaces it later). std::string lastOut; inst.captureState(); { std::string out = inst.serializeState(); if (!out.empty() && out.size() < PRESET_BLOB_SIZE) { memcpy((void*)ipc->presetOut, out.data(), out.size()); ipc->presetOutLen = (uint32_t)out.size(); lastOut = out; printf("[plugin_host] initial preset published ch=%u (%uB)\n", channel, ipc->presetOutLen); fflush(stdout); } // G4.6 auto-play fix (clobber-safe): release stuck notes AFTER the real // chunk is already published, then absorb the panic-mutated chunk so the // periodic republish never echoes it back over presetOut. Startup stays // silent AND the state file keeps the real patch. panicAfterPreset(); inst.captureState(); { std::string postPanic = inst.serializeState(); if (!postPanic.empty()) lastOut = postPanic; } } // Heartbeat ~10Hz, independent of the audio loop (G3.1 pattern). std::thread([ipc]() { for (;;) { Sleep(100); ipc->base.heartbeat++; } }).detach(); // G4.7: publish counter per slot (how many times this child published // each slot). Paired with the bridge's bridgeConsumed[2] - see the // wait after publish below. uint32_t childPub[2] = {0, 0}; int activeNotes = 0; bool idle = false; bool quit = false; uint32_t lastCaptureTick = 0; for (;;) { MSG msg; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } // VST2 controls: 4=OPEN_GUI, 5=CLOSE_GUI, 6=QUIT. This SHM is // per-child - only the bridge-side SandboxVst2Host writes it, so no // frontend saveStateJob spam like the bridge's own control queue. if (vst2) { uint32_t nctl = ipc->base.controlQueueCount > 8 ? 8 : ipc->base.controlQueueCount; for (uint32_t i = 0; i < nctl; ++i) { const auto& c = ipc->base.controlQueue[i]; if (c.type == 4) { if (!g_hostHwnd) g_hostHwnd = make_host_window(); if (g_hostHwnd && !inst.hasAttachedView()) { printf("[plugin_host] OPEN_GUI ch=%u\n", channel); fflush(stdout); inst.openGUI(g_hostHwnd); ShowWindow(g_hostHwnd, SW_SHOW); // Đưa GUI lên top để tương tác ngay: TOPMOST tạm thời // (rồi hạ ngay) kéo window lên đỉnh z-order + focus. SetWindowPos(g_hostHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); SetWindowPos(g_hostHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); SetForegroundWindow(g_hostHwnd); BringWindowToTop(g_hostHwnd); } } else if (c.type == 5) { inst.closeGUI(); if (g_hostHwnd) ShowWindow(g_hostHwnd, SW_HIDE); } else if (c.type == 6) { quit = true; } else if (c.type == 7) { // ALL_NOTES_OFF: runtime panic (STOP/solo) panicAfterPreset(); // absorb the panic-mutated chunk so the ~2s periodic // republish never overwrites presetOut with it inst.captureState(); std::string postPanic = inst.serializeState(); if (!postPanic.empty()) lastOut = postPanic; printf("[plugin_host] ALL_NOTES_OFF ch=%u\n", channel); fflush(stdout); } } ipc->base.controlQueueCount = 0; if (quit) break; } // G4.5: preset round-trip. Apply bridge->child presetIn (restore path // writes it right after spawn; the seed default too). Republish // child->bridge presetOut when the chunk changed. if (ipc->presetInLen > 0 && ipc->presetInLen < PRESET_BLOB_SIZE) { // Reject tiny b64 (Qin's empty stub "KAM..." is 16 chars): the // saved-state file can hold a stub captured before the plugin // loaded an instrument; applying it would wipe the seeded // default and leave the channel silent. if (ipc->presetInLen < MIN_PRESET) { printf("[plugin_host] preset rejected ch=%u (stub %uB)\n", channel, (unsigned)ipc->presetInLen); fflush(stdout); ipc->presetInLen = 0; } else { // consume-before-apply: copy and zero presetInLen FIRST so the // bridge can safely queue the next preset while this plugin // applies the current one (prevents the restore clobber race). uint32_t plen = ipc->presetInLen; std::string b64(ipc->presetIn, plen); ipc->presetInLen = 0; inst.loadSerializedState(b64); printf("[plugin_host] preset applied from SHM ch=%u (%uB)\n", channel, (unsigned)plen); fflush(stdout); // republish immediately so a just-after restore save has it inst.captureState(); std::string out = inst.serializeState(); if (!out.empty() && out.size() < PRESET_BLOB_SIZE) { memcpy((void*)ipc->presetOut, out.data(), out.size()); ipc->presetOutLen = (uint32_t)out.size(); lastOut = out; } panicAfterPreset(); // absorb the panic-mutated chunk so the ~2s periodic republish // never overwrites presetOut with it inst.captureState(); std::string postPanic = inst.serializeState(); if (!postPanic.empty()) lastOut = postPanic; } } if (++lastCaptureTick >= 200) { // ~2s of loop passes lastCaptureTick = 0; inst.captureState(); std::string out = inst.serializeState(); if (!out.empty() && out != lastOut && out.size() < PRESET_BLOB_SIZE) { memcpy((void*)ipc->presetOut, out.data(), out.size()); ipc->presetOutLen = (uint32_t)out.size(); lastOut = out; printf("[plugin_host] preset republished ch=%u (%uB)\n", channel, ipc->presetOutLen); fflush(stdout); } } inst.guiIdle(); // VST2 editors repaint via effEditIdle; no-op for VST3 uint32_t n = ipc->base.midiQueueCount > 64 ? 64 : ipc->base.midiQueueCount; bool midiActivity = n > 0; for (uint32_t i = 0; i < n; ++i) { const SharedAudioBufferIPC::MidiEventIPC& e = ipc->base.midiQueue[i]; switch (e.command) { case 0x9: if (e.velocity > 0) { inst.noteOn(e.channel, e.pitch, e.velocity / 127.0f, 0); ++activeNotes; } else { inst.noteOff(e.channel, e.pitch, 0); if (activeNotes > 0) --activeNotes; } break; case 0x8: inst.noteOff(e.channel, e.pitch, 0); if (activeNotes > 0) --activeNotes; break; case 0xB: inst.controlChange(e.channel, e.pitch, e.data2); break; case 0xC: inst.programChange(e.channel, e.data2); break; case 0xE: inst.pitchBend(e.channel, e.data2 | (uint32_t(e.data3) << 7)); break; default: break; } } ipc->base.midiQueueCount = 0; if (midiActivity) idle = false; if (parentPid && !parent_alive(parentPid)) { printf("[plugin_host] parent gone - exiting\n"); break; } if (idle && !inst.hasAttachedView()) { // GUI preview needs continuous rendering Sleep(15); continue; } // G4.3 double buffer: render into the slot we are NOT publishing, then // publish. The bridge keeps the last published block, so a slow child // never tears audio (it just repeats the previous block). uint32_t slot = 1u - (ipc->writeSlot & 1u); inst.processAudioBlock(ipc->audioLeft[slot], ipc->audioRight[slot], block); ipc->writeSlot = slot; childPub[slot]++; ipc->base.bridgeWriteIndex++; if (activeNotes == 0) { float peak = 0.0f; for (uint32_t i = 0; i < block; ++i) { float l = ipc->audioLeft[slot][i]; if (l < 0.0f) l = -l; float r = ipc->audioRight[slot][i]; if (r < 0.0f) r = -r; if (l > peak) peak = l; if (r > peak) peak = r; } if (peak < 0.0005f) idle = true; } // G4.7: consumer-sync pacing. The bridge (DAW) consumes exactly one // block per real-time block; the old unpaced child rendered ~50x // real-time, so the bridge skipped most child blocks -> audio jumps // ("tạch tạch" instead of the instrument). Wait AFTER publishing // until the bridge copied THIS block before rendering the next one. // The bridge never stalls (DAW main loop renders + paces every // iteration), so this wait is bounded (~5ms). If the bridge dies, // parentPid points at it - the parent_alive check breaks the wait. while (ipc->bridgeConsumed[slot] < childPub[slot]) { if (parentPid && !parent_alive(parentPid)) break; Sleep(1); } Sleep(0); } inst.closeGUI(); #ifdef _WIN32 timeEndPeriod(1); CoUninitialize(); #endif shm_close(shm); return 0; }