1extern crate alloc;
8use alloc::vec;
9use alloc::vec::Vec;
10
11use resonant_filters::biquad::{Biquad, BiquadCoeffs};
12use resonant_filters::{design, PolyphaseResampler};
13
14use crate::AnalysisError;
15
16#[derive(Debug, Clone)]
23pub struct ChannelConfig {
24 pub weights: Vec<f32>,
26}
27
28impl ChannelConfig {
29 #[must_use]
31 pub fn mono() -> Self {
32 Self { weights: vec![1.0] }
33 }
34
35 #[must_use]
37 pub fn stereo() -> Self {
38 Self {
39 weights: vec![1.0, 1.0],
40 }
41 }
42
43 #[must_use]
48 pub fn surround_5_1() -> Self {
49 use core::f32::consts::SQRT_2;
50 Self {
51 weights: vec![1.0, 1.0, 1.0, 0.0, SQRT_2, SQRT_2],
52 }
53 }
54}
55
56const SILENCE_LUFS: f32 = -120.0;
58
59const ABS_GATE_LUFS: f32 = -70.0;
61
62const REL_GATE_OFFSET_LU: f32 = 10.0;
64
65const PRE_44100: BiquadCoeffs = BiquadCoeffs {
72 b0: 1.530_926_5_f32,
73 b1: -2.651_179_f32,
74 b2: 1.169_068_2_f32,
75 a1: -1.663_758_3_f32,
76 a2: 0.712_653_96_f32,
77};
78const RLB_44100: BiquadCoeffs = BiquadCoeffs {
79 b0: 1.0_f32,
80 b1: -2.0_f32,
81 b2: 1.0_f32,
82 a1: -1.988_378_9_f32,
83 a2: 0.988_520_47_f32,
84};
85const PRE_48000: BiquadCoeffs = BiquadCoeffs {
86 b0: 1.535_124_9_f32,
87 b1: -2.691_696_2_f32,
88 b2: 1.198_392_9_f32,
89 a1: -1.690_659_3_f32,
90 a2: 0.732_480_77_f32,
91};
92const RLB_48000: BiquadCoeffs = BiquadCoeffs {
93 b0: 1.0_f32,
94 b1: -2.0_f32,
95 b2: 1.0_f32,
96 a1: -1.990_047_5_f32,
97 a2: 0.990_072_25_f32,
98};
99
100pub struct LufsAnalyser {
122 sample_rate: f32,
123 pre: Biquad,
124 rlb: Biquad,
125}
126
127impl LufsAnalyser {
128 pub fn new(sample_rate: f32) -> Result<Self, AnalysisError> {
149 let (pre, rlb) = kweight_coeffs(sample_rate)?;
150 Ok(Self {
151 sample_rate,
152 pre: Biquad::new(pre),
153 rlb: Biquad::new(rlb),
154 })
155 }
156
157 pub fn integrated_loudness(&mut self, samples: &[f32]) -> Result<f32, AnalysisError> {
167 if samples.is_empty() {
168 return Err(AnalysisError::EmptyInput);
169 }
170
171 let block_samples = block_len(self.sample_rate);
172 let hop_samples = hop_len(self.sample_rate);
173
174 let kw_samples: Vec<f32> = samples.iter().map(|&x| self.k_weight(x)).collect();
175
176 let block_levels = block_mean_sq(&kw_samples, block_samples, hop_samples);
177 if block_levels.is_empty() {
178 return Err(AnalysisError::EmptyInput);
179 }
180
181 let abs_gate_ms = lufs_to_ms(ABS_GATE_LUFS);
183 let abs_gated: Vec<f32> = block_levels
184 .iter()
185 .copied()
186 .filter(|&z| z >= abs_gate_ms)
187 .collect();
188 if abs_gated.is_empty() {
189 return Err(AnalysisError::EmptyInput);
190 }
191
192 let rel_gate_lufs = ms_to_lufs(mean_f32(&abs_gated)) - REL_GATE_OFFSET_LU;
194 let rel_gate_ms = lufs_to_ms(rel_gate_lufs);
195 let rel_gated: Vec<f32> = block_levels
196 .iter()
197 .copied()
198 .filter(|&z| z >= rel_gate_ms)
199 .collect();
200 if rel_gated.is_empty() {
201 return Err(AnalysisError::EmptyInput);
202 }
203
204 Ok(ms_to_lufs(mean_f32(&rel_gated)))
205 }
206
207 #[must_use]
211 pub fn momentary(&mut self, frame_400ms: &[f32]) -> f32 {
212 ms_to_lufs(self.kw_mean_sq(frame_400ms))
213 }
214
215 #[must_use]
219 pub fn short_term(&mut self, frame_3s: &[f32]) -> f32 {
220 ms_to_lufs(self.kw_mean_sq(frame_3s))
221 }
222
223 pub fn true_peak_db(&self, samples: &[f32]) -> Result<f32, AnalysisError> {
235 if samples.is_empty() {
236 return Err(AnalysisError::EmptyInput);
237 }
238 let mut resampler =
239 PolyphaseResampler::new(4, 1).ok_or(AnalysisError::InvalidParameter {
240 name: "true_peak",
241 reason: "failed to construct 4× polyphase resampler",
242 })?;
243 let upsampled = resampler.process(samples);
244 let peak = upsampled.iter().map(|s| s.abs()).fold(0.0_f32, f32::max);
245 if peak <= 0.0 {
246 return Ok(SILENCE_LUFS);
247 }
248 Ok(20.0 * peak.log10())
249 }
250
251 pub fn reset(&mut self) {
256 let pre_coeffs = *self.pre.coeffs();
257 let rlb_coeffs = *self.rlb.coeffs();
258 self.pre = Biquad::new(pre_coeffs);
259 self.rlb = Biquad::new(rlb_coeffs);
260 }
261
262 pub fn integrated_loudness_multichannel(
276 &mut self,
277 interleaved: &[f32],
278 config: &ChannelConfig,
279 ) -> Result<f32, AnalysisError> {
280 let n_channels = config.weights.len();
281 if n_channels == 0 {
282 return Err(AnalysisError::InvalidParameter {
283 name: "config",
284 reason: "channel config has no weights",
285 });
286 }
287 if interleaved.is_empty() {
288 return Err(AnalysisError::EmptyInput);
289 }
290 if interleaved.len() % n_channels != 0 {
291 return Err(AnalysisError::InvalidParameter {
292 name: "interleaved",
293 reason: "sample count is not a multiple of channel count",
294 });
295 }
296
297 let n_frames = interleaved.len() / n_channels;
298 let block_samples = block_len(self.sample_rate);
299 let hop_samples = hop_len(self.sample_rate);
300
301 if n_frames < block_samples {
302 return Err(AnalysisError::EmptyInput);
303 }
304
305 let (pre_coeffs, rlb_coeffs) = kweight_coeffs(self.sample_rate)?;
306 let mut channel_filters: Vec<(Biquad, Biquad)> = (0..n_channels)
307 .map(|_| (Biquad::new(pre_coeffs), Biquad::new(rlb_coeffs)))
308 .collect();
309
310 let mut channel_kw_samples: Vec<Vec<f32>> = (0..n_channels)
312 .map(|_| Vec::with_capacity(n_frames))
313 .collect();
314 for frame in interleaved.chunks_exact(n_channels) {
315 for ((ch_kw_samples, (pre, rlb)), &sample) in channel_kw_samples
316 .iter_mut()
317 .zip(channel_filters.iter_mut())
318 .zip(frame.iter())
319 {
320 ch_kw_samples.push(rlb.process_sample(pre.process_sample(sample)));
321 }
322 }
323
324 let n_blocks = (n_frames - block_samples) / hop_samples + 1;
326 let block_levels: Vec<f32> = (0..n_blocks)
327 .map(|block_idx| {
328 let block_start = block_idx * hop_samples;
329 config
330 .weights
331 .iter()
332 .zip(channel_kw_samples.iter())
333 .map(|(&weight, ch_kw)| {
334 let block_slice = &ch_kw[block_start..block_start + block_samples];
335 let block_ms =
336 block_slice.iter().map(|&x| x * x).sum::<f32>() / block_samples as f32;
337 weight * block_ms
338 })
339 .sum()
340 })
341 .collect();
342
343 if block_levels.is_empty() {
344 return Err(AnalysisError::EmptyInput);
345 }
346
347 let abs_gate_ms = lufs_to_ms(ABS_GATE_LUFS);
349 let abs_gated: Vec<f32> = block_levels
350 .iter()
351 .copied()
352 .filter(|&z| z >= abs_gate_ms)
353 .collect();
354 if abs_gated.is_empty() {
355 return Err(AnalysisError::EmptyInput);
356 }
357
358 let rel_gate_lufs = ms_to_lufs(mean_f32(&abs_gated)) - REL_GATE_OFFSET_LU;
360 let rel_gate_ms = lufs_to_ms(rel_gate_lufs);
361 let rel_gated: Vec<f32> = block_levels
362 .iter()
363 .copied()
364 .filter(|&z| z >= rel_gate_ms)
365 .collect();
366 if rel_gated.is_empty() {
367 return Err(AnalysisError::EmptyInput);
368 }
369
370 Ok(ms_to_lufs(mean_f32(&rel_gated)))
371 }
372
373 #[inline]
374 fn k_weight(&mut self, x: f32) -> f32 {
375 self.rlb.process_sample(self.pre.process_sample(x))
376 }
377
378 fn kw_mean_sq(&mut self, samples: &[f32]) -> f32 {
379 if samples.is_empty() {
380 return 0.0;
381 }
382 let sum_sq: f32 = samples
383 .iter()
384 .map(|&x| {
385 let y = self.k_weight(x);
386 y * y
387 })
388 .sum();
389 sum_sq / samples.len() as f32
390 }
391}
392
393fn kweight_coeffs(sr: f32) -> Result<(BiquadCoeffs, BiquadCoeffs), AnalysisError> {
395 match sr.round() as u32 {
396 44100 => Ok((PRE_44100, RLB_44100)),
397 48000 => Ok((PRE_48000, RLB_48000)),
398 88200 | 96000 => {
399 let pre =
402 design::shelving_high(4.0, 1681.97, sr).ok_or(AnalysisError::InvalidParameter {
403 name: "sample_rate",
404 reason: "K-weighting pre-filter design failed",
405 })?;
406 let rlb = design::butterworth_highpass(38.135_f64, f64::from(sr)).ok_or(
407 AnalysisError::InvalidParameter {
408 name: "sample_rate",
409 reason: "K-weighting RLB high-pass design failed",
410 },
411 )?;
412 Ok((pre, rlb))
413 }
414 _ => Err(AnalysisError::InvalidParameter {
415 name: "sample_rate",
416 reason: "supported rates: 44100, 48000, 88200, 96000",
417 }),
418 }
419}
420
421fn block_len(sr: f32) -> usize {
423 (sr * 0.4).round() as usize
424}
425
426fn hop_len(sr: f32) -> usize {
428 (sr * 0.1).round() as usize
429}
430
431fn block_mean_sq(signal: &[f32], block: usize, hop: usize) -> Vec<f32> {
433 if block == 0 || hop == 0 || signal.len() < block {
434 return vec![];
435 }
436 let n_blocks = (signal.len() - block) / hop + 1;
437 (0..n_blocks)
438 .map(|i| {
439 let block_slice = &signal[i * hop..i * hop + block];
440 block_slice.iter().map(|&x| x * x).sum::<f32>() / block as f32
441 })
442 .collect()
443}
444
445fn ms_to_lufs(mean_sq: f32) -> f32 {
447 if mean_sq <= 0.0 {
448 SILENCE_LUFS
449 } else {
450 (-0.691 + 10.0 * mean_sq.log10()).max(SILENCE_LUFS)
451 }
452}
453
454fn lufs_to_ms(lufs: f32) -> f32 {
456 10f32.powf((lufs + 0.691) / 10.0)
457}
458
459fn mean_f32(v: &[f32]) -> f32 {
460 v.iter().sum::<f32>() / v.len() as f32
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use core::f32::consts::PI;
467
468 const SR: f32 = 44100.0;
469
470 fn sine(freq: f32, amplitude: f32, num_samples: usize, sr: f32) -> Vec<f32> {
471 (0..num_samples)
472 .map(|i| amplitude * (2.0 * PI * freq * i as f32 / sr).sin())
473 .collect()
474 }
475
476 fn make(sr: f32) -> LufsAnalyser {
478 match LufsAnalyser::new(sr) {
479 Ok(analyser) => analyser,
480 Err(e) => panic!("LufsAnalyser::new({sr}) failed: {e}"),
481 }
482 }
483
484 #[test]
485 fn new_44100_succeeds() {
486 assert!(LufsAnalyser::new(44100.0).is_ok());
487 }
488
489 #[test]
490 fn new_48000_succeeds() {
491 assert!(LufsAnalyser::new(48000.0).is_ok());
492 }
493
494 #[test]
495 fn new_88200_succeeds() {
496 assert!(LufsAnalyser::new(88200.0).is_ok());
497 }
498
499 #[test]
500 fn new_96000_succeeds() {
501 assert!(LufsAnalyser::new(96000.0).is_ok());
502 }
503
504 #[test]
505 fn unsupported_rate_returns_error() {
506 assert!(LufsAnalyser::new(22050.0).is_err());
507 assert!(LufsAnalyser::new(16000.0).is_err());
508 assert!(LufsAnalyser::new(0.0).is_err());
509 }
510
511 #[test]
512 fn empty_input_returns_error() {
513 let mut analyser = make(SR);
514 assert!(matches!(
515 analyser.integrated_loudness(&[]),
516 Err(AnalysisError::EmptyInput)
517 ));
518 }
519
520 #[test]
521 fn silence_gated_out() {
522 let mut analyser = make(SR);
523 let result = analyser.integrated_loudness(&vec![0.0_f32; 44100 * 5]);
524 assert!(matches!(result, Err(AnalysisError::EmptyInput)));
525 }
526
527 #[test]
528 fn too_short_for_one_block() {
529 let mut analyser = make(SR);
530 let result = analyser.integrated_loudness(&[0.1_f32; 100]);
531 assert!(matches!(result, Err(AnalysisError::EmptyInput)));
532 }
533
534 #[test]
538 fn integrated_lufs_calibration_tone() {
539 let num_samples = (SR * 5.0) as usize;
540 let sig = sine(1000.0, 0.10, num_samples, SR);
541 let mut analyser = make(SR);
542 match analyser.integrated_loudness(&sig) {
543 Ok(lufs) => assert!(
544 (lufs - (-23.0)).abs() < 0.2,
545 "expected ≈ −23 LUFS, got {lufs:.3}"
546 ),
547 Err(e) => panic!("unexpected error: {e}"),
548 }
549 }
550
551 #[test]
552 fn integrated_lufs_6lu_per_6db() {
553 let num_samples = (SR * 5.0) as usize;
554 let signal_quiet = sine(1000.0, 0.10, num_samples, SR);
555 let signal_loud = sine(1000.0, 0.20, num_samples, SR);
556 let mut analyser = make(SR);
557 let lufs_quiet = match analyser.integrated_loudness(&signal_quiet) {
558 Ok(lufs) => lufs,
559 Err(e) => panic!("quiet signal error: {e}"),
560 };
561 analyser.reset();
562 let lufs_loud = match analyser.integrated_loudness(&signal_loud) {
563 Ok(lufs) => lufs,
564 Err(e) => panic!("loud signal error: {e}"),
565 };
566 assert!(
567 (lufs_loud - lufs_quiet - 6.02).abs() < 0.05,
568 "doubling amplitude should give +6.02 LU, got {:.3}",
569 lufs_loud - lufs_quiet
570 );
571 }
572
573 #[test]
574 fn true_peak_full_scale_sine_near_zero_dbtp() {
575 let num_samples = SR as usize;
576 let sig: Vec<f32> = (0..num_samples)
577 .map(|i| (2.0 * PI * 997.0 * i as f32 / SR).sin())
578 .collect();
579 let analyser = make(SR);
580 match analyser.true_peak_db(&sig) {
581 Ok(true_peak) => assert!(
582 true_peak.abs() < 0.5,
583 "full-scale 997 Hz sine ≈ 0 dBTP, got {true_peak:.3}"
584 ),
585 Err(e) => panic!("unexpected error: {e}"),
586 }
587 }
588
589 #[test]
590 fn true_peak_empty_returns_error() {
591 let analyser = make(SR);
592 assert!(matches!(
593 analyser.true_peak_db(&[]),
594 Err(AnalysisError::EmptyInput)
595 ));
596 }
597
598 #[test]
599 fn momentary_reasonable_for_sine() {
600 let block_samples = block_len(SR);
601 let sig = sine(1000.0, 0.10, block_samples, SR);
602 let momentary_lufs = {
603 let mut analyser = make(SR);
604 analyser.momentary(&sig)
605 };
606 assert!(
607 momentary_lufs > -35.0 && momentary_lufs < -15.0,
608 "momentary LUFS out of range: {momentary_lufs:.2}"
609 );
610 }
611
612 #[test]
613 fn short_term_reasonable_for_sine() {
614 let num_samples = (SR * 3.0) as usize;
615 let sig = sine(1000.0, 0.10, num_samples, SR);
616 let short_term_lufs = {
617 let mut analyser = make(SR);
618 analyser.short_term(&sig)
619 };
620 assert!(
621 short_term_lufs > -35.0 && short_term_lufs < -15.0,
622 "short-term LUFS out of range: {short_term_lufs:.2}"
623 );
624 }
625
626 #[test]
627 fn reset_clears_filter_state() {
628 let block_samples = block_len(SR);
629 let pre_warm = sine(100.0, 1.0, block_samples, SR);
630 let test_sig = sine(1000.0, 0.10, block_samples, SR);
631
632 let lufs_after_reset = {
633 let mut analyser = make(SR);
634 let _ = analyser.momentary(&pre_warm);
635 analyser.reset();
636 analyser.momentary(&test_sig)
637 };
638 let lufs_fresh = {
639 let mut analyser = make(SR);
640 analyser.momentary(&test_sig)
641 };
642 assert!(
643 (lufs_after_reset - lufs_fresh).abs() < 1e-5,
644 "reset should give same result as fresh: {lufs_after_reset:.6} vs {lufs_fresh:.6}"
645 );
646 }
647
648 #[test]
649 fn block_mean_sq_constant_signal() {
650 let sig = vec![0.5_f32; 100];
651 let block_levels = block_mean_sq(&sig, 50, 25);
652 assert_eq!(block_levels.len(), 3);
653 for level in &block_levels {
654 assert!((level - 0.25).abs() < 1e-6, "expected 0.25, got {level}");
655 }
656 }
657
658 #[test]
659 fn block_mean_sq_too_short() {
660 assert!(block_mean_sq(&[1.0_f32; 10], 50, 25).is_empty());
661 }
662
663 #[test]
664 fn ms_to_lufs_known_value() {
665 let lufs = ms_to_lufs(0.5);
667 assert!((lufs - (-3.701)).abs() < 0.001, "got {lufs:.4}");
668 }
669
670 #[test]
671 fn ms_to_lufs_silence() {
672 assert_eq!(ms_to_lufs(0.0), SILENCE_LUFS);
673 assert_eq!(ms_to_lufs(-1.0), SILENCE_LUFS);
674 }
675
676 #[test]
677 fn lufs_to_ms_roundtrip() {
678 let lufs = -23.0_f32;
679 let mean_sq = lufs_to_ms(lufs);
680 let roundtripped_lufs = ms_to_lufs(mean_sq);
681 assert!(
682 (roundtripped_lufs - lufs).abs() < 0.001,
683 "roundtrip: {lufs} → {roundtripped_lufs:.4}"
684 );
685 }
686
687 #[test]
690 fn multichannel_empty_input_returns_error() {
691 let mut analyser = make(SR);
692 assert!(matches!(
693 analyser.integrated_loudness_multichannel(&[], &ChannelConfig::stereo()),
694 Err(AnalysisError::EmptyInput)
695 ));
696 }
697
698 #[test]
699 fn multichannel_empty_config_returns_error() {
700 let mut analyser = make(SR);
701 let config = ChannelConfig { weights: vec![] };
702 assert!(matches!(
703 analyser.integrated_loudness_multichannel(&[0.1_f32; 100], &config),
704 Err(AnalysisError::InvalidParameter { .. })
705 ));
706 }
707
708 #[test]
709 fn multichannel_mismatched_samples_returns_error() {
710 let mut analyser = make(SR);
711 assert!(matches!(
713 analyser.integrated_loudness_multichannel(&[0.1_f32; 3], &ChannelConfig::stereo()),
714 Err(AnalysisError::InvalidParameter { .. })
715 ));
716 }
717
718 #[test]
719 fn multichannel_mono_matches_integrated_loudness() {
720 let num_samples = (SR * 5.0) as usize;
723 let sig = sine(1000.0, 0.10, num_samples, SR);
724 let mut analyser_mono = make(SR);
725 let mut analyser_mc = make(SR);
726 let lufs_mono = analyser_mono
727 .integrated_loudness(&sig)
728 .unwrap_or_else(|e| panic!("integrated_loudness error: {e}"));
729 let lufs_mc = analyser_mc
730 .integrated_loudness_multichannel(&sig, &ChannelConfig::mono())
731 .unwrap_or_else(|e| panic!("multichannel mono error: {e}"));
732 assert!(
733 (lufs_mono - lufs_mc).abs() < 0.01,
734 "mono config should match integrated_loudness: {lufs_mono:.3} vs {lufs_mc:.3}"
735 );
736 }
737
738 #[test]
739 fn stereo_identical_channels_is_three_lu_above_mono() {
740 let num_samples = (SR * 5.0) as usize;
743 let mono = sine(1000.0, 0.10, num_samples, SR);
744 let stereo: Vec<f32> = mono.iter().flat_map(|&s| [s, s]).collect();
745
746 let mut analyser_mono = make(SR);
747 let mut analyser_stereo = make(SR);
748
749 let lufs_mono = analyser_mono
750 .integrated_loudness(&mono)
751 .unwrap_or_else(|e| panic!("mono error: {e}"));
752 let lufs_stereo = analyser_stereo
753 .integrated_loudness_multichannel(&stereo, &ChannelConfig::stereo())
754 .unwrap_or_else(|e| panic!("stereo error: {e}"));
755
756 let diff = lufs_stereo - lufs_mono;
757 assert!(
758 (diff - 3.01).abs() < 0.1,
759 "identical stereo should be +3.01 LU above mono, got {diff:.3} LU"
760 );
761 }
762
763 #[test]
764 fn surround_5_1_lfe_only_is_gated_out() {
765 let num_samples = (SR * 5.0) as usize;
768 let lfe_signal = sine(60.0, 0.5, num_samples, SR);
769 let interleaved: Vec<f32> = (0..num_samples)
771 .flat_map(|i| [0.0, 0.0, 0.0, lfe_signal[i], 0.0, 0.0])
772 .collect();
773
774 let mut analyser = make(SR);
775 assert!(
776 matches!(
777 analyser
778 .integrated_loudness_multichannel(&interleaved, &ChannelConfig::surround_5_1()),
779 Err(AnalysisError::EmptyInput)
780 ),
781 "LFE-only 5.1 should be gated out"
782 );
783 }
784
785 #[test]
786 fn stereo_silence_gated_out() {
787 let num_samples = (SR * 5.0) as usize;
788 let stereo = vec![0.0_f32; num_samples * 2];
789 let mut analyser = make(SR);
790 assert!(matches!(
791 analyser.integrated_loudness_multichannel(&stereo, &ChannelConfig::stereo()),
792 Err(AnalysisError::EmptyInput)
793 ));
794 }
795
796 #[test]
797 fn stereo_too_short_for_one_block_returns_error() {
798 let mut analyser = make(SR);
799 let stereo = vec![0.1_f32; 200]; assert!(matches!(
801 analyser.integrated_loudness_multichannel(&stereo, &ChannelConfig::stereo()),
802 Err(AnalysisError::EmptyInput)
803 ));
804 }
805}