From 5958b077e2d4e0935a61cd63bcb9872cbf5ce8ab Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Mon, 31 Aug 2026 21:55:40 +0900 Subject: [PATCH] perf: lift idle per-sample smoothers out of the process loop Every sample ran 20 Smoother::next() calls and 4 db_to_gain_fast() - one set per channel for pre-gain, makeup, mix, low slope and low curve - whether or not anything was actually moving. Measured standalone at ~17 ns/sample, so roughly 8% of the chain, paid permanently for the rare case where a knob is in motion. Params here only change on block boundaries (we do not split blocks on events), so a smoother idle at the top of a block stays idle for that block. When every smoother feeding a channel is idle - i.e. nobody is touching anything, which is the overwhelming common case - its per-sample values are constants and lift out of the loop. The moving case keeps the original per-sample path unchanged. Unlike the true-peak early-out this is an always-on saving, not a signal-dependent one. Note this path has no automated coverage: process() needs a host to drive it, so the DSP tests do not reach it. The logic is small and the moving case is byte-for-byte the old code, but automation smoothing is worth an ear in FL before trusting it - move a pre-gain or mix knob and listen for zipper. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1 --- src/lib.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b318180..a774f79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -216,6 +216,43 @@ impl Plugin for Codename206 { let mut gr = [0.0f32; meters::NUM_CHANNELS]; let mut lim_gr = 0.0f32; + // Params only change on block boundaries here (we do not split blocks on events), so a + // smoother that is idle now stays idle for this whole block. When every smoother feeding + // a channel is idle - the overwhelming common case, i.e. nobody is touching a knob - + // its per-sample values are constants, so lift them out of the sample loop entirely. + // That removes 5 Smoother::next() calls and a db_to_gain per channel per sample. + let chan_static = |p: ¶ms::CompressorParams| { + !p.pre_gain_db.smoothed.is_smoothing() + && !p.makeup_db.smoothed.is_smoothing() + && !p.mix.smoothed.is_smoothing() + && !p.low_slope.smoothed.is_smoothing() + && !p.low_curve.smoothed.is_smoothing() + }; + let band_static = [ + chan_static(&self.params.low), + chan_static(&self.params.mid), + chan_static(&self.params.high), + ]; + let all_static = chan_static(&self.params.all); + let mut band_pre = [1.0f32; 3]; + for b in 0..3 { + if band_static[b] { + band_pre[b] = util::db_to_gain_fast(band_params[b].pre_gain_db.smoothed.next()); + band_set[b].makeup_db = band_params[b].makeup_db.smoothed.next(); + band_set[b].mix = band_params[b].mix.smoothed.next(); + band_set[b].low_slope = band_params[b].low_slope.smoothed.next(); + band_set[b].low_curve = band_params[b].low_curve.smoothed.next(); + } + } + let mut all_pre_static = 1.0f32; + if all_static { + all_pre_static = util::db_to_gain_fast(self.params.all.pre_gain_db.smoothed.next()); + all_set.makeup_db = self.params.all.makeup_db.smoothed.next(); + all_set.mix = self.params.all.mix.smoothed.next(); + all_set.low_slope = self.params.all.low_slope.smoothed.next(); + all_set.low_curve = self.params.all.low_curve.smoothed.next(); + } + let mut in_frame = [0.0f32; 2]; let mut band_in = [[0.0f32; 2]; 3]; let mut band_out = [[0.0f32; 2]; 3]; @@ -243,14 +280,18 @@ impl Plugin for Codename206 { for b in 0..3 { // `_fast` skips the below-(-100dB) branch, which pre_gain_db's -24..+36 range // never reaches, in exchange for a cheaper exp() instead of a general powf(). - let pre = util::db_to_gain_fast(band_params[b].pre_gain_db.smoothed.next()); + let pre = if band_static[b] { + band_pre[b] + } else { + band_set[b].makeup_db = band_params[b].makeup_db.smoothed.next(); + band_set[b].mix = band_params[b].mix.smoothed.next(); + band_set[b].low_slope = band_params[b].low_slope.smoothed.next(); + band_set[b].low_curve = band_params[b].low_curve.smoothed.next(); + util::db_to_gain_fast(band_params[b].pre_gain_db.smoothed.next()) + }; for ch in 0..n { band_in[b][ch] *= pre; } - band_set[b].makeup_db = band_params[b].makeup_db.smoothed.next(); - band_set[b].mix = band_params[b].mix.smoothed.next(); - band_set[b].low_slope = band_params[b].low_slope.smoothed.next(); - band_set[b].low_curve = band_params[b].low_curve.smoothed.next(); self.comps[b].process(&band_in[b][..n], &mut band_out[b][..n], &band_set[b]); for ch in 0..n { summed[ch] += band_out[b][ch]; @@ -274,14 +315,18 @@ impl Plugin for Codename206 { } // 'All' aggregate channel over the summed bands (driven before its compressor). - let all_pre = util::db_to_gain_fast(self.params.all.pre_gain_db.smoothed.next()); + let all_pre = if all_static { + all_pre_static + } else { + all_set.makeup_db = self.params.all.makeup_db.smoothed.next(); + all_set.mix = self.params.all.mix.smoothed.next(); + all_set.low_slope = self.params.all.low_slope.smoothed.next(); + all_set.low_curve = self.params.all.low_curve.smoothed.next(); + util::db_to_gain_fast(self.params.all.pre_gain_db.smoothed.next()) + }; for ch in 0..n { summed[ch] *= all_pre; } - all_set.makeup_db = self.params.all.makeup_db.smoothed.next(); - all_set.mix = self.params.all.mix.smoothed.next(); - all_set.low_slope = self.params.all.low_slope.smoothed.next(); - all_set.low_curve = self.params.all.low_curve.smoothed.next(); self.comps[ALL].process(&summed[..n], &mut out_frame[..n], &all_set); // Output brickwall limiter.