Skip to main content

max / audiofiles

4.2 KB · 136 lines History Blame Raw
1 //! Normalize operations: peak normalize and LUFS normalize.
2
3 use crate::analysis::basic::peak_db;
4 use crate::error::CoreError;
5
6 /// Peak-normalize samples to the target dBFS level.
7 ///
8 /// Computes current peak, calculates gain needed to reach `target_db`,
9 /// and scales all samples. No clipping protection — the caller should
10 /// ensure `target_db <= 0.0`.
11 pub fn apply_normalize_peak(samples: &mut [f32], target_db: f64) -> Result<(), CoreError> {
12 if samples.is_empty() {
13 return Ok(());
14 }
15
16 let current_peak = peak_db(samples);
17 if current_peak <= -96.0 {
18 // Silent — nothing to normalize
19 return Ok(());
20 }
21
22 let gain_db = target_db - current_peak;
23 let scale = 10.0_f64.powf(gain_db / 20.0) as f32;
24
25 for sample in samples.iter_mut() {
26 *sample *= scale;
27 }
28
29 Ok(())
30 }
31
32 /// LUFS-normalize samples to the target loudness level.
33 ///
34 /// Uses a simplified integrated loudness measurement (mean square of
35 /// all samples, which approximates ITU-R BS.1770 for short samples).
36 pub fn apply_normalize_lufs(
37 samples: &mut [f32],
38 _channels: u16,
39 _sample_rate: u32,
40 target_lufs: f64,
41 ) -> Result<(), CoreError> {
42 if samples.is_empty() {
43 return Ok(());
44 }
45
46 // Compute integrated loudness (simplified: RMS-based approximation)
47 let sum_sq: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
48 let mean_sq = sum_sq / samples.len() as f64;
49 if mean_sq <= 1e-20 {
50 // Silent — nothing to normalize
51 return Ok(());
52 }
53
54 // LUFS ≈ -0.691 + 10 * log10(mean_square) for simple signals
55 // We use this approximation for short samples
56 let current_lufs = -0.691 + 10.0 * mean_sq.log10();
57 let gain_db = target_lufs - current_lufs;
58 let scale = 10.0_f64.powf(gain_db / 20.0) as f32;
59
60 for sample in samples.iter_mut() {
61 *sample = (*sample * scale).clamp(-1.0, 1.0);
62 }
63
64 Ok(())
65 }
66
67 #[cfg(test)]
68 mod tests {
69 use super::*;
70
71 #[test]
72 fn normalize_peak_to_zero() {
73 let mut samples = vec![0.5, -0.5, 0.25, -0.25];
74 apply_normalize_peak(&mut samples, 0.0).unwrap();
75 // Peak should now be ~1.0 (0 dBFS)
76 let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs()));
77 assert!((peak - 1.0).abs() < 0.001, "peak should be ~1.0, got {peak}");
78 }
79
80 #[test]
81 fn normalize_peak_to_minus_6() {
82 let mut samples = vec![1.0, -1.0, 0.5];
83 apply_normalize_peak(&mut samples, -6.0).unwrap();
84 // Peak should be ~0.5012 (-6 dBFS)
85 let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs()));
86 let expected = 10.0_f32.powf(-6.0 / 20.0);
87 assert!(
88 (peak - expected).abs() < 0.01,
89 "peak should be ~{expected}, got {peak}"
90 );
91 }
92
93 #[test]
94 fn normalize_peak_silence() {
95 let mut samples = vec![0.0, 0.0, 0.0];
96 apply_normalize_peak(&mut samples, 0.0).unwrap();
97 assert_eq!(samples, vec![0.0, 0.0, 0.0]);
98 }
99
100 #[test]
101 fn normalize_peak_empty() {
102 let mut samples: Vec<f32> = vec![];
103 apply_normalize_peak(&mut samples, 0.0).unwrap();
104 assert!(samples.is_empty());
105 }
106
107 #[test]
108 fn normalize_lufs_adjusts_level() {
109 let mut samples: Vec<f32> = (0..44100)
110 .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
111 .collect();
112 let original_rms: f64 = samples.iter().map(|&s| (s as f64).powi(2)).sum::<f64>()
113 / samples.len() as f64;
114 let original_rms = original_rms.sqrt();
115
116 apply_normalize_lufs(&mut samples, 1, 44100, -14.0).unwrap();
117
118 let new_rms: f64 = samples.iter().map(|&s| (s as f64).powi(2)).sum::<f64>()
119 / samples.len() as f64;
120 let new_rms = new_rms.sqrt();
121
122 // RMS should have changed (we don't test exact LUFS but verify gain was applied)
123 assert!(
124 (new_rms - original_rms).abs() > 0.001,
125 "RMS should have changed"
126 );
127 }
128
129 #[test]
130 fn normalize_lufs_silence() {
131 let mut samples = vec![0.0; 1000];
132 apply_normalize_lufs(&mut samples, 1, 44100, -14.0).unwrap();
133 assert!(samples.iter().all(|&s| s == 0.0));
134 }
135 }
136