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