use crate::audio::samples::AudioBuffer;
pub enum NormalizationTarget {
Peak(f32),
Rms(f32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NormalizationError {
NonFiniteTarget,
TargetOutOfRange,
}
impl std::fmt::Display for NormalizationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NormalizationError::NonFiniteTarget => {
write!(f, "normalization target must be a finite level in 0.0..=1.0")
}
NormalizationError::TargetOutOfRange => {
write!(f, "normalization target must be in 0.0..=1.0")
}
}
}
}
impl std::error::Error for NormalizationError {}
pub fn normalize(
buffer: &mut AudioBuffer,
target: NormalizationTarget,
) -> Result<(), NormalizationError> {
let level = match target {
NormalizationTarget::Peak(level) | NormalizationTarget::Rms(level) => level,
};
if !level.is_finite() {
return Err(NormalizationError::NonFiniteTarget);
}
if !(0.0..=1.0).contains(&level) {
return Err(NormalizationError::TargetOutOfRange);
}
if buffer.samples.is_empty() {
return Ok(());
}
let gain = match target {
NormalizationTarget::Peak(_) => {
let current_peak = compute_peak(&buffer.samples);
if current_peak > 0.0 {
level / current_peak
} else {
1.0
}
}
NormalizationTarget::Rms(_) => {
let rms = compute_rms(&buffer.samples);
if rms > 0.0 {
level / rms
} else {
1.0
}
}
};
if (gain - 1.0).abs() > f32::EPSILON {
for sample in &mut buffer.samples {
*sample *= gain;
}
}
Ok(())
}
pub fn compute_rms(samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let sum_sq: f32 = samples.iter().map(|s| s * s).sum();
(sum_sq / samples.len() as f32).sqrt()
}
pub fn compute_peak(samples: &[f32]) -> f32 {
samples.iter().map(|s| s.abs()).fold(0.0f32, f32::max)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peak_normalization() {
let mut buf = AudioBuffer::new(44100, vec![0.5, -0.3, 0.8, -0.2], 1);
normalize(&mut buf, NormalizationTarget::Peak(1.0)).unwrap();
let peak = compute_peak(&buf.samples);
assert!((peak - 1.0).abs() < 0.01);
}
#[test]
fn test_rms_normalization() {
let mut buf = AudioBuffer::new(44100, vec![0.5, -0.3, 0.8, -0.2], 1);
normalize(&mut buf, NormalizationTarget::Rms(0.5)).unwrap();
let rms = compute_rms(&buf.samples);
assert!((rms - 0.5).abs() < 0.01);
}
#[test]
fn test_normalize_empty() {
let mut buf = AudioBuffer::new(44100, vec![], 1);
normalize(&mut buf, NormalizationTarget::Peak(1.0)).unwrap(); assert!(buf.samples.is_empty());
}
#[test]
fn test_normalize_silence() {
let mut buf = AudioBuffer::new(44100, vec![0.0; 100], 1);
normalize(&mut buf, NormalizationTarget::Peak(1.0)).unwrap(); assert!(buf.samples.iter().all(|s| s.is_finite()));
}
#[test]
fn peak_reaches_requested_level_without_hidden_cap() {
let mut buf = AudioBuffer::new(44100, vec![0.01, -0.01], 1);
normalize(&mut buf, NormalizationTarget::Peak(0.5)).unwrap();
assert!((compute_peak(&buf.samples) - 0.5).abs() < 1e-6, "{:?}", buf.samples);
}
#[test]
fn rms_reaches_requested_level_without_hidden_cap() {
let mut buf = AudioBuffer::new(44100, vec![0.01; 64], 1);
normalize(&mut buf, NormalizationTarget::Rms(0.9)).unwrap();
assert!((compute_rms(&buf.samples) - 0.9).abs() < 1e-6, "{:?}", compute_rms(&buf.samples));
}
#[test]
fn non_finite_target_is_rejected_without_touching_samples() {
for target in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let original = vec![0.5f32, -0.25];
let mut buf = AudioBuffer::new(44100, original.clone(), 1);
let err = normalize(&mut buf, NormalizationTarget::Peak(target)).unwrap_err();
assert_eq!(err, NormalizationError::NonFiniteTarget);
assert_eq!(buf.samples, original, "buffer must be untouched for target {target}");
let mut buf = AudioBuffer::new(44100, original.clone(), 1);
let err = normalize(&mut buf, NormalizationTarget::Rms(target)).unwrap_err();
assert_eq!(err, NormalizationError::NonFiniteTarget);
assert_eq!(buf.samples, original, "buffer must be untouched for target {target}");
}
}
#[test]
fn out_of_range_target_is_rejected() {
for target in [-1.0f32, 1.0001, 2.0, -0.0 - 1e-6] {
let original = vec![0.5f32];
let mut buf = AudioBuffer::new(44100, original.clone(), 1);
assert_eq!(
normalize(&mut buf, NormalizationTarget::Peak(target)).unwrap_err(),
NormalizationError::TargetOutOfRange
);
assert_eq!(buf.samples, original);
let mut buf = AudioBuffer::new(44100, original.clone(), 1);
assert_eq!(
normalize(&mut buf, NormalizationTarget::Rms(target)).unwrap_err(),
NormalizationError::TargetOutOfRange
);
assert_eq!(buf.samples, original);
}
}
#[test]
fn zero_and_one_targets_are_valid() {
let mut buf = AudioBuffer::new(44100, vec![0.5, -0.5], 1);
normalize(&mut buf, NormalizationTarget::Peak(1.0)).unwrap();
assert!((compute_peak(&buf.samples) - 1.0).abs() < 1e-6);
normalize(&mut buf, NormalizationTarget::Peak(0.0)).unwrap();
assert_eq!(compute_peak(&buf.samples), 0.0);
let mut silent = AudioBuffer::new(44100, vec![0.0; 4], 1);
normalize(&mut silent, NormalizationTarget::Peak(0.0)).unwrap();
assert!(silent.samples.iter().all(|s| *s == 0.0));
}
#[test]
fn test_compute_rms() {
let samples = vec![1.0, -1.0, 1.0, -1.0];
let rms = compute_rms(&samples);
assert!((rms - 1.0).abs() < 0.01);
}
#[test]
fn test_compute_peak() {
let samples = vec![0.1, -0.5, 0.3, -0.9, 0.2];
let peak = compute_peak(&samples);
assert!((peak - 0.9).abs() < f32::EPSILON);
}
}