Files
SonicForgeStudio/native_bridge/src/plugin_host_main.cpp
T

164 lines
6.1 KiB
C++

// 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 <name> --path <vst3> [--sr <rate>] [--block <n>] [--parent <pid>]
#include "Vst3Instrument.h"
#include "SharedMemoryIPC.h"
#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
#endif
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
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 <name> --path <vst3> [--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;
}