From 37edb9fb417b249cacd0eabe49bac30cb46dd382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Golarits=20M=C3=A1rton?= Date: Sat, 16 May 2026 18:14:19 +0200 Subject: [PATCH] fix: avoid audio-thread stalls on model eviction and AU bypass toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real-time safety fixes that eliminate audible dropouts when disabling the plugin in Ableton Live (and likely other AU hosts): 1. Deferred model deletion (ProcessBlock / OnIdle) _ApplyDSPStaging() is called from ProcessBlock (audio thread). When a model is removed or replaced, the outgoing unique_ptr destructor was freeing several MB of Eigen weight matrices inline on the audio thread. On macOS, free() is non-deterministic on large blocks and can stall the audio callback long enough to miss the buffer deadline. Fix: evict the old model into mModelPendingDeletion (atomic raw ptr) instead of destroying it in place. OnIdle() (UI thread) performs the actual delete on the next tick (~20 ms later). 2. Skip _ResetModelAndIR() on AU bypass toggles (OnReset) iPlug2 calls OnReset() in response to kAudioUnitProperty_BypassEffect (the AU power-button toggle). This triggered _ResetModelAndIR() which calls DSP::Reset() -> SetMaxBufferSize() + prewarm(). prewarm() runs the model forward for thousands of samples to settle LSTM state — all on the audio thread — causing a guaranteed dropout on every toggle. Fix: cache the last sample rate and block size seen by OnReset(). Skip _ResetModelAndIR() when neither has changed, which is always the case for a bypass toggle. Legitimate format changes (sample rate, buffer size) still trigger a full reset. Note: the iPlug2 AUv2 handler for kAudioUnitProperty_BypassEffect already has a "TODO: should the following be called here?" comment on the OnReset() call, suggesting this was a known uncertainty upstream. --- NeuralAmpModeler/NeuralAmpModeler.cpp | 19 +++++++++++++++---- NeuralAmpModeler/NeuralAmpModeler.h | 8 ++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/NeuralAmpModeler/NeuralAmpModeler.cpp b/NeuralAmpModeler/NeuralAmpModeler.cpp index 4a08266ac..9d0992b46 100644 --- a/NeuralAmpModeler/NeuralAmpModeler.cpp +++ b/NeuralAmpModeler/NeuralAmpModeler.cpp @@ -409,14 +409,24 @@ void NeuralAmpModeler::OnReset() SetTailSize(tailCycles * (int)(sampleRate / kDCBlockerFrequency)); mInputSender.Reset(sampleRate); mOutputSender.Reset(sampleRate); - // If there is a model or IR loaded, they need to be checked for resampling. - _ResetModelAndIR(sampleRate, GetBlockSize()); + // Only reset the model when the format actually changed. iPlug2 also calls OnReset() in + // response to kAudioUnitProperty_BypassEffect (the AU bypass / power-button toggle), which + // would trigger prewarm() on the audio thread and cause an audible dropout. Sample rate and + // block size are unchanged on a bypass toggle, so skipping _ResetModelAndIR() is safe there. + if (sampleRate != mLastResetSampleRate || maxBlockSize != mLastResetBlockSize) + { + _ResetModelAndIR(sampleRate, maxBlockSize); + mLastResetSampleRate = sampleRate; + mLastResetBlockSize = maxBlockSize; + } mToneStack->Reset(sampleRate, maxBlockSize); _UpdateLatency(); } void NeuralAmpModeler::OnIdle() { + delete mModelPendingDeletion.exchange(nullptr, std::memory_order_acquire); + mInputSender.TransmitData(*this); mOutputSender.TransmitData(*this); @@ -597,7 +607,7 @@ void NeuralAmpModeler::_ApplyDSPStaging() // Remove marked modules if (mShouldRemoveModel) { - mModel = nullptr; + mModelPendingDeletion.store(mModel.release(), std::memory_order_release); mNAMPath.Set(""); mShouldRemoveModel = false; mModelCleared = true; @@ -614,8 +624,9 @@ void NeuralAmpModeler::_ApplyDSPStaging() // Move things from staged to live if (mStagedModel != nullptr) { + ResamplingNAM* old = mModel.release(); mModel = std::move(mStagedModel); - mStagedModel = nullptr; + mModelPendingDeletion.store(old, std::memory_order_release); mNewModelLoadedInDSP = true; _UpdateLatency(); _SetInputGain(); diff --git a/NeuralAmpModeler/NeuralAmpModeler.h b/NeuralAmpModeler/NeuralAmpModeler.h index f5dae839d..677433d15 100644 --- a/NeuralAmpModeler/NeuralAmpModeler.h +++ b/NeuralAmpModeler/NeuralAmpModeler.h @@ -306,6 +306,14 @@ class NeuralAmpModeler final : public iplug::Plugin std::atomic mNewModelLoadedInDSP = false; std::atomic mModelCleared = false; + // Holds a model evicted from mModel that must be deleted on the UI thread (OnIdle) rather + // than the audio thread, to avoid freeing large Eigen weight matrices inside ProcessBlock + // and causing a buffer-deadline miss. + std::atomic mModelPendingDeletion{nullptr}; + + // Last format seen by OnReset(). Used to guard _ResetModelAndIR() — see OnReset() for rationale. + double mLastResetSampleRate{0.0}; + int mLastResetBlockSize{0}; // Tone stack modules std::unique_ptr mToneStack;