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_.
This commit is contained in:
2026-08-21 08:50:28 +07:00
parent b91e6d3875
commit a7b01d3484
7 changed files with 143 additions and 23 deletions
+41 -6
View File
@@ -162,6 +162,14 @@ void SandboxVst2Host::watchdogLoop() {
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;
}
}
@@ -253,24 +261,51 @@ void SandboxVst2Host::seedDefaultPreset() {
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();
lastPreset_ = b64;
queuePreset(b64);
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;
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_
<< " (" << n << "B)" << std::endl;
<< " (" << base64.size() << "B)" << std::endl;
}
std::string SandboxVst2Host::serializeState() const {