use crate::analyzer::{ABSOLUTE_GATE_LUFS, LUFS_OFFSET, RELATIVE_GATE_OFFSET_LU};
#[inline]
fn lufs_to_ms_threshold(lufs: f64) -> f64 {
libm::pow(10.0, (lufs - LUFS_OFFSET) / 10.0)
}
pub(crate) fn compute_integrated(blocks: &[f32]) -> Option<f64> {
if blocks.is_empty() {
return None;
}
let abs_threshold_ms = lufs_to_ms_threshold(ABSOLUTE_GATE_LUFS);
let mut sum: f64 = 0.0;
let mut count: u64 = 0;
for &ms in blocks {
let msf = ms as f64;
if msf > abs_threshold_ms {
sum += msf;
count += 1;
}
}
if count == 0 {
return None;
}
let mean_after_abs = sum / count as f64;
let relative_threshold_lufs =
LUFS_OFFSET + 10.0 * libm::log10(mean_after_abs) + RELATIVE_GATE_OFFSET_LU;
let rel_threshold_ms = lufs_to_ms_threshold(relative_threshold_lufs);
let mut sum2: f64 = 0.0;
let mut count2: u64 = 0;
for &ms in blocks {
let msf = ms as f64;
if msf > abs_threshold_ms && msf > rel_threshold_ms {
sum2 += msf;
count2 += 1;
}
}
if count2 == 0 {
return None;
}
let mean = sum2 / count2 as f64;
Some(LUFS_OFFSET + 10.0 * libm::log10(mean))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_yields_none() {
assert!(compute_integrated(&[]).is_none());
}
#[test]
fn all_silent_blocks_yield_none() {
let blocks = vec![0.0f32; 100];
assert!(compute_integrated(&blocks).is_none());
}
#[test]
fn constant_loudness_returns_calibration() {
let target_lufs = -23.0_f64;
let target_ms = lufs_to_ms_threshold(target_lufs) as f32;
let blocks = vec![target_ms; 200];
let lufs = compute_integrated(&blocks).unwrap();
assert!((lufs - target_lufs).abs() < 1e-3, "got {lufs}");
}
#[test]
fn quiet_blocks_excluded_by_absolute_gate() {
let loud_lufs = -20.0_f64;
let loud_ms = lufs_to_ms_threshold(loud_lufs) as f32;
let mut blocks = vec![0.0f32; 999];
blocks.push(loud_ms);
let lufs = compute_integrated(&blocks).unwrap();
assert!((lufs - loud_lufs).abs() < 1.0, "got {lufs}");
}
}