perf: remove per-sample waste from the compressor/limiter hot path

Found by reading the audio-thread code, not by profiling - no profiler is
set up in this project yet. All four are algorithmic or arithmetic wins,
none change the DSP result; the existing tests still pass.

- oversampler: max_true_peak recomputed its circular-history tap index
  (including a % TAPS_PER_PHASE) inside the phase x channel loop, though
  the index depends only on pos/k - 8x redundant. Hoisted to a per-sample
  precomputed index array. This runs unconditionally every sample (it
  feeds the limiter's true-peak detector, not gated by metering), so it
  was the highest-value fix.

- limiter: the sliding-window max rescanned the entire look-ahead ring
  (~73 samples at 48kHz/1.5ms) every sample - O(window) per sample.
  Replaced with an O(1)-amortized monotonic-deque sliding max, backed by
  preallocated ring arrays and RT-safe. Covered by a new test against a
  naive brute-force reference. Also precomputed the constant true-peak
  margin gain in prepare() instead of a powf every sample.

- compressor: replaced per-sample log10/powf(10, x/20) with the
  equivalent ln/exp forms, mirroring nih-plug's own gain_to_db_fast /
  db_to_gain_fast. Called 4x/sample (3 bands + 'All').

- both: replaced % capacity ring-index math (a real integer division,
  since capacity is not a power of two) with a conditional-subtract
  wrap_add helper. Left the oversampler's % TAPS_PER_PHASE alone - that
  divisor is a compile-time constant LLVM already turns into a multiply.

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:19:23 +09:00
parent eda3d09399
commit 03d60a157b
4 changed files with 165 additions and 25 deletions
+33 -6
View File
@@ -24,9 +24,20 @@ pub const MAX_LOOKAHEAD_MS: f32 = 5.0;
/// Most channels we ever process in one frame (our audio layouts are mono/stereo).
const MAX_CHANNELS: usize = 2;
/// ~ -240 dBFS; keeps `log10` away from zero without affecting audible levels.
/// ~ -240 dBFS; keeps the level conversion away from zero without affecting audible levels.
const LEVEL_EPS: f32 = 1e-12;
/// `ln(x) * LOG10_GAIN_TO_DB == log10(x) * 20`, but a bare `ln()` runs faster than `log10()` on
/// most architectures (mirrors nih-plug's `util::gain_to_db_fast`). Used in the per-sample level
/// detector, called 4x/sample (once per band + 'All').
const LOG10_GAIN_TO_DB: f32 = std::f32::consts::LOG10_E * 20.0;
/// `exp(x * DB_TO_GAIN_EXP) == 10.0f32.powf(x / 20.0)`, but `exp()` runs faster than a general
/// `powf()` on most architectures (mirrors nih-plug's `util::db_to_gain_fast`) — `powf` has to
/// handle an arbitrary runtime base, while this bakes `ln(10)` in as a constant. Used for the
/// per-sample wet gain, called 4x/sample.
const DB_TO_GAIN_EXP: f32 = std::f32::consts::LN_10 / 20.0;
/// Silence-floor anchor for the below-threshold shaping: at/below this level the gain change is 0
/// (silence stays silence), and the low region fans up/down from here toward the threshold. Matches
/// the editor gain-curve's display floor.
@@ -230,8 +241,12 @@ impl Compressor {
// 2) Tap positions (circular). Output is always `fixed_delay` old; the detector reads
// `l` samples newer than the output, i.e. `l` samples into the output's future.
let out_pos = (self.write_pos + self.capacity - self.fixed_delay) % self.capacity;
let det_pos = (self.write_pos + self.capacity - (self.fixed_delay - l)) % self.capacity;
// `wrap_add` instead of `%`: capacity is rarely a power of two, so a plain modulo forces
// an integer division here; a conditional subtract is cheaper and behaves identically
// given the add is always <= capacity (true for every offset computed in this function).
let out_pos = wrap_add(self.write_pos, self.capacity - self.fixed_delay, self.capacity);
let det_pos =
wrap_add(self.write_pos, self.capacity - (self.fixed_delay - l), self.capacity);
// 3) Linked peak detector at the look-ahead tap.
let mut peak = 0.0f32;
@@ -245,7 +260,7 @@ impl Compressor {
// active (regardless of mode) so switching peak<->RMS is seamless.
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();
let level_db = (detector + LEVEL_EPS).ln() * LOG10_GAIN_TO_DB;
// Desired attenuation in dB, as a positive quantity.
let target = -Self::gain_computer(
level_db,
@@ -262,7 +277,7 @@ impl Compressor {
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 wet_gain = 10.0f32.powf((set.makeup_db - self.yl) / 20.0);
let wet_gain = ((set.makeup_db - self.yl) * DB_TO_GAIN_EXP).exp();
// 5) Dry/wet mix (parallel compression). Both paths use the same delayed input, so the
// blend is phase-aligned. mix = 0 -> dry passthrough (clean bypass), mix = 1 -> wet.
@@ -273,7 +288,19 @@ impl Compressor {
}
// 6) Advance the write head.
self.write_pos = (self.write_pos + 1) % self.capacity;
self.write_pos = wrap_add(self.write_pos, 1, self.capacity);
}
}
/// Wrap `pos + add` into `[0, capacity)`, assuming `add <= capacity` — a conditional subtract in
/// place of `%`, since `capacity` is rarely a power of two (so `%` costs a real integer division).
#[inline]
fn wrap_add(pos: usize, add: usize, capacity: usize) -> usize {
let sum = pos + add;
if sum >= capacity {
sum - capacity
} else {
sum
}
}