const KW_PRE_B: (f64, f64, f64) = (1.0, -2.0, 1.0);
const KW_PRE_A: (f64, f64) = (1.99004745483398, -0.99007225036621);
const KW_SHELF_B: (f64, f64, f64) = (1.53512485958697, -2.69169618940638, 1.19839281085285);
const KW_SHELF_A: (f64, f64) = (1.69065929318241, -0.73248077421585);
const LUFS_BLOCK_MS: u64 = 400;
const LUFS_OVERLAP_NUM: u64 = 3;
const LUFS_OVERLAP_DEN: u64 = 4;
pub(crate) const LUFS_ABS_GATE: f64 = -70.0;
const LUFS_REL_GATE: f64 = -10.0;
const LUFS_OFFSET: f64 = -0.691;
fn apply_biquad(samples: &[f32], b0: f64, b1: f64, b2: f64, a1: f64, a2: f64) -> Vec<f32> {
let mut out = Vec::with_capacity(samples.len());
let mut s1: f64 = 0.0;
let mut s2: f64 = 0.0;
for &x in samples {
let xf = x as f64;
let y = b0 * xf + s1;
s1 = b1 * xf + a1 * y + s2;
s2 = b2 * xf + a2 * y;
out.push(y as f32);
}
out
}
pub(crate) fn apply_k_weighting(samples: &[f32]) -> Vec<f32> {
let pre_filtered = apply_biquad(
samples, KW_PRE_B.0, KW_PRE_B.1, KW_PRE_B.2, KW_PRE_A.0, KW_PRE_A.1,
);
apply_biquad(
&pre_filtered,
KW_SHELF_B.0,
KW_SHELF_B.1,
KW_SHELF_B.2,
KW_SHELF_A.0,
KW_SHELF_A.1,
)
}
#[inline]
pub(crate) fn power_to_lkfs(power: f64) -> f64 {
if power <= f64::EPSILON {
f64::NEG_INFINITY
} else {
LUFS_OFFSET + 10.0 * power.log10()
}
}
fn compute_block_powers(
k_weighted: &[f32],
sample_rate: u32,
block_ms: u64,
overlap_num: u64,
overlap_den: u64,
) -> (Vec<f64>, usize) {
let block_samples = (sample_rate as usize * block_ms as usize) / 1000;
let hop = block_samples * (overlap_den - overlap_num) as usize / overlap_den as usize;
if block_samples > k_weighted.len() || hop == 0 {
return (Vec::new(), hop);
}
let num_blocks = (k_weighted.len() - block_samples) / hop + 1;
let mut powers = Vec::with_capacity(num_blocks);
for b in 0..num_blocks {
let start = b * hop;
let sum_sq: f64 = k_weighted[start..start + block_samples]
.iter()
.map(|&x| (x as f64).powi(2))
.sum();
powers.push(sum_sq / block_samples as f64);
}
(powers, hop)
}
pub fn compute_integrated_lufs(samples: &[f32], sample_rate: u32) -> f64 {
if samples.is_empty() {
return f64::NEG_INFINITY;
}
let k_weighted = apply_k_weighting(samples);
let (block_powers, _hop) = compute_block_powers(
&k_weighted,
sample_rate,
LUFS_BLOCK_MS,
LUFS_OVERLAP_NUM,
LUFS_OVERLAP_DEN,
);
if block_powers.is_empty() {
return f64::NEG_INFINITY;
}
let block_lkfs: Vec<f64> = block_powers.iter().map(|&p| power_to_lkfs(p)).collect();
let gated_1: Vec<f64> = (0..block_powers.len())
.filter(|&i| block_lkfs[i] > LUFS_ABS_GATE)
.map(|i| block_powers[i])
.collect();
if gated_1.is_empty() {
return f64::NEG_INFINITY;
}
let mean_power_1: f64 = gated_1.iter().sum::<f64>() / gated_1.len() as f64;
let ungated_lkfs = power_to_lkfs(mean_power_1);
let rel_threshold_lk = ungated_lkfs + LUFS_REL_GATE;
let gated_2: Vec<f64> = (0..block_powers.len())
.filter(|&i| block_lkfs[i] > LUFS_ABS_GATE && block_lkfs[i] > rel_threshold_lk)
.map(|i| block_powers[i])
.collect();
if gated_2.is_empty() {
return ungated_lkfs;
}
let mean_power_2: f64 = gated_2.iter().sum::<f64>() / gated_2.len() as f64;
power_to_lkfs(mean_power_2)
}
#[inline]
pub fn compute_lufs(samples: &[f32], sample_rate: u32) -> f64 {
compute_integrated_lufs(samples, sample_rate)
}