Denormals: rely on NIH-plug's FTZ, drop redundant in-code flush
Investigated the planned global FTZ for the IIR filters and found NIH-plug already handles it: process_wrapper wraps process()/reset() in a ScopedFtz guard that enables CPU Flush-To-Zero (x86 MXCSR bit 15 / AArch64 FPCR bit 24, via inline asm, restored on drop) on the vst3, clap, and standalone paths. SSE is baseline on x86_64 so FTZ is always active for our build. So adding our own guard would just duplicate the framework. Instead, removed the now-redundant flush_denormal() from compressor.rs (the biquads and envelope/RMS tails already relied on this FTZ) for a single consistent story, and rewrote the README 'Denormal flushing' note to document that the framework handles it (FTZ, not DAZ — sufficient for our feed-forward IIR). No functional change; 10 unit tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -221,11 +221,13 @@ must be pre-allocated in `initialize()`. Use `assert_process_allocs` feature fla
|
|||||||
development to catch violations.
|
development to catch violations.
|
||||||
|
|
||||||
### Denormal flushing
|
### Denormal flushing
|
||||||
The compressor flushes its envelope/RMS state to zero in code once it decays below audibility
|
Handled by the framework — no plugin code needed. NIH-plug wraps `process()` and `reset()` in
|
||||||
(`flush_denormal` in `compressor.rs`). The hardware `_MM_SET_FLUSH_ZERO_MODE` intrinsic is now
|
`process_wrapper`, which enables the CPU's **Flush-To-Zero** mode for the duration via its
|
||||||
deprecated and the matching DAZ helper isn't exposed by `std::arch`, so a global hardware FTZ/DAZ
|
`ScopedFtz` guard (x86 `MXCSR` bit 15 / AArch64 `FPCR` bit 24, set with inline asm and restored
|
||||||
(via inline asm on the audio thread) is deferred until the IIR crossover/limiter filters land,
|
on drop). FTZ has a fixed threshold at the normal/subnormal boundary (~−759 dB for f32), so the
|
||||||
where it matters more.
|
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
|
### Parameter smoothing
|
||||||
NIH-plug provides `Smoother` — use it for all gain/threshold params to avoid zipper noise.
|
NIH-plug provides `Smoother` — use it for all gain/threshold params to avoid zipper noise.
|
||||||
|
|||||||
+6
-17
@@ -30,16 +30,9 @@ const LEVEL_EPS: f32 = 1e-12;
|
|||||||
/// promoted to a parameter later.
|
/// promoted to a parameter later.
|
||||||
const RMS_WINDOW_MS: f32 = 5.0;
|
const RMS_WINDOW_MS: f32 = 5.0;
|
||||||
|
|
||||||
/// Flush a decaying envelope value to zero once it is far below audibility, so the
|
// Denormals (the exponentially-decaying envelope/RMS tails and the IIR filter state) are handled
|
||||||
/// exponential tail can't drift into denormal range (which causes CPU spikes).
|
// by the CPU's Flush-To-Zero mode, which NIH-plug enables around `process()`/`reset()` via its
|
||||||
#[inline]
|
// `ScopedFtz` guard (x86 MXCSR / AArch64 FPCR). So no per-value flushing is needed here.
|
||||||
fn flush_denormal(x: f32) -> f32 {
|
|
||||||
if x.abs() < 1e-30 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-block compressor settings. Cheap to copy; rebuilt each process block from params.
|
/// Per-block compressor settings. Cheap to copy; rebuilt each process block from params.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -195,9 +188,7 @@ impl Compressor {
|
|||||||
} else {
|
} else {
|
||||||
// RMS = running mean of the linked squared level over a fixed window. Updated
|
// RMS = running mean of the linked squared level over a fixed window. Updated
|
||||||
// whenever active (regardless of mode) so switching peak<->RMS is seamless.
|
// whenever active (regardless of mode) so switching peak<->RMS is seamless.
|
||||||
self.mean_sq = flush_denormal(
|
self.mean_sq = self.rms_coef * self.mean_sq + (1.0 - self.rms_coef) * peak * peak;
|
||||||
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 detector = if set.use_rms { self.mean_sq.sqrt() } else { peak };
|
||||||
let level_db = 20.0 * (detector + LEVEL_EPS).log10();
|
let level_db = 20.0 * (detector + LEVEL_EPS).log10();
|
||||||
// Desired attenuation in dB, as a positive quantity.
|
// 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:
|
// Smooth, decoupled peak detector (Giannoulis eq. 17–18) on the attenuation:
|
||||||
// y1 = max(target, release-smoothed y1) (fast up / slow down "peak hold")
|
// y1 = max(target, release-smoothed y1) (fast up / slow down "peak hold")
|
||||||
// yl = attack-smoothed y1
|
// yl = attack-smoothed y1
|
||||||
self.y1 = flush_denormal(
|
self.y1 = target.max(set.release_coef * self.y1 + (1.0 - set.release_coef) * target);
|
||||||
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;
|
||||||
);
|
|
||||||
self.yl = flush_denormal(set.attack_coef * self.yl + (1.0 - set.attack_coef) * self.y1);
|
|
||||||
|
|
||||||
let total_db = set.makeup_db - self.yl;
|
let total_db = set.makeup_db - self.yl;
|
||||||
10.0f32.powf(total_db / 20.0)
|
10.0f32.powf(total_db / 20.0)
|
||||||
|
|||||||
Reference in New Issue
Block a user