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:
@@ -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
|
||||
|
||||
@@ -177,6 +177,7 @@ private:
|
||||
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
|
||||
std::vector<float> 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<bool> isBypassed_{false};
|
||||
};
|
||||
|
||||
#endif // NATIVE_INSTRUMENT_ENGINE_H
|
||||
#endif // NATIVE_INSTRUMENT_ENGINE_H
|
||||
@@ -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<std::mutex> lock(mu_);
|
||||
if (channel < 16) calibratingCh_[channel] = true;
|
||||
}
|
||||
const float mk = calibrate(inst, sampleRate, blockSize);
|
||||
{
|
||||
std::lock_guard<std::mutex> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void()>(
|
||||
[&instruments, t, ch, path, sampleRate, block, spOld, spNew]() {
|
||||
std::lock_guard<std::mutex> 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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user