1use std::collections::VecDeque;
7use std::f64::consts::PI;
8
9#[cfg(test)]
12const MAX_GATING_BLOCKS: usize = 100;
13#[cfg(not(test))]
14const MAX_GATING_BLOCKS: usize = 36_000;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Mode(u32);
21
22impl Mode {
23 pub const M: Mode = Mode(1 << 0);
24 pub const S: Mode = Mode(1 << 1);
25 pub const I: Mode = Mode(1 << 2);
26 pub const SAMPLE_PEAK: Mode = Mode(1 << 3);
27 pub const TRUE_PEAK: Mode = Mode(1 << 4);
28
29 pub const fn all() -> Mode {
30 Mode(0x1F)
31 }
32
33 fn has(self, flag: Mode) -> bool {
34 self.0 & flag.0 != 0
35 }
36}
37
38impl std::ops::BitOr for Mode {
39 type Output = Mode;
40 fn bitor(self, rhs: Mode) -> Mode {
41 Mode(self.0 | rhs.0)
42 }
43}
44
45#[derive(Clone)]
49struct Biquad {
50 b0: f64,
51 b1: f64,
52 b2: f64,
53 a1: f64,
54 a2: f64,
55 z1: f64,
56 z2: f64,
57}
58
59impl Biquad {
60 fn process(&mut self, x: f64) -> f64 {
61 let y = self.b0 * x + self.z1;
62 self.z1 = self.b1 * x - self.a1 * y + self.z2;
63 self.z2 = self.b2 * x - self.a2 * y;
64 y
65 }
66
67 fn reset(&mut self) {
68 self.z1 = 0.0;
69 self.z2 = 0.0;
70 }
71}
72
73#[derive(Clone)]
76struct KWeightFilter {
77 stage1: Biquad,
78 stage2: Biquad,
79}
80
81impl KWeightFilter {
82 fn new(sample_rate: u32) -> Self {
83 let (s1, s2) = if sample_rate == 48000 {
84 Self::coeffs_48k()
85 } else {
86 Self::compute_coeffs(sample_rate as f64)
87 };
88 Self {
89 stage1: s1,
90 stage2: s2,
91 }
92 }
93
94 fn coeffs_48k() -> (Biquad, Biquad) {
96 let s1 = Biquad {
98 b0: 1.53512485958697,
99 b1: -2.69169618940638,
100 b2: 1.19839281085285,
101 a1: -1.69065929318241,
102 a2: 0.73248077421585,
103 z1: 0.0,
104 z2: 0.0,
105 };
106 let s2 = Biquad {
108 b0: 1.0,
109 b1: -2.0,
110 b2: 1.0,
111 a1: -1.99004745483398,
112 a2: 0.99007225036621,
113 z1: 0.0,
114 z2: 0.0,
115 };
116 (s1, s2)
117 }
118
119 fn compute_coeffs(fs: f64) -> (Biquad, Biquad) {
121 let fc1 = 1681.974450955533;
125 let q1 = 0.7071752369554196;
126 let db1 = 3.999843853973347;
127 let vh = 10.0_f64.powf(db1 / 20.0);
128 let vb = vh.powf(0.4996667741545416);
129 let k1 = (PI * fc1 / fs).tan();
130 let k1sq = k1 * k1;
131 let denom1 = 1.0 + k1 / q1 + k1sq;
132 let s1 = Biquad {
133 b0: (vh + vb * k1 / q1 + k1sq) / denom1,
134 b1: 2.0 * (k1sq - vh) / denom1,
135 b2: (vh - vb * k1 / q1 + k1sq) / denom1,
136 a1: 2.0 * (k1sq - 1.0) / denom1,
137 a2: (1.0 - k1 / q1 + k1sq) / denom1,
138 z1: 0.0,
139 z2: 0.0,
140 };
141
142 let fc2 = 38.13547087602444;
144 let q2 = 0.5003270373238773;
145 let k2 = (PI * fc2 / fs).tan();
146 let k2sq = k2 * k2;
147 let denom2 = 1.0 + k2 / q2 + k2sq;
148 let s2 = Biquad {
149 b0: 1.0 / denom2,
150 b1: -2.0 / denom2,
151 b2: 1.0 / denom2,
152 a1: 2.0 * (k2sq - 1.0) / denom2,
153 a2: (1.0 - k2 / q2 + k2sq) / denom2,
154 z1: 0.0,
155 z2: 0.0,
156 };
157
158 (s1, s2)
159 }
160
161 fn process(&mut self, x: f64) -> f64 {
162 let y1 = self.stage1.process(x);
163 self.stage2.process(y1)
164 }
165
166 fn reset(&mut self) {
167 self.stage1.reset();
168 self.stage2.reset();
169 }
170}
171
172const TRUE_PEAK_FIR_PHASES: [[f64; 12]; 4] = [
177 [
178 0.0017089843750,
179 -0.0291748046875,
180 -0.0189208984375,
181 0.0776367187500,
182 0.0983886718750,
183 -0.1897583007813,
184 -0.3953857421875,
185 0.8893127441406,
186 0.6444091796875,
187 -0.0517578125000,
188 -0.0245361328125,
189 0.0015869140625,
190 ],
191 [
192 -0.0291748046875,
193 0.0017089843750,
194 0.0776367187500,
195 -0.0189208984375,
196 -0.1897583007813,
197 0.0983886718750,
198 0.8893127441406,
199 -0.3953857421875,
200 -0.0517578125000,
201 0.6444091796875,
202 0.0015869140625,
203 -0.0245361328125,
204 ],
205 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
206 [
207 -0.0245361328125,
208 0.0015869140625,
209 0.6444091796875,
210 -0.0517578125000,
211 -0.3953857421875,
212 0.8893127441406,
213 0.0983886718750,
214 -0.1897583007813,
215 -0.0189208984375,
216 0.0776367187500,
217 0.0017089843750,
218 -0.0291748046875,
219 ],
220];
221
222const TRUE_PEAK_FIR_LEN: usize = 12;
223
224struct TruePeakDetector {
225 history: Vec<[f64; TRUE_PEAK_FIR_LEN]>,
226 peak: Vec<f64>,
227 prev_peak: Vec<f64>,
228}
229
230impl TruePeakDetector {
231 fn new(channels: usize) -> Self {
232 Self {
233 history: vec![[0.0; TRUE_PEAK_FIR_LEN]; channels],
234 peak: vec![0.0; channels],
235 prev_peak: vec![0.0; channels],
236 }
237 }
238
239 fn process_frame(&mut self, ch: usize, sample: f64) {
240 let h = &mut self.history[ch];
242 h.copy_within(1.., 0);
243 h[TRUE_PEAK_FIR_LEN - 1] = sample;
244
245 for phase in &TRUE_PEAK_FIR_PHASES {
247 let mut sum = 0.0;
248 for (i, &coeff) in phase.iter().enumerate() {
249 sum += coeff * h[i];
250 }
251 let abs_val = sum.abs();
252 if abs_val > self.peak[ch] {
253 self.peak[ch] = abs_val;
254 }
255 if abs_val > self.prev_peak[ch] {
256 self.prev_peak[ch] = abs_val;
257 }
258 }
259 }
260
261 fn reset(&mut self) {
262 for h in &mut self.history {
263 h.fill(0.0);
264 }
265 self.peak.fill(0.0);
266 self.prev_peak.fill(0.0);
267 }
268}
269
270struct SubBlockRing {
274 buf: Vec<f64>,
275 pos: usize,
276 count: usize,
277}
278
279impl SubBlockRing {
280 fn new(capacity: usize) -> Self {
281 Self {
282 buf: vec![0.0; capacity],
283 pos: 0,
284 count: 0,
285 }
286 }
287
288 fn push(&mut self, energy: f64) {
289 self.buf[self.pos] = energy;
290 self.pos = (self.pos + 1) % self.buf.len();
291 if self.count < self.buf.len() {
292 self.count += 1;
293 }
294 }
295
296 fn mean(&self) -> Option<f64> {
297 if self.count == 0 {
298 return None;
299 }
300 let sum: f64 = if self.count == self.buf.len() {
301 self.buf.iter().sum()
302 } else {
303 self.buf[..self.count].iter().sum()
304 };
305 Some(sum / self.count as f64)
306 }
307
308 fn is_full(&self) -> bool {
309 self.count >= self.buf.len()
310 }
311
312 fn reset(&mut self) {
313 self.buf.fill(0.0);
314 self.pos = 0;
315 self.count = 0;
316 }
317}
318
319fn channel_weight(ch: usize, num_channels: usize) -> f64 {
322 if num_channels <= 2 {
323 1.0
325 } else if num_channels == 5 {
326 match ch {
328 0..=2 => 1.0, 3 | 4 => 1.41, _ => 0.0,
331 }
332 } else if num_channels == 6 {
333 match ch {
335 0..=2 => 1.0, 3 => 0.0, 4 | 5 => 1.41, _ => 0.0,
339 }
340 } else {
341 1.0
342 }
343}
344
345pub struct EbuR128 {
349 channels: u32,
350 #[allow(dead_code)]
351 sample_rate: u32,
352 mode: Mode,
353
354 filters: Vec<KWeightFilter>,
356 channel_weights: Vec<f64>,
357
358 sub_block_frames: usize, sub_block_accum: Vec<f64>, sub_block_pos: usize, momentary_ring: SubBlockRing,
365 shortterm_ring: SubBlockRing,
366
367 gating_blocks: VecDeque<f64>,
369
370 sample_peak: Vec<f64>,
372 prev_sample_peak: Vec<f64>,
373 true_peak_detector: Option<TruePeakDetector>,
374}
375
376impl EbuR128 {
377 pub fn new(channels: u32, sample_rate: u32, mode: Mode) -> Result<Self, String> {
382 if channels == 0 {
383 return Err("channels must be > 0".into());
384 }
385 let nc = channels as usize;
386 let sub_block_frames = (sample_rate as usize) / 10; let filters: Vec<KWeightFilter> =
389 (0..nc).map(|_| KWeightFilter::new(sample_rate)).collect();
390 let channel_weights: Vec<f64> = (0..nc).map(|ch| channel_weight(ch, nc)).collect();
391
392 let true_peak_detector = if mode.has(Mode::TRUE_PEAK) {
393 Some(TruePeakDetector::new(nc))
394 } else {
395 None
396 };
397
398 Ok(Self {
399 channels,
400 sample_rate,
401 mode,
402 filters,
403 channel_weights,
404 sub_block_frames,
405 sub_block_accum: vec![0.0; nc],
406 sub_block_pos: 0,
407 momentary_ring: SubBlockRing::new(4), shortterm_ring: SubBlockRing::new(30), gating_blocks: if mode.has(Mode::I) {
410 VecDeque::with_capacity(6_000)
413 } else {
414 VecDeque::new()
415 },
416 sample_peak: vec![0.0; nc],
417 prev_sample_peak: vec![0.0; nc],
418 true_peak_detector,
419 })
420 }
421
422 pub fn add_frames_f32(&mut self, samples: &[f32]) -> Result<(), String> {
424 let nc = self.channels as usize;
425 if !samples.len().is_multiple_of(nc) {
426 return Err("samples length must be a multiple of channel count".into());
427 }
428
429 for frame in samples.chunks_exact(nc) {
430 for (ch, &s) in frame.iter().enumerate() {
431 let x = s as f64;
432
433 if self.mode.has(Mode::SAMPLE_PEAK) {
435 let abs_x = x.abs();
436 if abs_x > self.sample_peak[ch] {
437 self.sample_peak[ch] = abs_x;
438 }
439 if abs_x > self.prev_sample_peak[ch] {
440 self.prev_sample_peak[ch] = abs_x;
441 }
442 }
443
444 if let Some(ref mut tp) = self.true_peak_detector {
446 tp.process_frame(ch, x);
447 }
448
449 let y = self.filters[ch].process(x);
451 self.sub_block_accum[ch] += y * y;
452 }
453
454 self.sub_block_pos += 1;
455
456 if self.sub_block_pos >= self.sub_block_frames {
458 self.complete_sub_block();
459 }
460 }
461
462 Ok(())
463 }
464
465 fn complete_sub_block(&mut self) {
466 let nc = self.channels as usize;
467 let n = self.sub_block_frames as f64;
468
469 let mut block_energy = 0.0;
471 for ch in 0..nc {
472 block_energy += self.channel_weights[ch] * (self.sub_block_accum[ch] / n);
473 }
474
475 self.momentary_ring.push(block_energy);
477 self.shortterm_ring.push(block_energy);
478
479 if self.mode.has(Mode::I)
481 && self.momentary_ring.is_full()
482 && let Some(mean_energy) = self.momentary_ring.mean()
483 {
484 if self.gating_blocks.len() >= MAX_GATING_BLOCKS {
489 self.gating_blocks.pop_front();
490 }
491 self.gating_blocks.push_back(mean_energy);
492 }
493
494 self.sub_block_accum.fill(0.0);
496 self.sub_block_pos = 0;
497 }
498
499 pub fn loudness_momentary(&self) -> Result<f64, String> {
501 match self.momentary_ring.mean() {
502 Some(e) if e > 0.0 => Ok(energy_to_loudness(e)),
503 Some(_) => Ok(f64::NEG_INFINITY),
504 None => Ok(f64::NEG_INFINITY),
505 }
506 }
507
508 pub fn loudness_shortterm(&self) -> Result<f64, String> {
510 match self.shortterm_ring.mean() {
511 Some(e) if e > 0.0 => Ok(energy_to_loudness(e)),
512 Some(_) => Ok(f64::NEG_INFINITY),
513 None => Ok(f64::NEG_INFINITY),
514 }
515 }
516
517 pub fn loudness_global(&self) -> Result<f64, String> {
519 if self.gating_blocks.is_empty() {
520 return Ok(f64::NEG_INFINITY);
521 }
522 Ok(self.compute_gated_loudness())
523 }
524
525 fn compute_gated_loudness(&self) -> f64 {
528 let blocks = &self.gating_blocks;
529 if blocks.is_empty() {
530 return f64::NEG_INFINITY;
531 }
532
533 let abs_gate_energy = loudness_to_energy(-70.0);
535 let mut sum_abs = 0.0f64;
536 let mut count_abs = 0usize;
537 for &e in blocks {
538 if e > abs_gate_energy {
539 sum_abs += e;
540 count_abs += 1;
541 }
542 }
543 if count_abs == 0 {
544 return f64::NEG_INFINITY;
545 }
546
547 let mean_above_abs = sum_abs / count_abs as f64;
548
549 let rel_gate_energy = mean_above_abs * loudness_to_energy(-10.0); let mut sum_rel = 0.0f64;
552 let mut count_rel = 0usize;
553 for &e in blocks {
554 if e > rel_gate_energy {
555 sum_rel += e;
556 count_rel += 1;
557 }
558 }
559 if count_rel == 0 {
560 return f64::NEG_INFINITY;
561 }
562
563 let mean_above_rel = sum_rel / count_rel as f64;
564 energy_to_loudness(mean_above_rel)
565 }
566
567 pub fn sample_peak(&self, channel: u32) -> Result<f64, String> {
569 let ch = channel as usize;
570 if ch >= self.channels as usize {
571 return Err(format!("channel {} out of range", channel));
572 }
573 Ok(self.sample_peak[ch])
574 }
575
576 pub fn prev_sample_peak(&mut self, channel: u32) -> Result<f64, String> {
579 let ch = channel as usize;
580 if ch >= self.channels as usize {
581 return Err(format!("channel {} out of range", channel));
582 }
583 let val = self.prev_sample_peak[ch];
584 self.prev_sample_peak[ch] = 0.0;
585 Ok(val)
586 }
587
588 pub fn prev_true_peak(&mut self, channel: u32) -> Result<f64, String> {
591 let ch = channel as usize;
592 if ch >= self.channels as usize {
593 return Err(format!("channel {} out of range", channel));
594 }
595 match &mut self.true_peak_detector {
596 Some(tp) => {
597 let val = tp.prev_peak[ch];
598 tp.prev_peak[ch] = 0.0;
599 Ok(val)
600 }
601 None => Ok(0.0),
602 }
603 }
604
605 pub fn gating_block_count_and_energy(&self) -> Option<(u64, f64)> {
608 if self.gating_blocks.is_empty() {
609 return None;
610 }
611
612 let abs_gate_energy = loudness_to_energy(-70.0);
614 let above_abs: Vec<f64> = self
615 .gating_blocks
616 .iter()
617 .copied()
618 .filter(|&e| e > abs_gate_energy)
619 .collect();
620 if above_abs.is_empty() {
621 return None;
622 }
623 let mean_above_abs = above_abs.iter().sum::<f64>() / above_abs.len() as f64;
624
625 let rel_gate_energy = mean_above_abs * loudness_to_energy(-10.0);
627 let mut count: u64 = 0;
628 let mut total_energy: f64 = 0.0;
629 for &e in &self.gating_blocks {
630 if e > rel_gate_energy {
631 count += 1;
632 total_energy += e;
633 }
634 }
635
636 if count == 0 {
637 None
638 } else {
639 Some((count, total_energy))
640 }
641 }
642
643 pub fn reset(&mut self) {
645 for f in &mut self.filters {
646 f.reset();
647 }
648 self.sub_block_accum.fill(0.0);
649 self.sub_block_pos = 0;
650 self.momentary_ring.reset();
651 self.shortterm_ring.reset();
652 self.gating_blocks.clear();
653 self.sample_peak.fill(0.0);
654 self.prev_sample_peak.fill(0.0);
655 if let Some(ref mut tp) = self.true_peak_detector {
656 tp.reset();
657 }
658 }
659}
660
661pub fn energy_to_loudness(energy: f64) -> f64 {
663 -0.691 + 10.0 * energy.log10()
664}
665
666fn loudness_to_energy(lufs: f64) -> f64 {
668 10.0_f64.powf((lufs + 0.691) / 10.0)
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
676 fn silence_returns_neg_inf() {
677 let mut meter = EbuR128::new(2, 48000, Mode::all()).unwrap();
678 let silence = vec![0.0f32; 48000 * 2]; meter.add_frames_f32(&silence).unwrap();
680 let lufs = meter.loudness_global().unwrap();
681 assert!(lufs == f64::NEG_INFINITY || lufs < -100.0);
682 }
683
684 #[test]
685 fn sine_1khz_loudness() {
686 let sr = 48000;
687 let duration_s = 5;
688 let num_frames = sr * duration_s;
689 let mut samples = vec![0.0f32; num_frames * 2];
690
691 for i in 0..num_frames {
693 let t = i as f64 / sr as f64;
694 let s = (2.0 * PI * 1000.0 * t).sin() as f32;
695 samples[i * 2] = s;
696 samples[i * 2 + 1] = s;
697 }
698
699 let mut meter = EbuR128::new(2, sr as u32, Mode::all()).unwrap();
700 meter.add_frames_f32(&samples).unwrap();
701
702 let lufs = meter.loudness_global().unwrap();
703 assert!(
706 lufs > -2.0 && lufs < 1.0,
707 "Expected ~-0.3 LUFS for 0dBFS stereo 1kHz sine, got {lufs}"
708 );
709 }
710
711 #[test]
712 fn sample_peak_tracking() {
713 let mut meter = EbuR128::new(1, 48000, Mode::SAMPLE_PEAK).unwrap();
714 let mut samples = vec![0.0f32; 4800]; samples[100] = 0.75;
716 samples[200] = -0.9;
717 meter.add_frames_f32(&samples).unwrap();
718
719 let peak = meter.sample_peak(0).unwrap();
720 assert!((peak - 0.9).abs() < 1e-6, "Expected peak ~0.9, got {peak}");
721 }
722
723 #[test]
724 fn reset_clears_state() {
725 let mut meter = EbuR128::new(2, 48000, Mode::all()).unwrap();
726 let samples = vec![0.5f32; 48000 * 2];
727 meter.add_frames_f32(&samples).unwrap();
728
729 meter.reset();
730
731 let lufs = meter.loudness_global().unwrap();
732 assert!(lufs == f64::NEG_INFINITY || lufs < -100.0);
733 assert_eq!(meter.sample_peak(0).unwrap(), 0.0);
734 }
735
736 #[test]
737 fn energy_to_loudness_roundtrip() {
738 let lufs = -23.0;
739 let energy = loudness_to_energy(lufs);
740 let back = energy_to_loudness(energy);
741 assert!((back - lufs).abs() < 1e-10);
742 }
743
744 #[test]
745 fn gating_block_count_and_energy() {
746 let sr = 48000;
747 let duration_s = 5;
748 let num_frames = sr * duration_s;
749 let mut samples = vec![0.0f32; num_frames * 2];
750
751 for i in 0..num_frames {
752 let t = i as f64 / sr as f64;
753 let s = (2.0 * PI * 440.0 * t).sin() as f32 * 0.5;
754 samples[i * 2] = s;
755 samples[i * 2 + 1] = s;
756 }
757
758 let mut meter = EbuR128::new(2, sr as u32, Mode::I).unwrap();
759 meter.add_frames_f32(&samples).unwrap();
760
761 let result = meter.gating_block_count_and_energy();
762 assert!(result.is_some());
763 let (count, energy) = result.unwrap();
764 assert!(count > 0);
765 assert!(energy > 0.0);
766
767 let album_lufs = energy_to_loudness(energy / count as f64);
769 let global_lufs = meter.loudness_global().unwrap();
770 assert!(
771 (album_lufs - global_lufs).abs() < 0.5,
772 "Album LUFS {album_lufs} should match global {global_lufs}"
773 );
774 }
775
776 #[test]
777 fn gating_blocks_overflow_oldest_dropped() {
778 let sr = 48000;
782 let duration_s = 11;
785 let num_frames = sr * duration_s;
786 let mut samples = vec![0.0f32; num_frames * 2];
787
788 for i in 0..num_frames {
789 let t = i as f64 / sr as f64;
790 let s = (2.0 * PI * 440.0 * t).sin() as f32 * 0.5;
791 samples[i * 2] = s;
792 samples[i * 2 + 1] = s;
793 }
794
795 let mut meter = EbuR128::new(2, sr as u32, Mode::I).unwrap();
796 meter.add_frames_f32(&samples).unwrap();
797
798 let lufs = meter.loudness_global().unwrap();
800 assert!(
801 lufs > -30.0 && lufs < 5.0,
802 "global loudness should be reasonable after overflow, got {lufs}"
803 );
804
805 let result = meter.gating_block_count_and_energy();
806 assert!(
807 result.is_some(),
808 "gating_block_count_and_energy should work after overflow"
809 );
810 }
811}