use crate::core::preanalysis::LoudnessMeasurement;
use ebur128::{EbuR128, Mode};
pub fn measure_loudness(
interleaved: &[f32],
channels: usize,
sample_rate: u32,
) -> Option<LoudnessMeasurement> {
if interleaved.is_empty() || channels == 0 || sample_rate == 0 {
return None;
}
let mut meter = EbuR128::new(
channels as u32,
sample_rate,
Mode::I | Mode::LRA | Mode::TRUE_PEAK,
)
.ok()?;
let whole_frames = interleaved.len() / channels * channels;
meter.add_frames_f32(&interleaved[..whole_frames]).ok()?;
let integrated_lufs = meter.loudness_global().ok()?;
if !integrated_lufs.is_finite() {
return None; }
let loudness_range_lu = meter.loudness_range().ok()?;
let mut true_peak_linear = 0f64;
for ch in 0..channels as u32 {
true_peak_linear = true_peak_linear.max(meter.true_peak(ch).ok()?);
}
let true_peak_dbtp = if true_peak_linear > 0.0 {
20.0 * true_peak_linear.log10()
} else {
f64::NEG_INFINITY
};
Some(LoudnessMeasurement {
integrated_lufs,
true_peak_dbtp,
loudness_range_lu,
})
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_RATE: u32 = 44_100;
fn stereo_sine(freq: f32, amplitude: f32, secs: f32) -> Vec<f32> {
let frames = (secs * SAMPLE_RATE as f32) as usize;
let mut out = Vec::with_capacity(frames * 2);
for i in 0..frames {
let s =
amplitude * (std::f32::consts::TAU * freq * i as f32 / SAMPLE_RATE as f32).sin();
out.push(s);
out.push(s);
}
out
}
#[test]
fn test_full_scale_stereo_sine_reference_level() {
let m = measure_loudness(&stereo_sine(997.0, 1.0, 5.0), 2, SAMPLE_RATE).unwrap();
assert!(
m.integrated_lufs.abs() < 0.5,
"integrated {}",
m.integrated_lufs
);
assert!(
m.true_peak_dbtp.abs() < 0.3,
"true peak {}",
m.true_peak_dbtp
);
assert!(m.loudness_range_lu < 1.0, "LRA {}", m.loudness_range_lu);
}
#[test]
fn test_level_tracks_amplitude() {
let loud = measure_loudness(&stereo_sine(997.0, 1.0, 5.0), 2, SAMPLE_RATE).unwrap();
let quiet = measure_loudness(&stereo_sine(997.0, 0.1, 5.0), 2, SAMPLE_RATE).unwrap();
let drop = loud.integrated_lufs - quiet.integrated_lufs;
assert!((drop - 20.0).abs() < 0.5, "drop {}", drop);
}
#[test]
fn test_silence_and_degenerate_inputs_return_none() {
assert!(measure_loudness(&vec![0.0; SAMPLE_RATE as usize * 4], 2, SAMPLE_RATE).is_none());
assert!(measure_loudness(&[], 2, SAMPLE_RATE).is_none());
assert!(measure_loudness(&[0.5; 1024], 0, SAMPLE_RATE).is_none());
assert!(measure_loudness(&[0.5; 1024], 2, 0).is_none());
}
#[test]
fn test_gain_helpers() {
let m = LoudnessMeasurement {
integrated_lufs: -10.0,
true_peak_dbtp: -0.5,
loudness_range_lu: 3.0,
};
assert!((m.gain_db_to(-14.0) - (-4.0)).abs() < 1e-12);
assert!((m.gain_linear_to(-14.0) - 10f64.powf(-0.2)).abs() < 1e-12);
assert!((m.gain_db_to(-10.0)).abs() < 1e-12);
}
}