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