audio_engine_core/processor/loudness/
limiter.rs1use crate::processor::dsp::{db_to_linear, linear_to_db};
6
7#[derive(Debug, Clone)]
8struct MonotonicMaxQueue {
9 indices: Box<[u64]>,
10 peaks: Box<[f64]>,
11 head: usize,
12 tail: usize,
13 len: usize,
14}
15
16impl MonotonicMaxQueue {
17 fn new(capacity: usize) -> Self {
18 let capacity = capacity.max(1);
19 Self {
20 indices: vec![0; capacity].into_boxed_slice(),
21 peaks: vec![0.0; capacity].into_boxed_slice(),
22 head: 0,
23 tail: 0,
24 len: 0,
25 }
26 }
27
28 #[inline]
29 fn clear(&mut self) {
30 self.head = 0;
31 self.tail = 0;
32 self.len = 0;
33 }
34
35 #[inline]
36 fn current_peak(&self) -> f64 {
37 if self.len == 0 {
38 0.0
39 } else {
40 self.peaks[self.head]
41 }
42 }
43
44 #[inline]
45 fn push(&mut self, frame_index: u64, peak: f64) {
46 while self.len > 0 && self.back_peak() <= peak {
47 self.pop_back();
48 }
49
50 if self.len == self.indices.len() {
51 self.pop_front();
52 }
53
54 self.indices[self.tail] = frame_index;
55 self.peaks[self.tail] = peak;
56 self.tail = (self.tail + 1) % self.indices.len();
57 self.len += 1;
58 }
59
60 #[inline]
61 fn expire_through(&mut self, max_expired_index: u64) {
62 while self.len > 0 && self.indices[self.head] <= max_expired_index {
63 self.pop_front();
64 }
65 }
66
67 #[inline]
68 fn back_peak(&self) -> f64 {
69 let index = (self.tail + self.indices.len() - 1) % self.indices.len();
70 self.peaks[index]
71 }
72
73 #[inline]
74 fn pop_front(&mut self) {
75 self.head = (self.head + 1) % self.indices.len();
76 self.len -= 1;
77 }
78
79 #[inline]
80 fn pop_back(&mut self) {
81 self.tail = (self.tail + self.indices.len() - 1) % self.indices.len();
82 self.len -= 1;
83 }
84}
85
86pub struct PeakLimiter {
95 threshold: f64,
97 lookahead_frames: usize,
99 delay_buffer: Box<[f64]>,
101 peak_queue: MonotonicMaxQueue,
103 global_frame: u64,
105 write_pos: usize,
107 gain_reduction: f64,
109 release_coeff: f64,
111 channels: usize,
113 sample_rate: f64,
115}
116
117impl PeakLimiter {
118 pub fn new(
127 channels: usize,
128 sample_rate: u32,
129 threshold_db: f64,
130 lookahead_ms: f64,
131 release_ms: f64,
132 ) -> Self {
133 let threshold = db_to_linear(threshold_db);
134 let lookahead_frames = ((lookahead_ms / 1000.0) * sample_rate as f64).ceil() as usize;
135 let lookahead_frames = lookahead_frames.max(1);
136
137 let release_samples = (release_ms / 1000.0) * sample_rate as f64;
140 let release_coeff = (-1.0 / release_samples).exp();
141
142 let buffer_size = lookahead_frames * channels;
144 let delay_buffer = vec![0.0; buffer_size].into_boxed_slice();
145
146 Self {
147 threshold,
148 lookahead_frames,
149 delay_buffer,
150 peak_queue: MonotonicMaxQueue::new(lookahead_frames),
151 global_frame: 0,
152 write_pos: 0,
153 gain_reduction: 1.0,
154 release_coeff,
155 channels,
156 sample_rate: sample_rate as f64,
157 }
158 }
159
160 pub fn process(&mut self, samples: &mut [f64]) {
167 let total_samples = samples.len();
168 let frames = total_samples / self.channels;
169 if frames == 0 {
170 return;
171 }
172
173 for frame in 0..frames {
174 let peak = self.peak_queue.current_peak();
178
179 let target_gain = if peak > self.threshold {
181 self.threshold / peak
182 } else {
183 1.0
184 };
185
186 if target_gain < self.gain_reduction {
190 self.gain_reduction = target_gain;
192 } else {
193 self.gain_reduction =
195 self.gain_reduction + (1.0 - self.gain_reduction) * (1.0 - self.release_coeff);
196 self.gain_reduction = self.gain_reduction.min(target_gain);
198 }
199
200 let mut frame_peak = 0.0_f64;
202 for ch in 0..self.channels {
203 let input_idx = frame * self.channels + ch;
204 let buffer_idx = self.write_pos * self.channels + ch;
205 let input = samples[input_idx];
206 frame_peak = frame_peak.max(input.abs());
207
208 let delayed = self.delay_buffer[buffer_idx];
210
211 self.delay_buffer[buffer_idx] = input;
213
214 samples[input_idx] = delayed * self.gain_reduction;
216 }
217
218 self.push_frame_peak(frame_peak);
219
220 self.write_pos = (self.write_pos + 1) % self.lookahead_frames;
222 }
223 }
224
225 #[inline]
226 fn push_frame_peak(&mut self, frame_peak: f64) {
227 if self.global_frame >= self.lookahead_frames as u64 {
228 self.peak_queue
229 .expire_through(self.global_frame - self.lookahead_frames as u64);
230 }
231 self.peak_queue.push(self.global_frame, frame_peak);
232 self.global_frame = self.global_frame.wrapping_add(1);
233 }
234
235 pub fn set_threshold_db(&mut self, threshold_db: f64) {
237 self.threshold = db_to_linear(threshold_db);
238 }
239
240 pub fn set_threshold(&mut self, threshold_db: f64) {
242 self.threshold = db_to_linear(threshold_db);
243 }
244
245 pub fn set_release_ms(&mut self, release_ms: f64) {
247 let release_samples = (release_ms / 1000.0) * self.sample_rate;
248 self.release_coeff = (-1.0 / release_samples.max(1.0)).exp();
249 }
250
251 pub fn is_enabled(&self) -> bool {
253 true
254 }
255
256 pub fn gain_reduction_db(&self) -> f64 {
258 linear_to_db(self.gain_reduction)
259 }
260
261 pub fn reset(&mut self) {
263 for sample in self.delay_buffer.iter_mut() {
264 *sample = 0.0;
265 }
266 self.peak_queue.clear();
267 self.global_frame = 0;
268 self.write_pos = 0;
269 self.gain_reduction = 1.0;
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 struct LegacyPeakLimiter {
278 threshold: f64,
279 lookahead_frames: usize,
280 delay_buffer: Box<[f64]>,
281 write_pos: usize,
282 gain_reduction: f64,
283 release_coeff: f64,
284 channels: usize,
285 }
286
287 impl LegacyPeakLimiter {
288 fn new(
289 channels: usize,
290 sample_rate: u32,
291 threshold_db: f64,
292 lookahead_ms: f64,
293 release_ms: f64,
294 ) -> Self {
295 let threshold = db_to_linear(threshold_db);
296 let lookahead_frames = ((lookahead_ms / 1000.0) * sample_rate as f64).ceil() as usize;
297 let lookahead_frames = lookahead_frames.max(1);
298 let release_samples = (release_ms / 1000.0) * sample_rate as f64;
299 let release_coeff = (-1.0 / release_samples).exp();
300
301 Self {
302 threshold,
303 lookahead_frames,
304 delay_buffer: vec![0.0; lookahead_frames * channels].into_boxed_slice(),
305 write_pos: 0,
306 gain_reduction: 1.0,
307 release_coeff,
308 channels,
309 }
310 }
311
312 fn process(&mut self, samples: &mut [f64]) {
313 let frames = samples.len() / self.channels;
314 if frames == 0 {
315 return;
316 }
317
318 for frame in 0..frames {
319 let peak = self.scan_lookahead_peak();
320 let target_gain = if peak > self.threshold {
321 self.threshold / peak
322 } else {
323 1.0
324 };
325
326 if target_gain < self.gain_reduction {
327 self.gain_reduction = target_gain;
328 } else {
329 self.gain_reduction = self.gain_reduction
330 + (1.0 - self.gain_reduction) * (1.0 - self.release_coeff);
331 self.gain_reduction = self.gain_reduction.min(target_gain);
332 }
333
334 for ch in 0..self.channels {
335 let input_idx = frame * self.channels + ch;
336 let buffer_idx = self.write_pos * self.channels + ch;
337 let delayed = self.delay_buffer[buffer_idx];
338 self.delay_buffer[buffer_idx] = samples[input_idx];
339 samples[input_idx] = delayed * self.gain_reduction;
340 }
341
342 self.write_pos = (self.write_pos + 1) % self.lookahead_frames;
343 }
344 }
345
346 fn scan_lookahead_peak(&self) -> f64 {
347 let mut peak = 0.0_f64;
348 for frame in 0..self.lookahead_frames {
349 let pos = (self.write_pos + frame) % self.lookahead_frames;
350 for ch in 0..self.channels {
351 let idx = pos * self.channels + ch;
352 peak = peak.max(self.delay_buffer[idx].abs());
353 }
354 }
355 peak
356 }
357 }
358
359 fn assert_samples_eq(left: &[f64], right: &[f64]) {
360 assert_eq!(left.len(), right.len());
361 for (index, (a, b)) in left.iter().zip(right.iter()).enumerate() {
362 assert_eq!(
363 a.to_bits(),
364 b.to_bits(),
365 "sample {index}: left={a}, right={b}"
366 );
367 }
368 }
369
370 fn deterministic_transient_corpus(frames: usize, channels: usize) -> Vec<f64> {
371 let mut samples = Vec::with_capacity(frames * channels);
372 for frame in 0..frames {
373 let base =
374 ((frame as f64 * 0.037).sin() * 0.35) + ((frame as f64 * 0.011).cos() * 0.08);
375 for ch in 0..channels {
376 let mut sample = base * (1.0 - ch as f64 * 0.15);
377 if matches!(frame, 32 | 257 | 513 | 1024) {
378 sample = if ch == 0 { 1.8 } else { -1.35 };
379 }
380 samples.push(sample);
381 }
382 }
383 samples
384 }
385
386 #[test]
387 fn monotonic_queue_matches_legacy_scan_for_transient_corpus() {
388 let mut limiter = PeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
389 let mut legacy = LegacyPeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
390 let mut samples = deterministic_transient_corpus(2_000, 2);
391 let mut expected = samples.clone();
392
393 limiter.process(&mut samples);
394 legacy.process(&mut expected);
395
396 assert_samples_eq(&samples, &expected);
397 }
398
399 #[test]
400 fn monotonic_queue_preserves_cross_buffer_continuity() {
401 let source = deterministic_transient_corpus(6_400, 2);
402 let mut one_shot = source.clone();
403 let mut chunked = source.clone();
404
405 let mut one_shot_limiter = PeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
406 let mut chunked_limiter = PeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
407
408 one_shot_limiter.process(&mut one_shot);
409 for chunk in chunked.chunks_mut(64 * 2) {
410 chunked_limiter.process(chunk);
411 }
412
413 assert_samples_eq(&chunked, &one_shot);
414 }
415
416 #[test]
417 fn monotonic_queue_handles_sustained_pre_clipping() {
418 let mut limiter = PeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
419 let mut samples = vec![1.2; 2_000 * 2];
420
421 limiter.process(&mut samples);
422
423 let expected_gain = db_to_linear(-1.0) / 1.2;
424 assert!((limiter.gain_reduction - expected_gain).abs() < 1e-12);
425 assert!(samples
426 .iter()
427 .all(|sample| sample.abs() <= db_to_linear(-1.0) + 1e-12));
428 }
429
430 #[test]
431 fn monotonic_queue_resets_state() {
432 let mut limiter = PeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
433 let mut samples = deterministic_transient_corpus(1_000, 2);
434
435 limiter.process(&mut samples);
436 assert!(limiter.peak_queue.current_peak() > 0.0);
437
438 limiter.reset();
439
440 assert_eq!(limiter.peak_queue.current_peak(), 0.0);
441 assert_eq!(limiter.global_frame, 0);
442 assert_eq!(limiter.write_pos, 0);
443 assert_eq!(limiter.gain_reduction, 1.0);
444 }
445
446 #[test]
447 fn lookahead_one_frame_matches_legacy_scan() {
448 let mut limiter = PeakLimiter::new(2, 1_000, -1.0, 1.0, 10.0);
449 let mut legacy = LegacyPeakLimiter::new(2, 1_000, -1.0, 1.0, 10.0);
450 let mut samples = deterministic_transient_corpus(128, 2);
451 let mut expected = samples.clone();
452
453 limiter.process(&mut samples);
454 legacy.process(&mut expected);
455
456 assert_samples_eq(&samples, &expected);
457 }
458
459 #[test]
460 fn non_finite_samples_do_not_poison_queue_peak() {
461 let mut limiter = PeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
462 let mut samples = vec![0.2; 64 * 2];
463 samples[4] = f64::NAN;
464 samples[9] = f64::INFINITY;
465
466 limiter.process(&mut samples);
467
468 assert!(limiter.peak_queue.current_peak().is_infinite());
469
470 let mut finite_samples = vec![0.25; 600 * 2];
471 limiter.process(&mut finite_samples);
472
473 assert!(limiter.peak_queue.current_peak().is_finite());
474 assert_eq!(limiter.peak_queue.current_peak(), 0.25);
475 }
476
477 #[test]
478 fn process_is_steady_state_no_alloc() {
479 let mut limiter = PeakLimiter::new(2, 48_000, -1.0, 10.0, 100.0);
480 let mut samples = deterministic_transient_corpus(64, 2);
481
482 assert_no_alloc::assert_no_alloc(|| {
483 for _ in 0..1_000 {
484 limiter.process(&mut samples);
485 }
486 });
487 }
488}