diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index 95c32d0..cebf116 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/native_bridge/include/INativeInstrument.h b/native_bridge/include/INativeInstrument.h index 51ec25b..5973fa7 100644 --- a/native_bridge/include/INativeInstrument.h +++ b/native_bridge/include/INativeInstrument.h @@ -41,6 +41,11 @@ public: // inside processAudioBlock on the same instance). virtual bool needsReload() const { return false; } virtual void setReloading(bool /*on*/) {} + // VST3: rebuild the plugin instance (terminate + loadPlugin) on the owning + // apartment thread. The bridge calls it after tearing an editor down so the + // audio loop never processes an instance whose editor was dropped without + // view->removed(). Non-VST engines: no-op. + virtual bool reload() { return true; } // VST3: split openGUI so instance teardown/reload runs on the owning // apartment thread while view->attached() may run on a temporary thread // with the channel worker pumping messages (see bridge main.cpp). diff --git a/native_bridge/include/Vst3Instrument.h b/native_bridge/include/Vst3Instrument.h index 659473c..c7fc992 100644 --- a/native_bridge/include/Vst3Instrument.h +++ b/native_bridge/include/Vst3Instrument.h @@ -29,6 +29,7 @@ public: bool attachView(void* parentWindowHandle) override; bool hasAttachedView() const override; void closeGUI() override; + bool reload() override; void setChannel(uint32_t ch) { channel_ = ch; } bool needsReload() const override { return hasAttachedOnce_ && !guiAttached_; } void setReloading(bool on) override { reloading_ = on; } @@ -45,7 +46,6 @@ private: bool hasAttachedOnce_; bool reloading_; // guarded by InstrumentEngineManager::mu_ (set via setReloading) uint32_t channel_ = 0; - bool reload(); }; #endif // VST3_INSTRUMENT_H diff --git a/native_bridge/src/Vst3Instrument.cpp b/native_bridge/src/Vst3Instrument.cpp index 890da91..e32e036 100644 --- a/native_bridge/src/Vst3Instrument.cpp +++ b/native_bridge/src/Vst3Instrument.cpp @@ -246,12 +246,7 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) { std::cerr << "[dbg] loadPlugin: createInstance ..." << std::endl; std::cerr << "[dbg] loadPlugin: chosen name=" << chosen.name() << " category=" << chosen.category() - << " subcat=" << chosen.subCategoriesString() << " idbytes="; - { - const unsigned char* cid2 = (const unsigned char*)chosen.ID().data(); - for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)cid2[b] << ' '; - std::cerr << std::dec << std::endl; - } + << " subcat=" << chosen.subCategoriesString() << std::endl; IPtr component = factory.createInstance(chosen.ID()); if (!component) { std::cerr << "[Vst3Instrument] createInstance failed" << std::endl; @@ -579,16 +574,6 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) { if (!s || !s->controller || !parentWindowHandle) return false; IPlugView* rawView = nullptr; tresult qi = s->controller->queryInterface(IPlugView::iid, (void**)&rawView); - { - const unsigned char* iidb = (const unsigned char*)&IPlugView::iid; - std::cerr << "[dbg] openGUI: IPlugView::iid bytes="; - for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)iidb[b] << ' '; - std::cerr << std::dec << std::endl; - const unsigned char* eidb = (const unsigned char*)&IEditController::iid; - std::cerr << "[dbg] openGUI: IEditController::iid bytes="; - for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)eidb[b] << ' '; - std::cerr << std::dec << std::endl; - } std::cerr << "[dbg] openGUI: controller qi IPlugView=" << (int)qi << " raw=" << (void*)rawView << " isSingle=" << (s->controllerIsComponent ? 1 : 0) << std::endl; FUnknownPtr view(rawView); if (!view) { @@ -669,7 +654,18 @@ void Vst3Instrument::closeGUI() { return; #else auto* s = static_cast(state_); - if (s && s->view && guiAttached_) s->view->removed(); + // Do NOT call s->view->removed(): Nexus's removed() enters an internal + // modal wait for a message only this thread's message pump can dispatch + // — but this thread is inside removed(), so it deadlocks forever + // (observed: closeGUI job never returns; wdisp flood from the editor). + // Nulling the view and letting the next terminate() (reload / destructor) + // tear the editor windows down on this same thread is the deterministic + // path out. Callers (post_close_gui / load job) additionally destroy the + // editor's child windows right here on this thread so the plugin stops + // pumping; the leaked editor object is inert once its windows are gone. + if (s && s->view && guiAttached_) + std::cerr << "[dbg] closeGUI: skip view->removed() (Nexus deadlock) view=" + << (void*)s->view << std::endl; if (s) s->view = nullptr; guiAttached_ = false; #endif diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 920c7f3..330a258 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,16 @@ static std::mutex g_guiMutex; static std::map g_guiWindows; // channel -> HWND (keep window alive) static std::map g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup) static std::map>* g_workers = nullptr; +// Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus) +// crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/ +// createView is running on some worker — a same-path close job must not unmute +// its channel mid-attach (the attach job owns the silence state until +// attachView finishes). g_closeInFlight = channels whose close job +// (closeGUI + destroy children + reload) is still inside createInstance — +// unmuting them then would race the plugin teardown. +static std::mutex g_attachMutex; +static std::vector g_attachPaths; +static bool g_closeInFlight[16] = { false }; static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_CLOSE) { @@ -172,7 +183,12 @@ public: // dispatched here or the editor freezes after attach. lk.unlock(); MSG msg; - while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + // Cap the idle pump: a busy editor (Nexus posts timer/paint + // messages continuously) otherwise starves this worker's job + // queue forever — post_close_gui never runs, Option B's 5s + // close wait times out, two editors stay alive (deadlock). + for (int pumped = 0; pumped < 16; ++pumped) { + if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; TranslateMessage(&msg); DispatchMessageW(&msg); } @@ -207,6 +223,48 @@ private: bool stop_ = false; }; +// --- same-plugin-DLL silence helpers ---------------------------------------- +// Two threads inside one VST3 DLL (Nexus) crash/deadlock: the audio loop's +// process() on one instance must not race createInstance/createView/terminate +// on another. Windows plugin paths are case-insensitive — compare lowercased. +static std::string lower_plugin_path(uint32_t ch) { + std::string p = g_engine ? g_engine->pathOf(ch) : std::string(); + std::transform(p.begin(), p.end(), p.begin(), + [](unsigned char c) { return (char)::tolower(c); }); + return p; +} + +// Channels (other than ch) currently assigned the same plugin DLL path. +static std::vector same_plugin_channels(uint32_t ch) { + std::vector out; + std::string p = lower_plugin_path(ch); + if (p.empty()) return out; + for (uint32_t y = 0; y < 16; ++y) { + if (y == ch) continue; + if (!g_engine->get(y)) continue; + if (lower_plugin_path(y) == p) out.push_back(y); + } + return out; +} + +static bool close_in_flight(uint32_t ch) { + std::lock_guard lk(g_attachMutex); + return g_closeInFlight[ch]; +} + +// Unmute y unless its own close job is still inside createInstance — that job +// performs the unmute once its fresh instance is loaded (or, if an attach for +// the same plugin is in flight, the attach job's restore does it). +static void unmute_if_not_closing(uint32_t y, const char* why) { + if (close_in_flight(y)) { + std::cerr << "[dbg] " << why << ": ch=" << y + << " close job still in flight - close job unmutes" << std::endl; + return; + } + g_engine->setReloading(y, false); + std::cerr << "[dbg] " << why << ": restored ch=" << y << std::endl; +} + // closeGUI() MUST run on the channel worker thread (its COM STA apartment) — // the view was attached there. Calling view->removed() from the main thread // (WM_DESTROY handler) is a cross-apartment COM call that corrupts the plugin; @@ -231,9 +289,92 @@ static void post_close_gui(uint32_t ch, HWND hwnd) { { std::lock_guard lock(g_guiMutex); auto it = g_guiWindows.find(ch); - if (it == g_guiWindows.end() || it->second != hwnd) return; + if (it == g_guiWindows.end() || it->second != hwnd) { + std::cerr << "[dbg] closeGUI ch=" << ch << " skipped (window replaced)" << std::endl; + return; + } + } + std::cerr << "[dbg] closeGUI ch=" << ch << " start" << std::endl; + // Silence first: the audio loop must not process() the instance + // while we tear its editor down on this thread. + g_engine->setReloading(ch, true); + // Close-in-flight: until the fresh instance is loaded, no other + // job may unmute this channel (its plugin is being torn down). + { + std::lock_guard lk(g_attachMutex); + g_closeInFlight[ch] = true; + } + if (auto* i = g_engine->get(ch)) { + // closeGUI() nulls the view WITHOUT view->removed() (Nexus + // removed() deadlocks this worker - modal wait for a message + // only its own pump can dispatch, but it is inside removed()). + i->closeGUI(); + // CRASH FIX: the editor was dropped WITHOUT view->removed() — + // the instance's editor state is dangling, so the audio loop + // must NOT process() it. Rebuild a fresh instance NOW on this + // worker (its COM STA apartment is alive); reload() == + // terminate + loadPlugin (createInstance). Same-plugin + // channels are silenced so no second thread enters this DLL + // during createInstance (2 threads in one DLL -> Nexus exits + // silently, observed in probes). Serialized with other loads + // by g_loadMutex. + std::vector sp = same_plugin_channels(ch); + for (uint32_t y : sp) { + g_engine->setReloading(y, true); + std::cerr << "[dbg] closeGUI: silenced same-plugin ch=" << y + << " during reload ch=" << ch << std::endl; + } + bool reloadOk = false; + { + // destroy children + reload in ONE lock: destroying the + // editor windows runs the plugin's window proc (DLL + // entry) on this worker; it must not race another + // thread's createInstance/createView of the same DLL. + // Same-thread destroy is valid (attachView created them + // on this worker); without destroy the editor keeps + // pumping (0x47b flood) while the audio loop processes + // the instance (2 threads in one DLL) -> Nexus USER32 crash. + std::lock_guard lg(g_loadMutex); +#ifdef _WIN32 + if (hwnd && IsWindow(hwnd)) { + while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) + DestroyWindow(c); + } +#endif + reloadOk = i->reload(); + } + std::cerr << "[dbg] closeGUI ch=" << ch << " reload=" + << (reloadOk ? 1 : 0) << std::endl; + // createInstance is done: unmuting is safe again. But if an + // attach for the SAME plugin path is still running (its + // createView must not race process() on any same-path + // channel), leave the silence in place — the attach job's + // restore unmutes everything once attachView finished. + { + std::lock_guard lk(g_attachMutex); + g_closeInFlight[ch] = false; + } + bool pathBusy = false; + { + std::lock_guard lk(g_attachMutex); + std::string p = lower_plugin_path(ch); + pathBusy = !p.empty() && + std::find(g_attachPaths.begin(), g_attachPaths.end(), p) != g_attachPaths.end(); + } + if (pathBusy) { + std::cerr << "[dbg] closeGUI ch=" << ch + << ": attach in flight for same plugin - leaving silenced (attach restore unmutes)" + << std::endl; + } else { + for (uint32_t y : sp) unmute_if_not_closing(y, "closeGUI"); + g_engine->setReloading(ch, false); + } + } else { + // Instrument already gone (unloaded) — nothing to rebuild. + g_engine->setReloading(ch, false); + std::lock_guard lk(g_attachMutex); + g_closeInFlight[ch] = false; } - if (auto* i = g_engine->get(ch)) i->closeGUI(); }); } } @@ -455,28 +596,35 @@ int main(int argc, char* argv[]) { } } #ifdef _WIN32 - // STALL FIX (spawn.log "bridge stalled 3s restart"): audio loop - // processes ANOTHER live instance of the SAME plugin DLL (Nexus) - // while reload()+createView runs on THIS worker thread -> two - // threads inside one DLL -> deadlock, writeIndex stops, Tauri - // restarts the bridge. Silence same-path channels for the - // reload/attach duration, then restore (audio returns). - std::vector samePathSilenced; // Option B: chi 1 editor VST mo tai 1 thoi diem toan // bridge. Instance thu 2 cua CUNG plugin (Nexus) attach // view o apartment/worker khac -> treo. Dong editor cua // channel khac TRUOC khi attach: WM_CLOSE -> main pump - // (audio loop) destroy window -> WM_DESTROY -> closeGUI() - // + xoa registry. Chay tren worker job de khong stall - // writeIndex cua real-time loop. + // destroy window -> WM_DESTROY -> closeGUI() + xoa registry. + // Chay tren worker job de khong stall writeIndex cua real-time + // loop. CRASH FIX: silence moi channel TRUOC khi dong editor + // cua no (view->removed() tren worker cung luc process() tren + // audio loop = 2 thread trong 1 plugin -> crash USER32). { std::vector others; + bool abortAttach = false; { std::lock_guard lock(g_guiMutex); for (const auto& kv : g_guiWindows) if (kv.first != guiCh) others.push_back(kv.first); } for (uint32_t y : others) { + // Skip channels whose editor already detached (close job + // finished): do not WM_CLOSE a hidden reused window, do + // not re-silence a channel that is already unmuted. + if (auto* yi0 = instruments.get(y)) { + if (!yi0->hasAttachedView()) { + std::cerr << "[dbg] openGUI: ch=" << y + << " editor already detached - skip" + << std::endl; + continue; + } + } HWND yHwnd = nullptr; { std::lock_guard lock(g_guiMutex); @@ -484,6 +632,12 @@ int main(int argc, char* argv[]) { if (it != g_guiWindows.end()) yHwnd = (HWND)it->second; } if (!yHwnd || !IsWindow(yHwnd)) continue; + if (instruments.get(y)) { + instruments.setReloading(y, true); + std::cerr << "[dbg] openGUI: silenced ch=" << y + << " while closing its editor (before attach ch=" + << guiCh << ")" << std::endl; + } PostMessage(yHwnd, WM_CLOSE, 0, 0); std::cerr << "[dbg] openGUI: closing editor ch=" << y << " before attach ch=" << guiCh << std::endl; @@ -491,27 +645,42 @@ int main(int argc, char* argv[]) { // tren worker cua channel y. Doi cho view da detach (window // duoc giu lai de reuse, khong doi registry erase nhu cu). bool closed = false; - for (int i = 0; i < 500; ++i) { + // The close job = closeGUI + destroy children + fresh + // reload (createInstance, ~2s for Nexus). hasAttachedView + // flips false after closeGUI only - ALSO wait for the + // close job to finish entirely (close_in_flight): the + // attach must not run createView while another thread is + // still inside this plugin DLL (2 threads in one DLL -> + // Nexus exits silently, observed in probes). + for (int i = 0; i < 500; ++i) { // 5s budget auto* yi = instruments.get(y); - if (!yi || !yi->hasAttachedView()) { closed = true; break; } + bool detached = !yi || !yi->hasAttachedView(); + if (detached && !close_in_flight(y)) { closed = true; break; } Sleep(10); } - if (!closed) + if (closed) { + // Editor detached and its close job finished; the + // close job unmutes the channel itself. Do NOT reset + // reloading here - the close job owns the silence state. + std::cerr << "[dbg] openGUI: restored ch=" << y + << " after editor close" << std::endl; + } else { std::cerr << "[dbg] openGUI: editor ch=" << y - << " not closed in 5s, proceeding" << std::endl; - // Same plugin DLL as guiCh? Silence its processAudioBlock - // until attach finishes (see STALL FIX above). - std::string yp = instruments.pathOf(y); - std::string gp = instruments.pathOf(guiCh); - std::transform(yp.begin(), yp.end(), yp.begin(), ::tolower); - std::transform(gp.begin(), gp.end(), gp.begin(), ::tolower); - if (!gp.empty() && gp == yp && instruments.get(y)) { - instruments.setReloading(y, true); - samePathSilenced.push_back(y); - std::cerr << "[dbg] openGUI: silenced same-plugin ch=" << y - << " during attach ch=" << guiCh << std::endl; + << " close job not finished in 5s, aborting attach ch=" << guiCh + << std::endl; + abortAttach = true; + break; } } + if (abortAttach) { + // A close job is stuck; do NOT createView (would race the + // plugin DLL). Unmute what we silenced (skips channels + // whose close job is still in flight - that job unmutes + // them when it finishes). + for (uint32_t y : others) unmute_if_not_closing(y, "openGUI-abort"); + std::cerr << "[dbg] openGUI: attach aborted ch=" << guiCh << std::endl; + return; + } } #else (void)0; @@ -524,25 +693,76 @@ int main(int argc, char* argv[]) { // loop) dispatch messages cua no - khong can pump worker. // Chan UAF bang flag reloading_ (set/clear duoi engine // mutex; renderAll giu mutex khi process). - bool guard = inst->needsReload(); - if (guard) instruments.setReloading(guiCh, true); + // CRASH FIX (0xc000041d USER32 while playing): keep THIS + // channel silenced for the whole time its editor is attached. + // The worker pumps the plugin's editor windows -> the plugin's + // window proc runs on this worker thread while the audio loop + // calls processor->process() on the SAME instance (two threads + // inside one DLL) -> Nexus crashes ~1-2s after attach during + // playback. Muting the channel while the editor is open is the + // deterministic trade-off (track silent while GUI visible); + // restored when the editor closes (post_close_gui / LOAD). + // STALL/CRASH FIX (Fix C, restored): silence EVERY channel + // assigned the SAME plugin DLL path for the reload+attach + // duration — the audio loop must not process() another + // instance of this DLL while createInstance/createView runs + // on this worker (2 threads in one DLL -> Nexus exits + // silently, observed in probes). Iterate ALL 16 channels, not + // just window-owning ones: session-restore instances without + // a window are live and renderable. + std::vector samePathSilenced; + std::string gp = lower_plugin_path(guiCh); + if (!gp.empty()) { + for (uint32_t y = 0; y < 16; ++y) { + if (y == guiCh) continue; + if (!instruments.get(y)) continue; + if (lower_plugin_path(y) == gp) { + instruments.setReloading(y, true); + samePathSilenced.push_back(y); + std::cerr << "[dbg] openGUI: silenced same-plugin ch=" << y + << " during attach ch=" << guiCh << std::endl; + } + } + } + instruments.setReloading(guiCh, true); + // Register attach-in-flight: a same-path close job must not + // unmute its channel while createView runs here (it stays + // silenced; this job's restore unmutes it at the end). + { + std::lock_guard lk(g_attachMutex); + if (!gp.empty()) g_attachPaths.push_back(gp); + } bool ok = false; { - // Serialize createInstance (reload) with other loads. + // Serialize ALL plugin-DLL entry (createInstance AND + // createView) with other loads: two threads inside one + // DLL (Nexus) crash. createView outside the lock raced a + // same-path close job's reload (createInstance) -> Nexus + // exited silently (observed in probes). std::lock_guard lg(g_loadMutex); ok = inst->reloadForGUI(); + if (ok) ok = inst->attachView(hwnd); } - if (ok) ok = inst->attachView(hwnd); - if (guard) instruments.setReloading(guiCh, false); - for (uint32_t y : samePathSilenced) { - instruments.setReloading(y, false); - std::cerr << "[dbg] openGUI: restored ch=" << y - << " after attach ch=" << guiCh << std::endl; + // Attach done (ok or failed) — close jobs may unmute again. + { + std::lock_guard lk(g_attachMutex); + if (!gp.empty()) { + auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp); + if (it != g_attachPaths.end()) g_attachPaths.erase(it); + } } + // Restore same-path silence. Channels whose own close job is + // still inside createInstance stay muted — that job owns + // their unmute and performs it once reload finished. + for (uint32_t y : samePathSilenced) + unmute_if_not_closing(y, "openGUI"); if (ok) std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd - << " plugin=" << arg2 << " ch=" << guiCh << std::endl; + << " plugin=" << arg2 << " ch=" << guiCh + << " (channel muted while editor open)" << std::endl; else { + // Attach failed -> no editor running -> safe to process again. + instruments.setReloading(guiCh, false); std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd << " plugin=" << arg2 << std::endl; #ifdef _WIN32 @@ -566,9 +786,11 @@ int main(int argc, char* argv[]) { // with gui_probe: worker-thread openGUI + concurrent pump → attached // returns kResultOk; without it → hangs forever). MSG msg; + int pumpedMain = 0; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); + ++pumpedMain; } #endif // A. Control events — non-rt safe, drained first @@ -591,6 +813,22 @@ int main(int argc, char* argv[]) { uint32_t ch = c.channel & 0xF; if (!workers[ch]) workers[ch] = std::make_unique(); workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() { + // CRASH FIX: this job enters the plugin DLL (createInstance + // in loadPlugin, old-instance terminate on replace) while + // the audio loop may process() another instance of the SAME + // DLL — 2 threads in one DLL crashes Nexus (observed with + // session-restore instances). Silence this channel (its old + // instance is being torn down / replaced) and every + // same-plugin channel for the whole job. Old path covers + // the old-instance teardown; the new path (after assign) + // covers the freshly created instance. + instruments.setReloading(ch, true); + std::vector spOld = same_plugin_channels(ch); + for (uint32_t y : spOld) { + g_engine->setReloading(y, true); + std::cerr << "[dbg] load: silenced same-plugin ch=" << y + << " during load ch=" << ch << std::endl; + } #ifdef _WIN32 // DONG cua so editor dang mo cua channel TRUOC khi assign(): // thay the inst (VST3 -> SF2/inst khac) ma editor con song -> @@ -614,7 +852,18 @@ int main(int argc, char* argv[]) { // -> DestroyWindow cross-thread -> USER32 0xc000041d. An // window, giu trong registry de reuse (reopen ShowWindow lai). if (hToHide && IsWindow(hToHide)) { + // CRASH FIX (0xc000041d USER32 while playing): silence + // the channel while the old editor detaches — view->removed() + // on this worker would race processor->process() on the + // audio loop (same plugin instance). + instruments.setReloading(ch, true); if (auto* i = instruments.get(ch)) i->closeGUI(); + // Destroy the old editor's child windows (worker-owned, + // created by attachView here) so they stop pumping; + // the parent window is hidden and kept for reuse. + // See post_close_gui for the same pattern. + while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) + DestroyWindow(c); ShowWindow(hToHide, SW_HIDE); } #endif @@ -630,6 +879,25 @@ int main(int argc, char* argv[]) { std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch << " type=" << (int)t << " " << path << std::endl; } + // New-path channels (the freshly created instance's DLL) — + // silence any not already covered by the old path. + std::vector spNew; + if (ok) spNew = same_plugin_channels(ch); + for (uint32_t y : spNew) { + if (std::find(spOld.begin(), spOld.end(), y) == spOld.end()) { + g_engine->setReloading(y, true); + std::cerr << "[dbg] load: silenced same-plugin ch=" << y + << " (new path) during load ch=" << ch << std::endl; + } + } + // Restore. Failed load keeps the OLD instance — unmute it + // back (silenced at job start); a successful assign leaves + // the fresh instance unmuted by construction. + if (!ok) g_engine->setReloading(ch, false); + for (uint32_t y : spOld) unmute_if_not_closing(y, "load"); + for (uint32_t y : spNew) + if (std::find(spOld.begin(), spOld.end(), y) == spOld.end()) + unmute_if_not_closing(y, "load"); }); } else if (c.type == 1) { // PANIC instruments.allNotesOff();