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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
This commit is contained in:
Mikkeli Matlock
2026-08-31 21:55:40 +09:00
parent b862f5a971
commit 5958b077e2
+53 -8
View File
@@ -216,6 +216,43 @@ impl Plugin for Codename206 {
let mut gr = [0.0f32; meters::NUM_CHANNELS]; let mut gr = [0.0f32; meters::NUM_CHANNELS];
let mut lim_gr = 0.0f32; 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: &params::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 in_frame = [0.0f32; 2];
let mut band_in = [[0.0f32; 2]; 3]; let mut band_in = [[0.0f32; 2]; 3];
let mut band_out = [[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 { for b in 0..3 {
// `_fast` skips the below-(-100dB) branch, which pre_gain_db's -24..+36 range // `_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(). // 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] {
for ch in 0..n { band_pre[b]
band_in[b][ch] *= pre; } else {
}
band_set[b].makeup_db = band_params[b].makeup_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].mix = band_params[b].mix.smoothed.next();
band_set[b].low_slope = band_params[b].low_slope.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(); 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;
}
self.comps[b].process(&band_in[b][..n], &mut band_out[b][..n], &band_set[b]); self.comps[b].process(&band_in[b][..n], &mut band_out[b][..n], &band_set[b]);
for ch in 0..n { for ch in 0..n {
summed[ch] += band_out[b][ch]; 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). // '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 {
for ch in 0..n { all_pre_static
summed[ch] *= all_pre; } else {
}
all_set.makeup_db = self.params.all.makeup_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.mix = self.params.all.mix.smoothed.next();
all_set.low_slope = self.params.all.low_slope.smoothed.next(); all_set.low_slope = self.params.all.low_slope.smoothed.next();
all_set.low_curve = self.params.all.low_curve.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;
}
self.comps[ALL].process(&summed[..n], &mut out_frame[..n], &all_set); self.comps[ALL].process(&summed[..n], &mut out_frame[..n], &all_set);
// Output brickwall limiter. // Output brickwall limiter.