Stage 4b: true-peak limiting via 4x polyphase oversampling
Upgrades the brickwall limiter from sample-peak to true-peak (inter-sample). - src/dsp/oversampler.rs: 4x polyphase windowed-sinc (4 phases x 12 taps, Blackman, each phase normalized to unity DC). Detection-only: max_true_peak() returns the inter-sample max magnitude and discards the upsampled samples; the audio path is untouched. Built in prepare(), no realtime allocation. Cost ~ one base-rate FIR per channel; its small group delay is absorbed by the limiter look-ahead, so no added reported latency. - src/dsp/limiter.rs: detector peak = max(sample_peak, oversampler.max_true_peak()); targets a 0.3 dB margin under the ceiling to cover the 4x detection residual. - 16 unit tests (2 new: detects ~3 dB fs/4 inter-sample overshoot; preserves DC amplitude). README/docs updated: Stage 4 complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-4
@@ -8,11 +8,16 @@
|
||||
//! Detection is stereo-linked (one gain for all channels). This stage limits **sample** peaks at
|
||||
//! the base rate; true-peak (inter-sample) limiting via oversampling is a later addition.
|
||||
|
||||
use super::oversampler::Oversampler;
|
||||
|
||||
const MAX_CHANNELS: usize = 2;
|
||||
/// Fixed look-ahead — also this stage's constant latency contribution.
|
||||
const LOOKAHEAD_MS: f32 = 1.5;
|
||||
/// Near-instant attack; the look-ahead gives it time to act before the peak arrives.
|
||||
const ATTACK_MS: f32 = 0.05;
|
||||
/// The 4× true-peak detector can still under-read by a few tenths of a dB near Nyquist, so we
|
||||
/// target a hair below the ceiling to keep the actual inter-sample peak under it.
|
||||
const TRUE_PEAK_MARGIN_DB: f32 = 0.3;
|
||||
|
||||
fn time_to_coef(time_ms: f32, sample_rate: f32) -> f32 {
|
||||
if time_ms <= 0.0 {
|
||||
@@ -33,6 +38,8 @@ pub struct Limiter {
|
||||
/// Current smoothed gain (<= 1).
|
||||
gain: f32,
|
||||
attack_coef: f32,
|
||||
/// 4× interpolator for true-peak (inter-sample) detection.
|
||||
oversampler: Oversampler,
|
||||
}
|
||||
|
||||
impl Default for Limiter {
|
||||
@@ -45,6 +52,7 @@ impl Default for Limiter {
|
||||
fixed_delay: 0,
|
||||
gain: 1.0,
|
||||
attack_coef: 0.0,
|
||||
oversampler: Oversampler::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +70,7 @@ impl Limiter {
|
||||
let channels = num_channels.clamp(1, MAX_CHANNELS);
|
||||
self.delay = vec![vec![0.0; self.capacity]; channels];
|
||||
self.peaks = vec![0.0; self.capacity];
|
||||
self.oversampler.prepare(channels);
|
||||
self.reset();
|
||||
}
|
||||
|
||||
@@ -70,6 +79,7 @@ impl Limiter {
|
||||
ch.iter_mut().for_each(|s| *s = 0.0);
|
||||
}
|
||||
self.peaks.iter_mut().for_each(|p| *p = 0.0);
|
||||
self.oversampler.reset();
|
||||
self.write_pos = 0;
|
||||
self.gain = 1.0;
|
||||
}
|
||||
@@ -86,11 +96,15 @@ impl Limiter {
|
||||
pub fn process(&mut self, input: &[f32], output: &mut [f32], ceiling: f32, release_coef: f32) {
|
||||
let n = input.len().min(self.delay.len());
|
||||
|
||||
// Linked peak of the current input.
|
||||
let mut peak = 0.0f32;
|
||||
// Detector = max of the sample peak and the 4× true-peak (inter-sample) estimate.
|
||||
let mut sample_peak = 0.0f32;
|
||||
for &x in &input[..n] {
|
||||
peak = peak.max(x.abs());
|
||||
sample_peak = sample_peak.max(x.abs());
|
||||
}
|
||||
let peak = sample_peak.max(self.oversampler.max_true_peak(&input[..n]));
|
||||
|
||||
// Target a hair below the ceiling so the (slightly under-read) true peak stays under it.
|
||||
let target_ceiling = ceiling * 10.0f32.powf(-TRUE_PEAK_MARGIN_DB / 20.0);
|
||||
|
||||
// Write into the ring.
|
||||
for ch in 0..n {
|
||||
@@ -105,7 +119,11 @@ impl Limiter {
|
||||
for &p in &self.peaks {
|
||||
window_max = window_max.max(p);
|
||||
}
|
||||
let target = if window_max > ceiling { ceiling / window_max } else { 1.0 };
|
||||
let target = if window_max > target_ceiling {
|
||||
target_ceiling / window_max
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
// Decoupled smoothing: fast attack down, slow release up.
|
||||
self.gain = if target < self.gain {
|
||||
|
||||
Reference in New Issue
Block a user