diff --git a/native_bridge/include/INativeInstrument.h b/native_bridge/include/INativeInstrument.h index 20a002f..9ee23b5 100644 --- a/native_bridge/include/INativeInstrument.h +++ b/native_bridge/include/INativeInstrument.h @@ -36,6 +36,10 @@ public: // Headless engines (VST2 without an editor, SF2/SFZ): the bridge must // not create/reuse a window for them. virtual bool canOpenGUI() const { return true; } + // Sandbox hosts whose child owns its own top-level window (sandbox VST2): + // the bridge must not create a parent native window nor close other + // editors — attachView(nullptr) just forwards the OPEN_GUI control. + virtual bool ownsGuiWindow() const { return false; } virtual void closeGUI() = 0; // VST3 reopen-after-close: the bridge checks before openGUI whether the // instance must be rebuilt; while it is, the engine sets reloading=true diff --git a/native_bridge/include/NativeInstrumentEngine.h b/native_bridge/include/NativeInstrumentEngine.h index d1a37dc..0ece99a 100644 --- a/native_bridge/include/NativeInstrumentEngine.h +++ b/native_bridge/include/NativeInstrumentEngine.h @@ -177,6 +177,7 @@ private: // Per-instrument scratch so engines that overwrite (not mix) stay additive. std::vector scratchL_, scratchR_; // Channel-level reload flag (see isReloading) — indexed by MIDI channel. + bool calibratingCh_[16] = {}; // probe-render mute: renderAll skips channel during calibrate() bool reloadingCh_[16] = {}; // G3.3: channel muted after a plugin fault inside processAudioBlock. bool crashedCh_[16] = {}; @@ -188,4 +189,4 @@ private: std::atomic isBypassed_{false}; }; -#endif // NATIVE_INSTRUMENT_ENGINE_H +#endif // NATIVE_INSTRUMENT_ENGINE_H \ No newline at end of file diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index ba8e737..5af6911 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -313,9 +313,16 @@ float InstrumentEngineManager::recalibrate(uint32_t channel, double sampleRate, } if (!inst) return 1.0f; // calibrate() probe-renders (up to ~0.9s) - never hold mu_ (audio stall). + // Flag the channel so renderAll skips it: the probe notes would otherwise + // leak into the live mix (auto-play at startup, bug 1). + { + std::lock_guard lock(mu_); + if (channel < 16) calibratingCh_[channel] = true; + } const float mk = calibrate(inst, sampleRate, blockSize); { std::lock_guard lock(mu_); + if (channel < 16) calibratingCh_[channel] = false; makeup_[channel] = mk; } return mk; @@ -421,7 +428,7 @@ bool InstrumentEngineManager::isCrashed(uint32_t channel) const { // khong xac dinh sau fault; xu ly tiep se fault lai. // Called with mu_ held by the real-time dispatch. bool InstrumentEngineManager::channelQuiet(uint32_t ch) const { - if (ch >= 16 || reloadingCh_[ch] || crashedCh_[ch]) return true; + if (ch >= 16 || reloadingCh_[ch] || crashedCh_[ch] || calibratingCh_[ch]) return true; return false; } @@ -684,4 +691,4 @@ void InstrumentEngineManager::guiIdleAll() { for (auto& kv : channels_) { kv.second->guiIdle(); } -} +} \ No newline at end of file diff --git a/native_bridge/src/SandboxVst2Host.cpp b/native_bridge/src/SandboxVst2Host.cpp index 79e6c3d..d92903c 100644 --- a/native_bridge/src/SandboxVst2Host.cpp +++ b/native_bridge/src/SandboxVst2Host.cpp @@ -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 { diff --git a/native_bridge/src/SandboxVst2Host.h b/native_bridge/src/SandboxVst2Host.h index 49a2ea8..e4c6711 100644 --- a/native_bridge/src/SandboxVst2Host.h +++ b/native_bridge/src/SandboxVst2Host.h @@ -36,6 +36,7 @@ public: // 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 ownsGuiWindow() const override { return true; } // child owns its top-level window bool attachView(void* parentWindowHandle) override; // bridge OPEN_GUI job calls this bool hasAttachedView() const override { return guiOpen_.load(); } void closeGUI() override; @@ -55,9 +56,11 @@ private: void watchdogLoop(); void pushControl(uint32_t type, uint32_t arg0 = 0, uint32_t arg1 = 0); void seedDefaultPreset(); + void queuePreset(const std::string& base64, bool waitApply = false); std::string path_; std::string shmName_; + std::string lastPreset_; // last preset queued (seed or real patch) — re-queued on watchdog respawn double sampleRate_ = 44100.0; uint32_t block_ = AUDIO_BLOCK_SIZE; uint32_t channel_ = 0; diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 56c9a50..6e67f93 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -1129,6 +1129,24 @@ int main(int argc, char* argv[]) { return; } } + // G4.6: child-own GUI (sandbox VST2 like Qin). The child process + // opens its OWN top-level window on control 4 — no native parent + // window to create, no g_guiWindows registry, no same-path + // silencing, no closing of other editors (separate process; + // nothing to serialize). Just forward the OPEN_GUI control. + if (auto* inst0 = instruments.get(guiCh)) { + if (inst0->ownsGuiWindow()) { + std::cerr << "[NativeBridge] GUI via child-own ch=" << guiCh + << " plugin=" << arg2 << std::endl; + try { + inst0->attachView(nullptr); + } catch (...) { + std::cerr << "[NativeBridge] GUI child-own attach EXCEPTION ch=" << guiCh + << std::endl; + } + return; + } + } void* hwnd = (void*)arg1; #ifdef _WIN32 HWND nativeHwnd = nullptr; @@ -1577,6 +1595,23 @@ int main(int argc, char* argv[]) { auto doAssign = std::function( [&instruments, t, ch, path, sampleRate, block, spOld, spNew]() { std::lock_guard lg(g_loadMutex); + // G4.6: deferred double-load re-check. Restore's + // doRestore (also deferred via runOnMain) can assign + // the SAME path FIRST — a second assign here replaces + // the restored instance with a seed-only one (doAssign + // applies NO preset) and clobbers the real patch in + // the state file (ch3 63ba3667 -> 7f07473c observed). + // Skip like the job-level guard; undo the silence. + if (!path.empty() && !lower_plugin_path(ch).empty() && + lower_plugin_path(ch) == lower_path_str(path)) { + std::cout << "[NativeBridge] LOAD skip (deferred re-check) ch=" << ch + << " path=" << path << std::endl; + g_engine->setReloading(ch, false); + for (uint32_t y : spOld) unmute_if_not_closing(y, "load-skip"); + for (uint32_t y : spNew) unmute_if_not_closing(y, "load-skip"); + g_stateDirty = true; + return; + } bool ok = false; try { ok = instruments.assign(ch, t, path, sampleRate, block); diff --git a/native_bridge/src/plugin_host_main.cpp b/native_bridge/src/plugin_host_main.cpp index 9922443..41eddb3 100644 --- a/native_bridge/src/plugin_host_main.cpp +++ b/native_bridge/src/plugin_host_main.cpp @@ -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