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
+10 -4
View File
@@ -97,15 +97,21 @@ impl Oversampler {
self.history[ch][slot] = input[ch];
}
// The tap index only depends on `pos` and `k`, not on `phase` or `ch` — hoist it out of
// those loops instead of recomputing (and re-dividing) it PHASES*MAX_CHANNELS times over.
let mut idx = [0usize; TAPS_PER_PHASE];
for (k, slot) in idx.iter_mut().enumerate() {
// k = 0 is the newest sample, increasing k goes back in time.
*slot = (self.pos + TAPS_PER_PHASE - k) % TAPS_PER_PHASE;
}
let mut peak = 0.0f32;
for ch in 0..n {
let hist = &self.history[ch];
for phase in &self.coeffs {
let mut acc = 0.0f32;
for (k, &c) in phase.iter().enumerate() {
// k = 0 is the newest sample, increasing k goes back in time.
let idx = (self.pos + TAPS_PER_PHASE - k) % TAPS_PER_PHASE;
acc += c * hist[idx];
for (&c, &i) in phase.iter().zip(idx.iter()) {
acc += c * hist[i];
}
peak = peak.max(acc.abs());
}