audio_engine_core/processor/loudness/
meter.rs1use crate::processor::dsp::linear_to_db;
4use std::sync::OnceLock;
5
6const TRUE_PEAK_PHASES: usize = 4;
7const TRUE_PEAK_FIR_TAPS: usize = 49;
8const TRUE_PEAK_DELAY: usize = TRUE_PEAK_FIR_TAPS.div_ceil(TRUE_PEAK_PHASES);
9const TRUE_PEAK_HISTORY_LEN: usize = TRUE_PEAK_DELAY * 2;
10const TRUE_PEAK_INTER_SAMPLE_TAPS: usize = TRUE_PEAK_DELAY - 1;
11
12static TRUE_PEAK_FIR: OnceLock<TruePeakFir> = OnceLock::new();
13
14#[derive(Clone, Copy)]
15struct TruePeakFir {
16 sample_phase_coeff: f64,
17 inter_sample_coeffs: [[f64; TRUE_PEAK_INTER_SAMPLE_TAPS]; TRUE_PEAK_PHASES - 1],
18}
19
20pub struct LoudnessMeter {
23 ebur128: Option<ebur128::EbuR128>,
24 sample_rate: u32,
25 channels: usize,
26 integrated_loudness: f64,
28 short_term_loudness: f64,
29 momentary_loudness: f64,
30 loudness_range: f64,
31 true_peak: f64,
32 samples_processed: u64,
33 true_peak_detectors: Vec<TruePeakDetector>,
35}
36
37impl LoudnessMeter {
38 pub fn new(channels: usize, sample_rate: u32) -> Self {
39 let ebur128 =
40 ebur128::EbuR128::new(channels as u32, sample_rate, ebur128::Mode::all()).ok();
41
42 let true_peak_detectors = (0..channels).map(|_| TruePeakDetector::new()).collect();
44
45 Self {
46 ebur128,
47 sample_rate,
48 channels,
49 integrated_loudness: -70.0,
50 short_term_loudness: -70.0,
51 momentary_loudness: -70.0,
52 loudness_range: 0.0,
53 true_peak: -70.0,
54 samples_processed: 0,
55 true_peak_detectors,
56 }
57 }
58
59 pub fn reset(&mut self) {
61 if let Some(ref mut ebur) = self.ebur128 {
62 ebur.reset();
63 }
64 self.integrated_loudness = -70.0;
65 self.short_term_loudness = -70.0;
66 self.momentary_loudness = -70.0;
67 self.loudness_range = 0.0;
68 self.true_peak = -70.0;
69 self.samples_processed = 0;
70 for detector in &mut self.true_peak_detectors {
72 detector.reset();
73 }
74 }
75
76 pub fn process(&mut self, samples: &[f64]) {
78 let Some(ref mut ebur) = self.ebur128 else {
79 return;
80 };
81
82 let frames = samples.len() / self.channels;
83 if frames == 0 {
84 return;
85 }
86 let sample_count = frames * self.channels;
87 let samples = &samples[..sample_count];
88
89 if let Err(e) = ebur.add_frames_f64(samples) {
90 log::warn!("EBU R128 add_frames error: {:?}", e);
91 return;
92 }
93
94 self.samples_processed += frames as u64;
95
96 if let Ok(loudness) = ebur.loudness_global() {
98 self.integrated_loudness = loudness;
99 }
100
101 if let Ok(loudness) = ebur.loudness_shortterm() {
102 self.short_term_loudness = loudness;
103 }
104
105 if let Ok(loudness) = ebur.loudness_momentary() {
106 self.momentary_loudness = loudness;
107 }
108
109 if let Ok(lra) = ebur.loudness_range() {
110 self.loudness_range = lra;
111 }
112
113 let fir = true_peak_fir();
115 for frame in samples.chunks_exact(self.channels) {
116 for (sample, detector) in frame.iter().zip(self.true_peak_detectors.iter_mut()) {
117 detector.process_sample(*sample, fir);
118 }
119 }
120
121 let max_true_peak = self
123 .true_peak_detectors
124 .iter()
125 .map(|d| d.max_true_peak())
126 .fold(0.0_f64, f64::max);
127
128 if max_true_peak > 0.0 {
129 let peak_db = 20.0 * max_true_peak.log10();
130 self.true_peak = peak_db.max(self.true_peak);
131 }
132 }
133
134 pub fn integrated_loudness(&self) -> f64 {
135 self.integrated_loudness
136 }
137 pub fn short_term_loudness(&self) -> f64 {
138 self.short_term_loudness
139 }
140 pub fn momentary_loudness(&self) -> f64 {
141 self.momentary_loudness
142 }
143 pub fn loudness_range(&self) -> f64 {
144 self.loudness_range
145 }
146 pub fn true_peak(&self) -> f64 {
147 self.true_peak
148 }
149 pub fn samples_processed(&self) -> u64 {
150 self.samples_processed
151 }
152
153 pub fn has_reliable_measurement(&self) -> bool {
154 let min_samples = (self.sample_rate as f64 * 0.4) as u64;
155 self.samples_processed >= min_samples
156 }
157}
158
159pub struct TruePeakDetector {
169 history: [f64; TRUE_PEAK_HISTORY_LEN],
171 write_pos: usize,
172 max_true_peak: f64,
174}
175
176impl TruePeakDetector {
177 pub fn new() -> Self {
178 let _ = true_peak_fir();
179 Self {
180 history: [0.0; TRUE_PEAK_HISTORY_LEN],
181 write_pos: 0,
182 max_true_peak: 0.0,
183 }
184 }
185
186 pub fn process(&mut self, samples: &[f64]) {
188 let fir = true_peak_fir();
189 for &sample in samples {
190 self.process_sample(sample, fir);
191 }
192 }
193
194 pub fn process_strided(&mut self, samples: &[f64], offset: usize, stride: usize) {
196 let fir = true_peak_fir();
197 let mut index = offset;
198 while index < samples.len() {
199 self.process_sample(samples[index], fir);
200 index += stride;
201 }
202 }
203
204 #[inline]
205 fn process_sample(&mut self, sample: f64, fir: &TruePeakFir) {
206 self.max_true_peak = self.max_true_peak.max(sample.abs());
207
208 self.history[self.write_pos] = sample;
209 self.history[self.write_pos + TRUE_PEAK_DELAY] = sample;
210
211 let dot_base = self.write_pos + TRUE_PEAK_DELAY - 11;
212 let history = &self.history[dot_base..dot_base + TRUE_PEAK_INTER_SAMPLE_TAPS];
213 let phase1 = dot12_contiguous(history, &fir.inter_sample_coeffs[0]);
214 let phase2 = dot12_contiguous(history, &fir.inter_sample_coeffs[1]);
215 let phase3 = dot12_contiguous(history, &fir.inter_sample_coeffs[2]);
216
217 self.max_true_peak = self
218 .max_true_peak
219 .max(phase1.abs())
220 .max(phase2.abs())
221 .max(phase3.abs());
222
223 self.write_pos += 1;
224 if self.write_pos == TRUE_PEAK_DELAY {
225 self.write_pos = 0;
226 }
227 }
228
229 pub fn max_true_peak(&self) -> f64 {
231 self.max_true_peak
232 }
233
234 pub fn max_true_peak_db(&self) -> f64 {
236 linear_to_db(self.max_true_peak)
237 }
238
239 pub fn reset(&mut self) {
241 self.history.fill(0.0);
242 self.write_pos = 0;
243 self.max_true_peak = 0.0;
244 }
245}
246
247impl Default for TruePeakDetector {
248 fn default() -> Self {
249 Self::new()
250 }
251}
252
253fn true_peak_fir() -> &'static TruePeakFir {
254 TRUE_PEAK_FIR.get_or_init(generate_true_peak_fir)
255}
256
257fn generate_true_peak_fir() -> TruePeakFir {
258 let mut fir = TruePeakFir {
259 sample_phase_coeff: 0.0,
260 inter_sample_coeffs: [[0.0; TRUE_PEAK_INTER_SAMPLE_TAPS]; TRUE_PEAK_PHASES - 1],
261 };
262 let center = (TRUE_PEAK_FIR_TAPS as f64 - 1.0) * 0.5;
263
264 for tap_index in 0..TRUE_PEAK_FIR_TAPS {
265 let phase = tap_index % TRUE_PEAK_PHASES;
266 let position = tap_index as f64 - center;
267 let window = 0.5
268 * (1.0
269 - (2.0 * std::f64::consts::PI * tap_index as f64
270 / (TRUE_PEAK_FIR_TAPS as f64 - 1.0))
271 .cos());
272 let coeff = sinc(position / TRUE_PEAK_PHASES as f64) * window;
273
274 if coeff.abs() > 1.0e-12 {
275 if phase == 0 {
276 fir.sample_phase_coeff = coeff;
277 } else {
278 fir.inter_sample_coeffs[phase - 1][tap_index / TRUE_PEAK_PHASES] = coeff;
279 }
280 }
281 }
282
283 fir
284}
285
286#[inline]
287fn dot12_contiguous(history: &[f64], coeffs: &[f64; TRUE_PEAK_INTER_SAMPLE_TAPS]) -> f64 {
288 history[11] * coeffs[0]
289 + history[10] * coeffs[1]
290 + history[9] * coeffs[2]
291 + history[8] * coeffs[3]
292 + history[7] * coeffs[4]
293 + history[6] * coeffs[5]
294 + history[5] * coeffs[6]
295 + history[4] * coeffs[7]
296 + history[3] * coeffs[8]
297 + history[2] * coeffs[9]
298 + history[1] * coeffs[10]
299 + history[0] * coeffs[11]
300}
301
302#[inline]
303fn sinc(x: f64) -> f64 {
304 if x.abs() < 1.0e-12 {
305 1.0
306 } else {
307 let pix = std::f64::consts::PI * x;
308 pix.sin() / pix
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 fn deterministic_interleaved(frames: usize, channels: usize) -> Vec<f64> {
317 let mut samples = Vec::with_capacity(frames * channels);
318 for frame in 0..frames {
319 for ch in 0..channels {
320 let sample = ((frame as f64 * 0.017) + ch as f64 * 0.13).sin() * 0.5;
321 samples.push(sample);
322 }
323 }
324 samples
325 }
326
327 #[test]
328 fn true_peak_strided_matches_channel_extract_for_common_channel_counts() {
329 for channels in [1, 2, 6, 8] {
330 let samples = deterministic_interleaved(512, channels);
331
332 for ch in 0..channels {
333 let channel_samples: Vec<f64> =
334 samples.iter().skip(ch).step_by(channels).copied().collect();
335 let mut contiguous = TruePeakDetector::new();
336 let mut strided = TruePeakDetector::new();
337
338 contiguous.process(&channel_samples);
339 strided.process_strided(&samples, ch, channels);
340
341 assert_eq!(
342 contiguous.max_true_peak().to_bits(),
343 strided.max_true_peak().to_bits(),
344 "channels={channels}, channel={ch}"
345 );
346 }
347 }
348 }
349
350 #[test]
351 fn loudness_meter_truncates_partial_frames() {
352 let mut meter = LoudnessMeter::new(2, 48_000);
353 let samples = vec![0.1, -0.1, 0.2];
354
355 meter.process(&samples);
356
357 assert_eq!(meter.samples_processed(), 1);
358 }
359
360 #[test]
361 fn loudness_meter_process_is_steady_state_no_alloc() {
362 let mut meter = LoudnessMeter::new(2, 48_000);
363 let samples = deterministic_interleaved(64, 2);
364
365 assert_no_alloc::assert_no_alloc(|| {
366 for _ in 0..1_000 {
367 meter.process(&samples);
368 }
369 });
370 }
371
372 #[test]
373 fn loudness_meter_handles_surround_channel_counts() {
374 for channels in [1, 2, 6, 8] {
375 let mut meter = LoudnessMeter::new(channels, 48_000);
376 let samples = deterministic_interleaved(256, channels);
377
378 meter.process(&samples);
379
380 assert_eq!(meter.samples_processed(), 256);
381 assert!(meter.true_peak().is_finite());
382 }
383 }
384
385 #[test]
386 fn true_peak_fir_matches_libebur128_polyphase_shape() {
387 let fir = true_peak_fir();
388
389 assert!(fir.sample_phase_coeff.is_finite());
390 assert!(fir.sample_phase_coeff.abs() > 1.0e-12);
391
392 for phase in 0..TRUE_PEAK_PHASES - 1 {
393 for tap in 0..TRUE_PEAK_INTER_SAMPLE_TAPS {
394 assert!(fir.inter_sample_coeffs[phase][tap].is_finite());
395 assert!(fir.inter_sample_coeffs[phase][tap].abs() > 1.0e-12);
396 }
397 }
398 }
399
400 #[test]
401 fn true_peak_reset_clears_ring_history() {
402 let mut detector = TruePeakDetector::new();
403 detector.process(&[1.0; TRUE_PEAK_DELAY]);
404 assert!(detector.max_true_peak() > 0.0);
405
406 detector.reset();
407 detector.process(&[0.0; TRUE_PEAK_DELAY]);
408
409 assert_eq!(detector.max_true_peak(), 0.0);
410 }
411
412 #[test]
413 fn true_peak_cross_buffer_continuity_matches_single_process() {
414 let samples: Vec<f64> = (0..1024).map(|i| (i as f64 * 0.071).sin()).collect();
415 let mut single = TruePeakDetector::new();
416 let mut chunked = TruePeakDetector::new();
417
418 single.process(&samples);
419 for chunk in samples.chunks(17) {
420 chunked.process(chunk);
421 }
422
423 assert_eq!(
424 single.max_true_peak().to_bits(),
425 chunked.max_true_peak().to_bits()
426 );
427 }
428
429 #[test]
430 fn true_peak_impulse_reaches_sample_peak_without_cubic_overshoot() {
431 let mut detector = TruePeakDetector::new();
432 let mut samples = vec![0.0; TRUE_PEAK_DELAY * 2];
433 samples[TRUE_PEAK_DELAY / 2] = 1.0;
434
435 detector.process(&samples);
436
437 assert!(detector.max_true_peak() >= 1.0);
438 assert!(detector.max_true_peak() < 1.1);
439 }
440}