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
+49 -14
View File
@@ -107,6 +107,19 @@ int main(int argc, char* argv[]) {
}
INativeInstrument& inst = vst2 ? (INativeInstrument&)instV2 : (INativeInstrument&)instV3;
// G4.6 auto-play fix: a loaded chunk can embed a stuck/sustained note
// (factory state, seed, or a patch captured mid-note). Release everything
// right after a preset apply so the channel starts silent instead of
// sounding on app open (STOP later releases it too, but startup must not
// play).
auto panicAfterPreset = [&inst, channel]() {
inst.controlChange(0, 64, 0); // sustain pedal off
inst.controlChange(0, 123, 0); // all notes off
for (uint32_t p = 0; p < 128; ++p) inst.noteOff(0, p, 0);
printf("[plugin_host] panic after preset ch=%u\n", channel);
fflush(stdout);
};
// 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.
@@ -145,15 +158,28 @@ int main(int argc, char* argv[]) {
// 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).
std::string lastOut;
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;
printf("[plugin_host] initial preset published ch=%u (%uB)\n", channel, ipc->presetOutLen);
fflush(stdout);
}
// G4.6 auto-play fix (clobber-safe): release stuck notes AFTER the real
// chunk is already published, then absorb the panic-mutated chunk so the
// periodic republish never echoes it back over presetOut. Startup stays
// silent AND the state file keeps the real patch.
panicAfterPreset();
inst.captureState();
{
std::string postPanic = inst.serializeState();
if (!postPanic.empty()) lastOut = postPanic;
}
}
// Heartbeat ~10Hz, independent of the audio loop (G3.1 pattern).
@@ -168,7 +194,6 @@ int main(int argc, char* argv[]) {
bool idle = false;
bool quit = false;
uint32_t lastCaptureTick = 0;
std::string lastOut;
for (;;) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
@@ -213,19 +238,29 @@ int main(int argc, char* argv[]) {
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;
}
// consume-before-apply: copy and zero presetInLen FIRST so the
// bridge can safely queue the next preset while this plugin
// applies the current one (prevents the restore clobber race).
uint32_t plen = ipc->presetInLen;
std::string b64(ipc->presetIn, plen);
ipc->presetInLen = 0;
inst.loadSerializedState(b64);
printf("[plugin_host] preset applied from SHM ch=%u (%uB)\n", channel, (unsigned)plen);
fflush(stdout);
// 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;
}
panicAfterPreset();
// absorb the panic-mutated chunk so the ~2s periodic republish
// never overwrites presetOut with it
inst.captureState();
std::string postPanic = inst.serializeState();
if (!postPanic.empty()) lastOut = postPanic;
}
}
if (++lastCaptureTick >= 200) { // ~2s of loop passes