Skip to content

fix: avoid audio-thread stalls on model eviction and AU bypass toggle#627

Open
Ahorix wants to merge 1 commit into
sdatkinson:mainfrom
Ahorix:fix/rt-safe-bypass-and-model-eviction
Open

fix: avoid audio-thread stalls on model eviction and AU bypass toggle#627
Ahorix wants to merge 1 commit into
sdatkinson:mainfrom
Ahorix:fix/rt-safe-bypass-and-model-eviction

Conversation

@Ahorix
Copy link
Copy Markdown

@Ahorix Ahorix commented May 16, 2026

Problem

Disabling the NAM plugin in Ableton Live (and likely other AU hosts) via the device power button causes an audible dropout/crackle, even on completely silent tracks. This was reproducible and isolated to NAM — simpler plugins toggle cleanly.

Two separate real-time safety violations were found and fixed.


Fix 1: Deferred model deletion

_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 (an std::atomic<ResamplingNAM*>) instead of destroying it in place. OnIdle() (UI thread) performs the actual delete on the next tick (~20 ms later).

Fix 2: Skip _ResetModelAndIR() on AU bypass toggles

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. The answer for NAM is: no, it should not.


Testing

  • Toggle power button off/on repeatedly → no dropout
  • Load a model, swap to a different model → no dropout, new model sounds correct
  • Change DAW sample rate with model loaded → model resets correctly
  • Change buffer size → model resets correctly
  • Multiple instances → independent, no interference

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants