diff --git a/README.md b/README.md index 03b517d..79c8f4b 100644 --- a/README.md +++ b/README.md @@ -221,11 +221,13 @@ must be pre-allocated in `initialize()`. Use `assert_process_allocs` feature fla development to catch violations. ### Denormal flushing -The compressor flushes its envelope/RMS state to zero in code once it decays below audibility -(`flush_denormal` in `compressor.rs`). The hardware `_MM_SET_FLUSH_ZERO_MODE` intrinsic is now -deprecated and the matching DAZ helper isn't exposed by `std::arch`, so a global hardware FTZ/DAZ -(via inline asm on the audio thread) is deferred until the IIR crossover/limiter filters land, -where it matters more. +Handled by the framework — no plugin code needed. NIH-plug wraps `process()` and `reset()` in +`process_wrapper`, which enables the CPU's **Flush-To-Zero** mode for the duration via its +`ScopedFtz` guard (x86 `MXCSR` bit 15 / AArch64 `FPCR` bit 24, set with inline asm and restored +on drop). FTZ has a fixed threshold at the normal/subnormal boundary (~−759 dB for f32), so the +decaying envelope/RMS tails and all the IIR filter state are flushed to zero automatically, +far below audibility. We therefore do **not** set the register ourselves or flush values in code. +(Note: NIH-plug sets FTZ but not DAZ; for our feed-forward IIR work FTZ on results is sufficient.) ### Parameter smoothing NIH-plug provides `Smoother` — use it for all gain/threshold params to avoid zipper noise. diff --git a/src/dsp/compressor.rs b/src/dsp/compressor.rs index 515c13d..2565cbb 100644 --- a/src/dsp/compressor.rs +++ b/src/dsp/compressor.rs @@ -30,16 +30,9 @@ const LEVEL_EPS: f32 = 1e-12; /// promoted to a parameter later. const RMS_WINDOW_MS: f32 = 5.0; -/// Flush a decaying envelope value to zero once it is far below audibility, so the -/// exponential tail can't drift into denormal range (which causes CPU spikes). -#[inline] -fn flush_denormal(x: f32) -> f32 { - if x.abs() < 1e-30 { - 0.0 - } else { - x - } -} +// Denormals (the exponentially-decaying envelope/RMS tails and the IIR filter state) are handled +// by the CPU's Flush-To-Zero mode, which NIH-plug enables around `process()`/`reset()` via its +// `ScopedFtz` guard (x86 MXCSR / AArch64 FPCR). So no per-value flushing is needed here. /// Per-block compressor settings. Cheap to copy; rebuilt each process block from params. #[derive(Clone, Copy)] @@ -195,9 +188,7 @@ impl Compressor { } else { // RMS = running mean of the linked squared level over a fixed window. Updated // whenever active (regardless of mode) so switching peak<->RMS is seamless. - self.mean_sq = flush_denormal( - self.rms_coef * self.mean_sq + (1.0 - self.rms_coef) * peak * peak, - ); + self.mean_sq = self.rms_coef * self.mean_sq + (1.0 - self.rms_coef) * peak * peak; let detector = if set.use_rms { self.mean_sq.sqrt() } else { peak }; let level_db = 20.0 * (detector + LEVEL_EPS).log10(); // Desired attenuation in dB, as a positive quantity. @@ -206,10 +197,8 @@ impl Compressor { // Smooth, decoupled peak detector (Giannoulis eq. 17–18) on the attenuation: // y1 = max(target, release-smoothed y1) (fast up / slow down "peak hold") // yl = attack-smoothed y1 - self.y1 = flush_denormal( - target.max(set.release_coef * self.y1 + (1.0 - set.release_coef) * target), - ); - self.yl = flush_denormal(set.attack_coef * self.yl + (1.0 - set.attack_coef) * self.y1); + self.y1 = target.max(set.release_coef * self.y1 + (1.0 - set.release_coef) * target); + self.yl = set.attack_coef * self.yl + (1.0 - set.attack_coef) * self.y1; let total_db = set.makeup_db - self.yl; 10.0f32.powf(total_db / 20.0)