Skip to main content

audiobox/mixer/
limiter.rs

1//! A look-ahead peak limiter on the master bus, after the offline `Audio::limit`.
2use crate::math::exp2;
3use crate::{Error, Result};
4
5/// Settings of the master bus's limiter, enabled with [`MixerConfig::limiter`].
6///
7/// The output is delayed by `lookahead_ms`, and the gain at each frame comes from the
8/// loudest frame within that window, so the gain is already down when a peak leaves
9/// the delay: the output never exceeds `ceiling_db`. The gain drops at once and
10/// recovers exponentially with a time constant of `release_ms`. A look-ahead of 0
11/// adds no latency and still keeps every sample at or below the ceiling, at the cost
12/// of a sharper attack. Below the ceiling, the signal passes unchanged.
13///
14/// [`MixerConfig::limiter`]: super::MixerConfig::limiter
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct MasterLimiter {
17    /// The highest output level, in dBFS, -24..=0. Default -1.
18    pub ceiling_db: f32,
19    /// Delay that lets the gain anticipate peaks, 0..=10 ms. Default 0.
20    pub lookahead_ms: f32,
21    /// Time constant of the gain's recovery, 1..=2000 ms. Default 50, as `Audio::limit`.
22    pub release_ms: f32,
23}
24impl Default for MasterLimiter {
25    fn default() -> Self {
26        Self {
27            ceiling_db: -1.0,
28            lookahead_ms: 0.0,
29            release_ms: 50.0,
30        }
31    }
32}
33
34pub(super) struct Limiter {
35    ceiling: f64,
36    /// Per-frame factor of the release's exponential recovery.
37    release: f64,
38    channels: usize,
39    /// Frames of look-ahead plus the current one.
40    window: usize,
41    /// The last `window` input frames, in a ring.
42    delay: Vec<f32>,
43    at: usize,
44    gain: f64,
45    /// A monotonic queue of (frame, peak) over the window, as `Audio::limit` keeps:
46    /// peaks decrease from front to back, so the front is the window's maximum.
47    queue: Vec<(u64, f64)>,
48    queue_head: usize,
49    queue_len: usize,
50    frames: u64,
51}
52
53impl Limiter {
54    pub fn new(settings: MasterLimiter, rate: u32, channels: usize) -> Result<Self> {
55        let MasterLimiter {
56            ceiling_db,
57            lookahead_ms,
58            release_ms,
59        } = settings;
60        if !(-24.0..=0.0).contains(&ceiling_db)
61            || !(0.0..=10.0).contains(&lookahead_ms)
62            || !(1.0..=2000.0).contains(&release_ms)
63        {
64            return Err(Error::invalid(
65                "The limiter needs a ceiling of -24..=0 dBFS, a look-ahead of 0..=10 ms and a release of 1..=2000 ms",
66            ));
67        }
68        let lookahead = (lookahead_ms as f64 * rate as f64 / 1000.0).round() as usize;
69        let release_frames = release_ms as f64 * rate as f64 / 1000.0;
70        Ok(Self {
71            ceiling: exp2(ceiling_db as f64 * std::f64::consts::LOG2_10 / 20.0),
72            // exp(-1 / frames), with the crate's own exp2.
73            release: exp2(-std::f64::consts::LOG2_E / release_frames),
74            channels,
75            window: lookahead + 1,
76            delay: vec![0.0; (lookahead + 1) * channels],
77            at: 0,
78            gain: 1.0,
79            queue: vec![(0, 0.0); lookahead + 1],
80            queue_head: 0,
81            queue_len: 0,
82            frames: 0,
83        })
84    }
85    /// Frames by which the limiter delays the output.
86    pub fn latency(&self) -> usize {
87        self.window - 1
88    }
89    /// Adds the current frame's peak and returns the loudest peak in the window, in
90    /// constant amortized time.
91    fn push_peak(&mut self, peak: f64) -> f64 {
92        let w = self.window;
93        // Expire peaks that leave the window first, so at most `w - 1` remain and the
94        // new one always fits.
95        while self.queue_len > 0 && self.queue[self.queue_head].0 + w as u64 <= self.frames {
96            self.queue_head = (self.queue_head + 1) % w;
97            self.queue_len -= 1;
98        }
99        while self.queue_len > 0 && self.queue[(self.queue_head + self.queue_len - 1) % w].1 <= peak
100        {
101            self.queue_len -= 1;
102        }
103        self.queue[(self.queue_head + self.queue_len) % w] = (self.frames, peak);
104        self.queue_len += 1;
105        self.frames += 1;
106        self.queue[self.queue_head].1
107    }
108    /// Limits interleaved `out` in place and returns the lowest gain applied.
109    pub fn process(&mut self, out: &mut [f32]) -> f64 {
110        let ch = self.channels;
111        let mut lowest = 1.0f64;
112        let ceiling = self.ceiling as f32;
113        for frame in out.chunks_exact_mut(ch) {
114            // A sum can overflow f32 with extreme gains: hold an infinite sample at full
115            // scale and silence a NaN, so the output stays bounded whatever comes in.
116            for s in frame.iter_mut() {
117                if s.is_nan() {
118                    *s = 0.0;
119                } else if s.is_infinite() {
120                    *s = f32::MAX.copysign(*s);
121                }
122            }
123            let peak = frame.iter().map(|s| s.abs() as f64).fold(0.0, f64::max);
124            let slot = self.at;
125            let loudest = self.push_peak(peak);
126            // The frame that leaves the delay is the oldest in the window: it sits
127            // where the new frame goes. With no look-ahead it is the new frame itself.
128            let next = (slot + 1) % self.window;
129            let mut delayed = [0.0f32; 8];
130            if self.window == 1 {
131                delayed[..ch].copy_from_slice(frame);
132            } else {
133                delayed[..ch].copy_from_slice(&self.delay[next * ch..next * ch + ch]);
134                self.delay[slot * ch..slot * ch + ch].copy_from_slice(frame);
135            }
136            let target = if loudest > self.ceiling {
137                self.ceiling / loudest
138            } else {
139                1.0
140            };
141            self.gain = if target < self.gain {
142                target
143            } else {
144                target + (self.gain - target) * self.release
145            };
146            lowest = lowest.min(self.gain);
147            for (o, &d) in frame.iter_mut().zip(&delayed[..ch]) {
148                // Rounding can leave a sample a hair above the ceiling; hold it there.
149                *o = ((d as f64 * self.gain) as f32).clamp(-ceiling, ceiling);
150            }
151            self.at = next;
152        }
153        lowest
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn the_queue_tracks_the_window_maximum() {
163        let mut limiter = Limiter::new(
164            MasterLimiter {
165                lookahead_ms: 1.0,
166                ..MasterLimiter::default()
167            },
168            48_000,
169            1,
170        )
171        .unwrap();
172        let window = limiter.window;
173        let mut seed = 0x9e37_79b9u32;
174        let mut peaks: Vec<f64> = (0..5000)
175            .map(|_| {
176                seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
177                (seed >> 8) as f64 / (1u32 << 24) as f64 * 2.0
178            })
179            .collect();
180        // Long decreasing ramps and plateaus fill the queue; random peaks rarely do.
181        peaks.extend((0..3 * window).map(|i| 2.0 - i as f64 / (3 * window) as f64));
182        peaks.extend(std::iter::repeat_n(0.5, 2 * window));
183        peaks.extend((0..window).map(|i| 0.5 + i as f64 * 0.01));
184        peaks.extend((0..4 * window).rev().map(|i| i as f64));
185        for (n, &peak) in peaks.iter().enumerate() {
186            let expected = peaks[n.saturating_sub(window - 1)..=n]
187                .iter()
188                .copied()
189                .fold(0.0, f64::max);
190            assert_eq!(limiter.push_peak(peak), expected, "frame {n}");
191            assert!(limiter.queue_len <= window);
192        }
193    }
194
195    #[test]
196    fn decreasing_peaks_keep_the_window_maximum() {
197        // A two-frame window: 1 ms of look-ahead at 1 kHz.
198        let settings = MasterLimiter {
199            lookahead_ms: 1.0,
200            ..MasterLimiter::default()
201        };
202        let mut limiter = Limiter::new(settings, 1000, 1).unwrap();
203        assert_eq!(limiter.window, 2);
204        let got: Vec<f64> = [3.0, 2.0, 1.0, 0.5, 4.0, 0.0]
205            .iter()
206            .map(|&peak| limiter.push_peak(peak))
207            .collect();
208        assert_eq!(got, [3.0, 3.0, 2.0, 1.0, 4.0, 4.0]);
209    }
210}