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:
@@ -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/
|
||||
|
||||
@@ -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) =="
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user