//! Normalize operations: peak normalize and LUFS normalize. use crate::analysis::basic::peak_db; use crate::error::CoreError; /// Peak-normalize samples to the target dBFS level. /// /// Computes current peak, calculates gain needed to reach `target_db`, /// and scales all samples. No clipping protection — the caller should /// ensure `target_db <= 0.0`. pub fn apply_normalize_peak(samples: &mut [f32], target_db: f64) -> Result<(), CoreError> { if samples.is_empty() { return Ok(()); } let current_peak = peak_db(samples); if current_peak <= -96.0 { // Silent — nothing to normalize return Ok(()); } let gain_db = target_db - current_peak; let scale = 10.0_f64.powf(gain_db / 20.0) as f32; for sample in samples.iter_mut() { *sample *= scale; } Ok(()) } /// LUFS-normalize samples to the target loudness level. /// /// Uses a simplified integrated loudness measurement (mean square of /// all samples, which approximates ITU-R BS.1770 for short samples). pub fn apply_normalize_lufs( samples: &mut [f32], _channels: u16, _sample_rate: u32, target_lufs: f64, ) -> Result<(), CoreError> { if samples.is_empty() { return Ok(()); } // Compute integrated loudness (simplified: RMS-based approximation) let sum_sq: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum(); let mean_sq = sum_sq / samples.len() as f64; if mean_sq <= 1e-20 { // Silent — nothing to normalize return Ok(()); } // LUFS ≈ -0.691 + 10 * log10(mean_square) for simple signals // We use this approximation for short samples let current_lufs = -0.691 + 10.0 * mean_sq.log10(); let gain_db = target_lufs - current_lufs; let scale = 10.0_f64.powf(gain_db / 20.0) as f32; for sample in samples.iter_mut() { *sample = (*sample * scale).clamp(-1.0, 1.0); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn normalize_peak_to_zero() { let mut samples = vec![0.5, -0.5, 0.25, -0.25]; apply_normalize_peak(&mut samples, 0.0).unwrap(); // Peak should now be ~1.0 (0 dBFS) let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs())); assert!((peak - 1.0).abs() < 0.001, "peak should be ~1.0, got {peak}"); } #[test] fn normalize_peak_to_minus_6() { let mut samples = vec![1.0, -1.0, 0.5]; apply_normalize_peak(&mut samples, -6.0).unwrap(); // Peak should be ~0.5012 (-6 dBFS) let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs())); let expected = 10.0_f32.powf(-6.0 / 20.0); assert!( (peak - expected).abs() < 0.01, "peak should be ~{expected}, got {peak}" ); } #[test] fn normalize_peak_silence() { let mut samples = vec![0.0, 0.0, 0.0]; apply_normalize_peak(&mut samples, 0.0).unwrap(); assert_eq!(samples, vec![0.0, 0.0, 0.0]); } #[test] fn normalize_peak_empty() { let mut samples: Vec = vec![]; apply_normalize_peak(&mut samples, 0.0).unwrap(); assert!(samples.is_empty()); } #[test] fn normalize_lufs_adjusts_level() { let mut samples: Vec = (0..44100) .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin()) .collect(); let original_rms: f64 = samples.iter().map(|&s| (s as f64).powi(2)).sum::() / samples.len() as f64; let original_rms = original_rms.sqrt(); apply_normalize_lufs(&mut samples, 1, 44100, -14.0).unwrap(); let new_rms: f64 = samples.iter().map(|&s| (s as f64).powi(2)).sum::() / samples.len() as f64; let new_rms = new_rms.sqrt(); // RMS should have changed (we don't test exact LUFS but verify gain was applied) assert!( (new_rms - original_rms).abs() > 0.001, "RMS should have changed" ); } #[test] fn normalize_lufs_silence() { let mut samples = vec![0.0; 1000]; apply_normalize_lufs(&mut samples, 1, 44100, -14.0).unwrap(); assert!(samples.iter().all(|&s| s == 0.0)); } }