1use crate::error::AnalysisError;
27
28#[derive(Debug, Clone, Copy)]
30pub enum NormalizationMethod {
31 Peak,
33 RMS,
35 Loudness,
37}
38
39#[derive(Debug, Clone)]
41pub struct NormalizationConfig {
42 pub target_loudness_lufs: f32,
44
45 pub max_headroom_db: f32,
47
48 pub method: NormalizationMethod,
50}
51
52impl Default for NormalizationConfig {
53 fn default() -> Self {
54 Self {
55 target_loudness_lufs: -14.0,
56 max_headroom_db: 1.0,
57 method: NormalizationMethod::Peak,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct LoudnessMetadata {
65 pub measured_lufs: Option<f32>,
67 pub peak_db: f32,
69 pub rms_db: f32,
71 pub gain_db: f32,
73}
74
75impl Default for LoudnessMetadata {
76 fn default() -> Self {
77 Self {
78 measured_lufs: None,
79 peak_db: f32::NEG_INFINITY,
80 rms_db: f32::NEG_INFINITY,
81 gain_db: 0.0,
82 }
83 }
84}
85
86const EPSILON: f32 = 1e-10;
88
89const LUFS_GATE_THRESHOLD: f32 = -70.0;
91
92const LUFS_BLOCK_DURATION_MS: f32 = 400.0;
95
96struct KWeightingFilter {
113 x1: f32,
116 x2: f32,
117 b0: f32,
119 b1: f32,
120 b2: f32,
121 a1: f32,
122 a2: f32,
123}
124
125impl KWeightingFilter {
126 fn new(sample_rate: f32) -> Self {
132 let w0 = 2.0 * std::f32::consts::PI * 1681.974450955533 / sample_rate;
138 let cos_w0 = w0.cos();
139 let sin_w0 = w0.sin();
140 let alpha = sin_w0 / 2.0 * (1.0f32 / 0.707f32).sqrt(); let b0 = (1.0 + cos_w0) / 2.0;
143 let b1 = -(1.0 + cos_w0);
144 let b2 = (1.0 + cos_w0) / 2.0;
145 let a0 = 1.0 + alpha;
146 let a1 = -2.0 * cos_w0;
147 let a2 = 1.0 - alpha;
148
149 Self {
150 x1: 0.0,
151 x2: 0.0,
152 b0: b0 / a0,
153 b1: b1 / a0,
154 b2: b2 / a0,
155 a1: a1 / a0,
156 a2: a2 / a0,
157 }
158 }
159
160 fn process(&mut self, sample: f32) -> f32 {
162 let output = self.b0 * sample + self.x1;
164 self.x1 = self.b1 * sample + self.x2 - self.a1 * output;
165 self.x2 = self.b2 * sample - self.a2 * output;
166 output
167 }
168
169 #[allow(dead_code)] fn reset(&mut self) {
172 self.x1 = 0.0;
173 self.x2 = 0.0;
174 }
175}
176
177fn calculate_lufs(samples: &[f32], sample_rate: f32) -> Result<f32, AnalysisError> {
186 if samples.is_empty() {
187 return Err(AnalysisError::InvalidInput("Empty audio samples".to_string()));
188 }
189
190 if sample_rate <= 0.0 {
191 return Err(AnalysisError::InvalidInput("Invalid sample rate".to_string()));
192 }
193
194 let block_size = (sample_rate * LUFS_BLOCK_DURATION_MS / 1000.0) as usize;
195 if block_size == 0 {
196 return Err(AnalysisError::InvalidInput("Sample rate too low for LUFS calculation".to_string()));
197 }
198
199 let mut filter = KWeightingFilter::new(sample_rate);
201 let filtered: Vec<f32> = samples.iter().map(|&s| filter.process(s)).collect();
202
203 let num_blocks = (filtered.len() + block_size - 1) / block_size;
205 let mut block_energies = Vec::with_capacity(num_blocks);
206
207 for i in 0..num_blocks {
208 let start = i * block_size;
209 let end = (start + block_size).min(filtered.len());
210
211 let sum_sq: f32 = filtered[start..end].iter().map(|&x| x * x).sum();
213 let mean_sq = sum_sq / (end - start) as f32;
214 block_energies.push(mean_sq);
215 }
216
217 if block_energies.is_empty() {
218 return Err(AnalysisError::ProcessingError("No blocks computed for LUFS".to_string()));
219 }
220
221 let gate_threshold_linear = 10.0_f32.powf((LUFS_GATE_THRESHOLD + 0.691) / 10.0);
225
226 let mut gated_energies = Vec::new();
227 for &energy in &block_energies {
228 if energy > gate_threshold_linear {
229 gated_energies.push(energy);
230 }
231 }
232
233 if gated_energies.is_empty() {
235 log::warn!("All audio blocks below LUFS gate threshold (-70 LUFS)");
236 return Ok(f32::NEG_INFINITY);
237 }
238
239 let mean_gated: f32 = gated_energies.iter().sum::<f32>() / gated_energies.len() as f32;
241
242 if mean_gated <= EPSILON {
244 return Err(AnalysisError::NumericalError("Mean square too small for LUFS calculation".to_string()));
245 }
246
247 let lufs = -0.691 + 10.0 * mean_gated.log10();
248 Ok(lufs)
249}
250
251fn normalize_peak(samples: &mut [f32], max_headroom_db: f32) -> Result<LoudnessMetadata, AnalysisError> {
253 if samples.is_empty() {
254 return Err(AnalysisError::InvalidInput("Empty audio samples".to_string()));
255 }
256
257 let peak = samples.iter()
259 .map(|&x| x.abs())
260 .fold(0.0f32, f32::max);
261
262 if peak <= EPSILON {
263 log::warn!("Audio is silent or extremely quiet, cannot normalize");
264 return Ok(LoudnessMetadata {
265 measured_lufs: None,
266 peak_db: f32::NEG_INFINITY,
267 rms_db: f32::NEG_INFINITY,
268 gain_db: 0.0,
269 });
270 }
271
272 let peak_db = 20.0 * peak.log10();
274
275 let target_peak_linear = 10.0_f32.powf((0.0 - max_headroom_db) / 20.0);
278 let gain_linear = target_peak_linear / peak;
279 let gain_db = 20.0 * gain_linear.log10();
280
281 let gain_linear = gain_linear.min(1.0 / peak);
283
284 for sample in samples.iter_mut() {
286 *sample *= gain_linear;
287 }
288
289 let rms = (samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt();
291 let rms_db = if rms > EPSILON {
292 20.0 * rms.log10()
293 } else {
294 f32::NEG_INFINITY
295 };
296
297 log::debug!("Peak normalization: peak={:.2} dB, gain={:.2} dB", peak_db, gain_db);
298
299 Ok(LoudnessMetadata {
300 measured_lufs: None,
301 peak_db,
302 rms_db,
303 gain_db,
304 })
305}
306
307fn normalize_rms(samples: &mut [f32], target_rms_db: f32, max_headroom_db: f32) -> Result<LoudnessMetadata, AnalysisError> {
309 if samples.is_empty() {
310 return Err(AnalysisError::InvalidInput("Empty audio samples".to_string()));
311 }
312
313 let rms_sq = samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32;
315 let rms = rms_sq.sqrt();
316
317 if rms <= EPSILON {
318 log::warn!("Audio is silent or extremely quiet, cannot normalize");
319 return Ok(LoudnessMetadata {
320 measured_lufs: None,
321 peak_db: f32::NEG_INFINITY,
322 rms_db: f32::NEG_INFINITY,
323 gain_db: 0.0,
324 });
325 }
326
327 let rms_db = 20.0 * rms.log10();
328
329 let peak = samples.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
331 let peak_db = 20.0 * peak.log10();
332
333 let target_rms_linear = 10.0_f32.powf((target_rms_db - max_headroom_db) / 20.0);
335 let gain_linear = target_rms_linear / rms;
336 let gain_db = 20.0 * gain_linear.log10();
337
338 let new_peak = peak * gain_linear;
340 if new_peak > 1.0 {
341 log::warn!("RMS normalization would cause clipping, limiting gain");
342 let max_gain_linear = 1.0 / peak;
343 let max_gain_db = 20.0 * max_gain_linear.log10();
344
345 for sample in samples.iter_mut() {
347 *sample *= max_gain_linear;
348 }
349
350 return Ok(LoudnessMetadata {
351 measured_lufs: None,
352 peak_db,
353 rms_db,
354 gain_db: max_gain_db,
355 });
356 }
357
358 for sample in samples.iter_mut() {
360 *sample *= gain_linear;
361 }
362
363 log::debug!("RMS normalization: rms={:.2} dB, gain={:.2} dB", rms_db, gain_db);
364
365 Ok(LoudnessMetadata {
366 measured_lufs: None,
367 peak_db,
368 rms_db,
369 gain_db,
370 })
371}
372
373fn normalize_lufs(
375 samples: &mut [f32],
376 target_lufs: f32,
377 max_headroom_db: f32,
378 sample_rate: f32,
379) -> Result<LoudnessMetadata, AnalysisError> {
380 if samples.is_empty() {
381 return Err(AnalysisError::InvalidInput("Empty audio samples".to_string()));
382 }
383
384 let measured_lufs = calculate_lufs(samples, sample_rate)?;
386
387 if measured_lufs == f32::NEG_INFINITY {
388 log::warn!("Audio is too quiet for LUFS measurement, using peak normalization fallback");
389 return normalize_peak(samples, max_headroom_db);
390 }
391
392 let lufs_diff = target_lufs - measured_lufs;
394 let gain_db = lufs_diff;
395 let gain_linear = 10.0_f32.powf(gain_db / 20.0);
396
397 let peak = samples.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
399 let peak_db = 20.0 * peak.log10();
400
401 let new_peak = peak * gain_linear;
402 let target_peak_linear = 10.0_f32.powf((0.0 - max_headroom_db) / 20.0);
403 if new_peak > target_peak_linear {
404 log::warn!("LUFS normalization would cause clipping, limiting gain to preserve headroom");
405 let max_gain_linear = target_peak_linear / peak;
406 let max_gain_db = 20.0 * max_gain_linear.log10();
407
408 for sample in samples.iter_mut() {
410 *sample *= max_gain_linear;
411 }
412
413 let rms = (samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt();
415 let rms_db = if rms > EPSILON {
416 20.0 * rms.log10()
417 } else {
418 f32::NEG_INFINITY
419 };
420
421 return Ok(LoudnessMetadata {
422 measured_lufs: Some(measured_lufs),
423 peak_db,
424 rms_db,
425 gain_db: max_gain_db,
426 });
427 }
428
429 for sample in samples.iter_mut() {
431 *sample *= gain_linear;
432 }
433
434 let rms = (samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt();
436 let rms_db = if rms > EPSILON {
437 20.0 * rms.log10()
438 } else {
439 f32::NEG_INFINITY
440 };
441
442 log::debug!("LUFS normalization: measured={:.2} LUFS, target={:.2} LUFS, gain={:.2} dB",
443 measured_lufs, target_lufs, gain_db);
444
445 Ok(LoudnessMetadata {
446 measured_lufs: Some(measured_lufs),
447 peak_db,
448 rms_db,
449 gain_db,
450 })
451}
452
453pub fn normalize(
488 samples: &mut [f32],
489 config: NormalizationConfig,
490 sample_rate: f32,
491) -> Result<LoudnessMetadata, AnalysisError> {
492 log::debug!("Normalizing {} samples using {:?} at {} Hz",
493 samples.len(), config.method, sample_rate);
494
495 match config.method {
496 NormalizationMethod::Peak => {
497 normalize_peak(samples, config.max_headroom_db)
498 }
499 NormalizationMethod::RMS => {
500 let target_rms_db = config.target_loudness_lufs + 3.0;
503 normalize_rms(samples, target_rms_db, config.max_headroom_db)
504 }
505 NormalizationMethod::Loudness => {
506 normalize_lufs(samples, config.target_loudness_lufs, config.max_headroom_db, sample_rate)
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 fn generate_test_signal(length: usize, amplitude: f32, sample_rate: f32) -> Vec<f32> {
517 let freq = 440.0;
518 (0..length)
519 .map(|i| {
520 let t = i as f32 / sample_rate;
521 amplitude * (2.0 * std::f32::consts::PI * freq * t).sin()
522 })
523 .collect()
524 }
525
526 #[test]
527 fn test_peak_normalization() {
528 let mut samples = generate_test_signal(44100, 0.5, 44100.0);
529
530 let config = NormalizationConfig {
531 method: NormalizationMethod::Peak,
532 target_loudness_lufs: -14.0,
533 max_headroom_db: 1.0,
534 };
535
536 let _metadata = normalize(&mut samples, config, 44100.0).unwrap();
537
538 let new_peak = samples.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
540 let target_peak = 10.0_f32.powf((0.0 - 1.0) / 20.0); assert!((new_peak - target_peak).abs() < 0.01,
543 "Peak normalization failed: expected ~{:.3}, got {:.3}", target_peak, new_peak);
544 assert!(new_peak <= 1.0, "Peak normalization caused clipping: peak = {:.3}", new_peak);
545 }
546
547 #[test]
548 fn test_rms_normalization() {
549 let mut samples = generate_test_signal(44100, 0.3, 44100.0);
550
551 let config = NormalizationConfig {
552 method: NormalizationMethod::RMS,
553 target_loudness_lufs: -14.0,
554 max_headroom_db: 1.0,
555 };
556
557 let _metadata = normalize(&mut samples, config, 44100.0).unwrap();
558
559 let rms = (samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt();
561 let target_rms_db = -14.0 + 3.0; let target_rms = 10.0_f32.powf((target_rms_db - 1.0) / 20.0); assert!((rms - target_rms).abs() < 0.1,
565 "RMS normalization failed: expected ~{:.3}, got {:.3}", target_rms, rms);
566
567 let peak = samples.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
569 assert!(peak <= 1.0, "RMS normalization caused clipping");
570 }
571
572 #[test]
573 fn test_lufs_calculation() {
574 let samples = generate_test_signal(48000 * 2, 0.8, 48000.0); let lufs = calculate_lufs(&samples, 48000.0).unwrap();
578
579 assert!(lufs.is_finite(), "LUFS should be finite: {:.2} LUFS", lufs);
583 assert!(lufs < 0.0, "LUFS should be negative for normalized audio: {:.2} LUFS", lufs);
584 }
585
586 #[test]
587 fn test_lufs_normalization() {
588 let mut samples = generate_test_signal(48000 * 2, 0.5, 48000.0); let config = NormalizationConfig {
592 method: NormalizationMethod::Loudness,
593 target_loudness_lufs: -14.0,
594 max_headroom_db: 1.0,
595 };
596
597 let metadata = normalize(&mut samples, config, 48000.0).unwrap();
598
599 assert!(metadata.measured_lufs.is_some(), "LUFS normalization should return measured LUFS");
601 assert!(metadata.gain_db != 0.0, "Gain should be applied");
602
603 let peak = samples.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
605 assert!(peak <= 1.0, "LUFS normalization caused clipping: peak = {:.3}", peak);
606 }
607
608 #[test]
609 fn test_silent_audio() {
610 let mut samples = vec![0.0f32; 44100];
611
612 let config = NormalizationConfig {
613 method: NormalizationMethod::Peak,
614 target_loudness_lufs: -14.0,
615 max_headroom_db: 1.0,
616 };
617
618 let metadata = normalize(&mut samples, config, 44100.0).unwrap();
620 assert_eq!(metadata.gain_db, 0.0, "Silent audio should not apply gain");
621 assert_eq!(metadata.peak_db, f32::NEG_INFINITY);
622 }
623
624 #[test]
625 fn test_ultra_quiet_audio() {
626 let mut samples = generate_test_signal(44100, 1e-6, 44100.0);
628
629 let config = NormalizationConfig {
630 method: NormalizationMethod::Peak,
631 target_loudness_lufs: -14.0,
632 max_headroom_db: 1.0,
633 };
634
635 let _metadata = normalize(&mut samples, config, 44100.0).unwrap();
637 }
639
640 #[test]
641 fn test_empty_samples() {
642 let mut samples = vec![];
643
644 let config = NormalizationConfig::default();
645
646 let result = normalize(&mut samples, config, 44100.0);
647 assert!(result.is_err(), "Empty samples should return error");
648 }
649
650 #[test]
651 fn test_k_weighting_filter() {
652 let sample_rate = 48000.0;
653 let mut filter = KWeightingFilter::new(sample_rate);
654
655 let output = filter.process(1.0);
657 assert!(!output.is_nan() && !output.is_infinite(),
658 "Filter output should be finite");
659
660 filter.reset();
662 let test_signal = generate_test_signal(1000, 0.5, sample_rate);
663 let filtered: Vec<f32> = test_signal.iter().map(|&s| filter.process(s)).collect();
664
665 assert_ne!(filtered, test_signal, "K-weighting filter should modify signal");
667
668 for &x in &filtered {
670 assert!(x.is_finite(), "Filter output should be finite");
671 }
672 }
673}
674