Files
SonicForgeStudio/native_bridge/src/SandboxVst2Host.cpp
T
admin a7b01d3484 fix(bridge): 3 bug âm thanh Qin/Nexus standalone
1) wavesynth tự phát khi mở app: probe calibrate() render vào live mix
   (renderAll process cùng instance) -> thêm calibratingCh_[16], renderAll
   bỏ qua channel đang calibrate; plugin_host panic (sustain off + all
   notes off + 128 noteOff) ngay sau preset apply để startup im lặng.
2) bấm GUI mở 2 cửa sổ Qin: sandbox VST2 child tự sở hữu top-level window
   -> ownsGuiWindow() + main.cpp forward OPEN_GUI control, không tạo native
   parent / không registry double-open.
3) play phát nhạc cụ sai: restore clobber — seed preset đè patch thật trong
   state file (ch3 63ba3667 -> 7f07473c) do saveStateJob chạy lúc child còn
   seed. Fix: queuePreset(waitApply=true) chờ child apply xong (presetOut
   memcmp), consume-before-apply (zero presetInLen trước khi apply), deferred
   double-load re-check trong main.cpp skip assign trùng path, watchdog
   respawn re-queue lastPreset_.
2026-08-21 08:50:28 +07:00

333 lines
12 KiB
C++

// 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;
// G4.6: respawned child starts from FACTORY state — re-queue the
// last preset (real patch or seed) so it does not publish/sound
// silent factory and clobber the state file.
if (!lastPreset_.empty()) {
queuePreset(lastPreset_);
std::cerr << "[SandboxVst2Host] preset re-queued after respawn ch="
<< channel_ << " (" << lastPreset_.size() << "B)" << std::endl;
}
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()) {
lastPreset_ = b64;
queuePreset(b64);
std::cerr << "[SandboxVst2Host] default preset seeded ch=" << channel_
<< " (" << b64.size() << "B)" << std::endl;
}
fclose(f);
}
void SandboxVst2Host::queuePreset(const std::string& base64, bool waitApply) {
if (!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;
// waitApply: block until the child has APPLIED this preset (presetOut
// echoes the chunk) before returning. The restore path queues the real
// saved preset right after spawn; without this wait a saveStateJob can
// fire while the child is still applying the seed default and persist the
// seed over the real chunk (restore clobber). Qin round-trips
// byte-identical; 20s timeout guards a stuck child.
if (!waitApply) return;
if (ipc_->presetOutLen == (uint32_t)n &&
memcmp(ipc_->presetOut, base64.data(), n) == 0) return;
auto t0 = std::chrono::steady_clock::now();
while (std::chrono::steady_clock::now() - t0 < std::chrono::seconds(20)) {
if (ipc_->presetOutLen == (uint32_t)n &&
memcmp(ipc_->presetOut, base64.data(), n) == 0) break;
Sleep(50);
}
bool confirmed = ipc_->presetOutLen == (uint32_t)n &&
memcmp(ipc_->presetOut, base64.data(), n) == 0;
std::cerr << "[SandboxVst2Host] preset apply waited ch=" << channel_
<< " (" << base64.size() << "B)"
<< (confirmed ? " confirmed" : " TIMEOUT")
<< std::endl;
}
void SandboxVst2Host::loadSerializedState(const std::string& base64) {
if (base64.empty()) return;
lastPreset_ = base64; // keep for watchdog respawn re-queue
if (!alive_.load() || !ipc_) return;
queuePreset(base64, true); // wait for apply so early saves see the real chunk
std::cerr << "[SandboxVst2Host] presetIn queued ch=" << channel_
<< " (" << base64.size() << "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];
}
}