bridge: fix Qin VST2 silent playback via per-child SHM sandbox

- out-of-process VST2 sandbox: each Qin track spawns its own plugin_host.exe child
  with unique SHM (SonicForge_PluginHost2_<bridgePid>_<ch>_<seq>), watchdog <=3 respawns
- preset round-trip bridge<->child over SHM (presetIn/presetOut 64KB), stub rejection MIN_PRESET=100
- seed qin_default.b64 default preset into every fresh child; saved patch wins on restore
- forward MIDI on channel 0 (Qin_RV only listens on ch0); one instrument per child
- controls 4/5/6 = OPEN_GUI/CLOSE_GUI/QUIT
- gitignore runtime carla_projects
This commit is contained in:
2026-08-20 19:16:43 +07:00
parent d6649e4474
commit b91e6d3875
10 changed files with 589 additions and 34 deletions
+1
View File
@@ -37,6 +37,7 @@ src-tauri/vc_redist.x64.exe
app/storage/plugin_dirs.json
app/storage/sf_scan_state.json.bak-root
app/storage/soundfonts/
app/storage/carla_projects/
# build-time vendor trees & accidental artifacts (never commit)
native_bridge/vcpkg_installed/
+5 -1
View File
@@ -79,7 +79,7 @@ cmake -S $NB_DIR -B $BUILD_DIR -G $VS_GEN -A $VS_ARCH `
-DCMAKE_BUILD_TYPE=Release `
-DVCPKG_TARGET_TRIPLET=x64-windows
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake configure failed" -ForegroundColor Red; exit 1 }
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge fx_vst_bridge -- /m
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge fx_vst_bridge plugin_host -- /m
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake build failed" -ForegroundColor Red; exit 1 }
# 6. Copy sidecar to Tauri externalBin location
@@ -93,6 +93,10 @@ Copy-Item $EXE (Join-Path $BIN_DIR "daw_vst_bridge-x86_64-pc-windows-msvc.exe")
$FX_EXE = Join-Path $BUILD_DIR "Release\fx_vst_bridge.exe"
if (-not (Test-Path $FX_EXE)) { Write-Host "ERROR: $FX_EXE not found" -ForegroundColor Red; exit 1 }
Copy-Item $FX_EXE (Join-Path $BIN_DIR "fx_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
# plugin_host: VST sandbox child (VST3 crash isolation + jBridge VST2 per-process instances)
$PH_EXE = Join-Path $BUILD_DIR "Release\plugin_host.exe"
if (-not (Test-Path $PH_EXE)) { Write-Host "ERROR: $PH_EXE not found" -ForegroundColor Red; exit 1 }
Copy-Item $PH_EXE (Join-Path $BIN_DIR "plugin_host-x86_64-pc-windows-msvc.exe") -Force
# 7. Copy runtime DLLs next to sidecar (Tauri externalBin bundles only the exe)
Write-Host "== [7/7] Copy runtime DLLs (fluidsynth + jpeg) =="
+3
View File
@@ -66,6 +66,7 @@ add_executable(daw_vst_bridge
src/Vst2Instrument.cpp
src/StateStore.cpp
src/SandboxVst3Host.cpp
src/SandboxVst2Host.cpp
)
else()
# Linux: offline --render only (SF2/SF3 + SFZ; VST3/VST2 hosting is
@@ -133,6 +134,7 @@ add_executable(gui_probe
src/Vst3Instrument.cpp
src/Vst2Instrument.cpp
src/SandboxVst3Host.cpp
src/SandboxVst2Host.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(gui_probe PRIVATE HAVE_VST3SDK=1)
@@ -151,6 +153,7 @@ add_executable(plugin_host
src/plugin_host_main.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
src/Vst2Instrument.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(plugin_host PRIVATE HAVE_VST3SDK=1)
+11
View File
@@ -87,11 +87,22 @@ struct SharedAudioBufferIPC {
// The child renders into the slot it is NOT publishing (writeSlot flips),
// then publishes; the bridge reads the published slot. A stale slot is safe
// to read (bridge keeps last block), so a slow child never tears audio.
// G4.5: preset blob round-trip for VST2 sandbox children. jBridge romplers
// like Kong Audio Qin_RV render SILENT from factory state - they need their
// program/bank chunk restored (bridge_state presetBase64). presetIn is
// bridge->child (base64 text; presetInLen>0 = pending, child applies and
// zeroes it). presetOut is child->bridge: the child republishes its chunk
// after load / preset apply / GUI edit, so the bridge can persist it.
#define PRESET_BLOB_SIZE 65536
struct SandboxHostIPC {
SharedAudioBufferIPC base; // MIDI/control/heartbeat, offsets unchanged
volatile uint32_t writeSlot; // 0 or 1: slot the child finished writing
float audioLeft[2][AUDIO_BLOCK_SIZE]; // 2 full blocks
float audioRight[2][AUDIO_BLOCK_SIZE];
char presetIn[PRESET_BLOB_SIZE]; // bridge -> child (base64 text)
volatile uint32_t presetInLen; // >0: pending; child applies + zeroes
char presetOut[PRESET_BLOB_SIZE]; // child -> bridge (base64 text)
volatile uint32_t presetOutLen; // 0 = none published yet
};
// Shared-memory helpers (impl in SharedMemoryIPC.cpp).
+30 -1
View File
@@ -7,6 +7,7 @@
#include "Vst3Instrument.h"
#include "Vst2Instrument.h"
#include "SandboxVst3Host.h"
#include "SandboxVst2Host.h"
#endif
// void* members keep fluid types out of the public header; cast here.
@@ -19,6 +20,7 @@
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <thread>
@@ -181,6 +183,24 @@ void SfizzInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t
// -----------------------------------------------------------------
// 3. MULTI-CHANNEL INSTRUMENT MANAGER (A10)
// -----------------------------------------------------------------
// jBridge stub detection: jBridge-wrapped VST2s (e.g. Qin_RV_x64.dll) embed
// the string "Bridger64.dll". The bundled wrapper allows only ONE instance
// of the same plugin per host process (2nd instance loses the shared audio
// buffer -> silent), so these MUST run in a sandbox child (one process per
// instance). Native VST2s (Nexus) host in-process with an embedded editor.
static bool is_jbridge_dll(const std::string& path) {
FILE* f = fopen(path.c_str(), "rb");
if (!f) return false;
std::vector<char> d;
char tmp[16384];
size_t n;
while ((n = fread(tmp, 1, sizeof(tmp), f)) > 0)
d.insert(d.end(), tmp, tmp + n);
fclose(f);
static const char needle[] = "Bridger64.dll";
return std::search(d.begin(), d.end(), needle, needle + sizeof(needle) - 1) != d.end();
}
std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(InstrumentType type) {
switch (type) {
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
@@ -211,6 +231,12 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
if (channel >= 16) return false;
auto inst = create_instrument(type);
if (!inst) return false;
#ifdef _WIN32
// jBridge VST2 (Qin): sandbox in a child process - one Bridger64
// instance per process. Native VST2 stays in-process.
if (type == InstrumentType::VST2 && is_jbridge_dll(path))
inst = std::make_unique<SandboxVst2Host>();
#endif
bool loaded = false;
if (type == InstrumentType::SOUNDFONT_SF2_SF3)
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
@@ -227,6 +253,8 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
if (auto* v2 = dynamic_cast<Vst2Instrument*>(inst.get())) {
v2->setChannel(channel);
loaded = v2->loadPlugin(path, sampleRate);
} else if (auto* s2 = dynamic_cast<SandboxVst2Host*>(inst.get())) {
loaded = s2->loadPlugin(path, sampleRate, channel);
}
}
if (type == InstrumentType::VST3) {
@@ -344,7 +372,8 @@ bool InstrumentEngineManager::snapshot(BridgeStateSnapshot& out) {
// here: mu_ is held, so the audio loop cannot be inside
// process() on this instance while getState runs.
i->captureState();
e.presetBase64 = i->serializeState();
std::string saved = i->serializeState();
e.presetBase64 = saved.size() >= 100 ? saved : std::string();
e.bank = i->bankOf(ch);
e.program = i->programOf(ch);
}
+297
View File
@@ -0,0 +1,297 @@
// native_bridge/src/SandboxVst2Host.cpp
#include "SandboxVst2Host.h"
#include <chrono>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
// Same escaping as SandboxVst3Host.cpp (F-PROC-1): CreateProcessA does not
// run a shell, but a path containing a double quote must not break out of
// --path "..." and inject arbitrary argv.
static std::string quote_arg(const std::string& s) {
std::string out = "\"";
size_t bs = 0;
for (char c : s) {
if (c == '\\') { ++bs; continue; }
if (c == '"') { out.append(bs * 2 + 1, '\\'); out += '"'; bs = 0; }
else { out.append(bs, '\\'); out += c; bs = 0; }
}
out.append(bs * 2, '\\');
out += '"';
return out;
}
static bool proc_alive(HANDLE h) {
#ifdef _WIN32
if (!h) return false;
DWORD code = 0;
if (!GetExitCodeProcess(h, &code)) return false;
return code == STILL_ACTIVE;
#else
return true;
#endif
}
// Unique SHM name per LOAD, not per channel: a same-channel reload (frontend
// LOAD retry spam) runs while the OLD child + mapping still exist - a name
// keyed only on channel would reopen the old mapping and two children would
// fight over one audio handoff. A per-load counter keeps each child isolated.
static std::atomic<uint32_t> g_seq{0};
SandboxVst2Host::SandboxVst2Host() {}
SandboxVst2Host::~SandboxVst2Host() {
stop_.store(true);
if (watchdog_.joinable()) watchdog_.join();
#ifdef _WIN32
if (childProc_) {
TerminateProcess(childProc_, 0);
CloseHandle(childProc_);
childProc_ = nullptr;
}
#endif
if (shm_) {
shm_close(shm_);
shm_ = nullptr;
ipc_ = nullptr;
}
}
bool SandboxVst2Host::loadPlugin(const std::string& path, double sampleRate, uint32_t channel) {
path_ = path;
sampleRate_ = sampleRate;
channel_ = channel;
char name[160];
snprintf(name, sizeof(name), "SonicForge_PluginHost2_%lu_%u_%u",
(unsigned long)GetCurrentProcessId(), channel, g_seq.fetch_add(1));
shmName_ = name;
if (!spawnChild()) return false;
seedDefaultPreset();
watchdog_ = std::thread([this]() { watchdogLoop(); });
return true;
}
bool SandboxVst2Host::spawnChild() {
if (shm_) {
shm_close(shm_);
shm_ = nullptr;
ipc_ = nullptr;
}
shm_ = shm_create(shmName_.c_str(), sizeof(SandboxHostIPC));
if (!shm_) {
std::cerr << "[SandboxVst2Host] shm_create failed ch=" << channel_ << std::endl;
return false;
}
ipc_ = shm_sandbox_ptr(shm_);
// plugin_host.exe sits next to daw_vst_bridge.exe (same dir, install/).
char exePath[MAX_PATH] = {};
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
std::string dir(exePath);
size_t slash = dir.find_last_of("\\/");
std::string hostExe = (slash == std::string::npos)
? "plugin_host.exe"
: dir.substr(0, slash + 1) + "plugin_host.exe";
std::string cmd = "--open --vst2 --channel " + std::to_string(channel_) +
" --shm " + shmName_ +
" --path " + quote_arg(path_) +
" --sr " + std::to_string((int)sampleRate_) +
" --block " + std::to_string(block_) +
" --parent " + std::to_string((unsigned long)GetCurrentProcessId());
std::cerr << "[SandboxVst2Host] spawn ch=" << channel_ << " " << cmd << std::endl;
STARTUPINFOA si = {};
si.cb = sizeof(si);
PROCESS_INFORMATION pi = {};
std::vector<char> buf(cmd.begin(), cmd.end());
buf.push_back('\0');
if (!CreateProcessA(hostExe.c_str(), buf.data(), nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
std::cerr << "[SandboxVst2Host] CreateProcessA failed err=" << (int)GetLastError()
<< " ch=" << channel_ << std::endl;
return false;
}
CloseHandle(pi.hThread);
#ifdef _WIN32
if (childProc_) CloseHandle(childProc_);
childProc_ = pi.hProcess;
#endif
// Wait for the child heartbeat (Qin load spawns auxhost + engine, can
// take tens of seconds).
uint32_t hb = ipc_->base.heartbeat;
auto t0 = std::chrono::steady_clock::now();
while (std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t0).count() < 90) {
if (!proc_alive(childProc_)) {
std::cerr << "[SandboxVst2Host] child exited during load ch=" << channel_ << std::endl;
return false;
}
if (ipc_->base.heartbeat != hb) {
alive_.store(true);
return true;
}
Sleep(200);
}
std::cerr << "[SandboxVst2Host] child heartbeat timeout ch=" << channel_ << std::endl;
return false;
}
void SandboxVst2Host::watchdogLoop() {
int fails = 0;
while (!stop_.load()) {
Sleep(500);
if (stop_.load()) break;
if (!alive_.load()) continue;
if (proc_alive(childProc_)) {
fails = 0;
continue;
}
alive_.store(false);
DWORD ec = 0; GetExitCodeProcess(childProc_, &ec);
std::cerr << "[SandboxVst2Host] child died ch=" << channel_ << " rc=" << ec << " - respawning" << std::endl;
++fails;
if (fails >= 3) {
std::cerr << "[SandboxVst2Host] ch=" << channel_
<< " respawn limit hit - muted until reload" << std::endl;
continue; // stays dead; processAudioBlock returns silence
}
Sleep(1000);
if (stop_.load()) break;
if (spawnChild()) {
fails = 0;
std::cerr << "[SandboxVst2Host] ch=" << channel_ << " respawned" << std::endl;
}
}
}
bool SandboxVst2Host::init(double sampleRate, uint32_t maxBlockSize) {
sampleRate_ = sampleRate;
block_ = maxBlockSize;
return true;
}
void SandboxVst2Host::pushControl(uint32_t type, uint32_t arg0, uint32_t arg1) {
if (!alive_.load() || !ipc_) return;
if (ipc_->base.controlQueueCount >= 8) return;
auto& c = ipc_->base.controlQueue[ipc_->base.controlQueueCount];
c.type = type;
c.arg0 = arg0;
c.arg1 = arg1;
c.channel = channel_;
std::memset(c.arg2, 0, sizeof(c.arg2));
ipc_->base.controlQueueCount++;
}
void SandboxVst2Host::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
if (!alive_.load() || !ipc_) return;
(void)channel; // one instrument per child; Qin listens only on MIDI ch 0
shm_write_midi(shm_, 0x9, 0, (uint8_t)pitch,
(uint8_t)(velocity * 127.0f), sampleOffset);
}
void SandboxVst2Host::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
if (!alive_.load() || !ipc_) return;
(void)channel; // see noteOn
shm_write_midi(shm_, 0x8, 0, (uint8_t)pitch, 0, sampleOffset);
}
void SandboxVst2Host::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
if (!alive_.load() || !ipc_) return;
(void)channel; // see noteOn
shm_write_midi(shm_, 0xB, 0, (uint8_t)cc, 0, 0, (uint8_t)value, 0);
}
void SandboxVst2Host::programChange(uint32_t channel, uint32_t program) {
if (!alive_.load() || !ipc_) return;
(void)channel; // see noteOn
shm_write_midi(shm_, 0xC, 0, 0, 0, 0, (uint8_t)program, 0);
}
void SandboxVst2Host::pitchBend(uint32_t channel, uint32_t bend14) {
if (!alive_.load() || !ipc_) return;
(void)channel; // see noteOn
shm_write_midi(shm_, 0xE, 0, 0, 0, 0,
(uint8_t)(bend14 & 0x7F), (uint8_t)((bend14 >> 7) & 0x7F));
}
bool SandboxVst2Host::openGUI(void* parentWindowHandle) {
(void)parentWindowHandle; // child owns its window
if (!alive_.load() || !ipc_) return false;
pushControl(4);
guiOpen_.store(true);
return true;
}
bool SandboxVst2Host::attachView(void* parentWindowHandle) {
return openGUI(parentWindowHandle);
}
void SandboxVst2Host::closeGUI() {
if (!alive_.load() || !ipc_) return;
pushControl(5);
guiOpen_.store(false);
}
void SandboxVst2Host::seedDefaultPreset() {
// Qin_RV renders SILENT from factory state (no instrument loaded).
// Seed a known-good captured chunk so preset-less loads still sound;
// the restore path writes the real saved preset to presetIn AFTER
// loadPlugin returns, so by ordering the real one wins.
if (!ipc_) return;
char buf[MAX_PATH];
if (!GetEnvironmentVariableA("APPDATA", buf, MAX_PATH)) return;
std::string file = std::string(buf) + "\\SonicForgeDAW\\qin_default.b64";
FILE* f = fopen(file.c_str(), "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= 0 || (size_t)sz >= PRESET_BLOB_SIZE) { fclose(f); return; }
std::string b64;
b64.resize((size_t)sz);
if (fread(&b64[0], 1, (size_t)sz, f) == (size_t)sz && !b64.empty()) {
memcpy((void*)ipc_->presetIn, b64.data(), b64.size());
ipc_->presetIn[b64.size()] = 0;
ipc_->presetInLen = (uint32_t)b64.size();
std::cerr << "[SandboxVst2Host] default preset seeded ch=" << channel_
<< " (" << b64.size() << "B)" << std::endl;
}
fclose(f);
}
void SandboxVst2Host::loadSerializedState(const std::string& base64) {
if (!alive_.load() || !ipc_ || base64.empty()) return;
size_t n = base64.size();
if (n >= PRESET_BLOB_SIZE) n = PRESET_BLOB_SIZE - 1;
memcpy((void*)ipc_->presetIn, base64.data(), n);
ipc_->presetIn[n] = 0;
ipc_->presetInLen = (uint32_t)n;
std::cerr << "[SandboxVst2Host] presetIn queued ch=" << channel_
<< " (" << n << "B)" << std::endl;
}
std::string SandboxVst2Host::serializeState() const {
if (!ipc_) return std::string();
uint32_t n = ipc_->presetOutLen;
if (n == 0 || n >= PRESET_BLOB_SIZE) return std::string();
return std::string(ipc_->presetOut, n);
}
void SandboxVst2Host::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
if (!alive_.load() || !ipc_) {
std::memset(outputL, 0, numSamples * sizeof(float));
std::memset(outputR, 0, numSamples * sizeof(float));
return;
}
// G4.3: the child publishes the slot it finished rendering (writeSlot).
// Copy that block. A stale slot means the child is still working on the
// other one - we repeat the previous block instead of tearing.
uint32_t slot = ipc_->writeSlot & 1u;
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] = ipc_->audioLeft[slot][i];
outputR[i] = ipc_->audioRight[slot][i];
}
}
+75
View File
@@ -0,0 +1,75 @@
// native_bridge/src/SandboxVst2Host.h
// G4.4: VST2 instrument (jBridge stubs like Qin_RV) hosted in a CHILD process.
// jBridge 1.6b3 allows only ONE instance of a given plugin per host process -
// the second instance's shared audio buffer already exists, so it stays
// silent. Each child process is the "first" Bridger64 instance for its own
// process, so any number of Qin tracks produce audio. The child owns its GUI
// window (opened on control 4 = OPEN_GUI).
#ifndef SANDBOX_VST2_HOST_H
#define SANDBOX_VST2_HOST_H
#include "INativeInstrument.h"
#include "SharedMemoryIPC.h"
#include <atomic>
#include <string>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#endif
class SandboxVst2Host : public INativeInstrument {
public:
SandboxVst2Host();
~SandboxVst2Host() override;
bool loadPlugin(const std::string& path, double sampleRate, uint32_t channel);
bool init(double sampleRate, uint32_t maxBlockSize) override;
void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override {}
void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override;
void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override;
void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override;
void programChange(uint32_t channel, uint32_t program) override;
void pitchBend(uint32_t channel, uint32_t bend14) override;
// The child owns its GUI window (plugin_host opens it on OPEN_GUI control).
bool openGUI(void* parentWindowHandle) override;
bool canOpenGUI() const override { return true; }
bool attachView(void* parentWindowHandle) override; // bridge OPEN_GUI job calls this
bool hasAttachedView() const override { return guiOpen_.load(); }
void closeGUI() override;
void guiIdle() override {} // child pumps effEditIdle itself
// G4.5: preset state lives in the child. The child republishes its
// chunk into presetOut (after load / presetIn apply / GUI edits);
// serializeState reads it. loadSerializedState writes presetIn for
// the child to apply on its next loop pass.
void captureState() override {}
void restoreState() override {}
std::string serializeState() const override;
void loadSerializedState(const std::string& base64) override;
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
bool spawnChild();
void watchdogLoop();
void pushControl(uint32_t type, uint32_t arg0 = 0, uint32_t arg1 = 0);
void seedDefaultPreset();
std::string path_;
std::string shmName_;
double sampleRate_ = 44100.0;
uint32_t block_ = AUDIO_BLOCK_SIZE;
uint32_t channel_ = 0;
ShmHandle* shm_ = nullptr;
SandboxHostIPC* ipc_ = nullptr;
std::atomic<bool> alive_{false};
std::atomic<bool> stop_{false};
std::atomic<bool> guiOpen_{false};
std::thread watchdog_;
#ifdef _WIN32
HANDLE childProc_ = nullptr;
#endif
};
#endif // SANDBOX_VST2_HOST_H
+1 -1
View File
@@ -1403,7 +1403,7 @@ int main(int argc, char* argv[]) {
if (!inst2) {
std::cerr << "[NativeBridge] restore lost inst ch=" << ch << std::endl;
} else {
if ((t == InstrumentType::VST3 || t == InstrumentType::VST2) && !preset.empty())
if ((t == InstrumentType::VST3 || t == InstrumentType::VST2) && preset.size() >= 100)
inst2->loadSerializedState(preset);
else
inst2->selectProgram(ch, bank, program);
+165 -30
View File
@@ -1,11 +1,19 @@
// 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.
// 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.
//
// argv: --shm <name> --path <vst3> [--sr <rate>] [--block <n>] [--parent <pid>]
// 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 <name> --path <vst> [--vst2] [--channel <n>] [--sr <rate>] [--block <n>] [--parent <pid>] [--open]
#include "Vst3Instrument.h"
#define MIN_PRESET 100 // b64 chars; real chunks are KBs
#include "Vst2Instrument.h"
#include "SharedMemoryIPC.h"
#ifdef _WIN32
@@ -30,12 +38,25 @@ static bool parent_alive(uint32_t pid) {
#endif
}
static HWND make_host_window() {
WNDCLASSEX wc = {};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = DefWindowProc;
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[]) {
std::string shmName, path;
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];
@@ -43,9 +64,12 @@ int main(int argc, char* argv[]) {
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 <name> --path <vst3> [--sr rate] [--block n] [--parent pid]\n");
printf("usage: plugin_host --shm <name> --path <vst> [--vst2] [--channel n] [--sr rate] [--block n] [--parent pid] [--open]\n");
return 2;
}
@@ -53,8 +77,8 @@ int main(int argc, char* argv[]) {
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);
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.
@@ -65,30 +89,72 @@ int main(int argc, char* argv[]) {
}
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;
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;
// 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);
}
// GUI on the main thread (editorhost pattern: attach then pump below).
// 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.
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);
if (!vst2) {
hwnd = make_host_window();
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);
}
// 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).
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();
printf("[plugin_host] initial preset published ch=%u (%uB)\n", channel, ipc->presetOutLen);
fflush(stdout);
}
}
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]() {
@@ -100,12 +166,81 @@ int main(int argc, char* argv[]) {
int activeNotes = 0;
bool idle = false;
bool quit = false;
uint32_t lastCaptureTick = 0;
std::string lastOut;
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 (!hwnd) hwnd = make_host_window();
if (hwnd && !inst.hasAttachedView()) {
printf("[plugin_host] OPEN_GUI ch=%u\n", channel);
fflush(stdout);
inst.openGUI(hwnd);
ShowWindow(hwnd, SW_SHOW);
}
} else if (c.type == 5) {
inst.closeGUI();
if (hwnd) ShowWindow(hwnd, SW_HIDE);
} else if (c.type == 6) {
quit = true;
}
}
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 {
std::string b64(ipc->presetIn, ipc->presetInLen);
inst.loadSerializedState(b64);
printf("[plugin_host] preset applied from SHM ch=%u (%uB)\n", channel, ipc->presetInLen);
fflush(stdout);
ipc->presetInLen = 0;
// 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;
}
}
}
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) {
+1 -1
View File
@@ -27,7 +27,7 @@
"binaries/libfluidsynth-3.dll": "libfluidsynth-3.dll",
"binaries/jpeg62.dll": "jpeg62.dll"
},
"externalBin": ["binaries/daw_vst_bridge", "binaries/fx_vst_bridge"],
"externalBin": ["binaries/daw_vst_bridge", "binaries/fx_vst_bridge", "binaries/plugin_host"],
"windows": {
"nsis": {
"installerHooks": "hooks.nsh"