// native_bridge/src/plugin_host_main.cpp - G4.1 sandbox VST3 host (child process). // Proof-of-concept (NOT shipped): runs ONE VST3 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. // // argv: --shm --path [--sr ] [--block ] [--parent ] #include "Vst3Instrument.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 } int main(int argc, char* argv[]) { std::string shmName, path; double sr = 44100.0; uint32_t block = AUDIO_BLOCK_SIZE; uint32_t parentPid = 0; bool isOpen = false; 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; } if (shmName.empty() || path.empty()) { printf("usage: plugin_host --shm --path [--sr rate] [--block n] [--parent pid]\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\n", (unsigned long)GetCurrentProcessId(), shmName.c_str(), path.c_str(), sr, block); 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 inst; inst.init(sr, block); // sets sampleRate_/maxBlockSize_ used by setupProcessing if (!inst.loadPlugin(path, sr)) { printf("[plugin_host] loadPlugin failed\n"); return 4; } // GUI on the main thread (editorhost pattern: attach then pump below). HWND hwnd = nullptr; { WNDCLASSEX wc = {}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = DefWindowProc; wc.hInstance = GetModuleHandle(nullptr); wc.lpszClassName = "PluginHostClass"; RegisterClassEx(&wc); hwnd = CreateWindowEx(0, "PluginHostClass", "PluginHost", WS_OVERLAPPEDWINDOW, 0, 0, 800, 600, nullptr, nullptr, wc.hInstance, nullptr); } printf("[plugin_host] ownHwnd=%p openGUI...\n", (void*)hwnd); fflush(stdout); bool gui = inst.openGUI(hwnd); printf("[plugin_host] openGUI=%d\n", gui ? 1 : 0); fflush(stdout); // Heartbeat ~10Hz, independent of the audio loop (G3.1 pattern). std::thread([ipc]() { for (;;) { Sleep(100); ipc->base.heartbeat++; } }).detach(); int activeNotes = 0; bool idle = false; for (;;) { MSG msg; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } 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) { 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; 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; } Sleep(0); } inst.closeGUI(); #ifdef _WIN32 timeEndPeriod(1); CoUninitialize(); #endif shm_close(shm); return 0; }