use alloc::vec::Vec;
use crate::channel_layout::ChannelLayout;
use crate::filter::{BiquadCoeffs, BiquadState, apply_biquad, k_weighting_coeffs};
const ABSOLUTE_GATE: f64 = -70.0;
const RELATIVE_GATE: f64 = -10.0;
const GATING_BLOCK_S: f64 = 0.4;
const GATING_OVERLAP: f64 = 0.75;
const MOMENTARY_S: f64 = 0.4;
const SHORT_TERM_S: f64 = 3.0;
const LOUDNESS_OFFSET: f64 = -0.691;
#[inline]
fn mean_sq_to_lkfs(mean_sq: f64) -> f64 {
if mean_sq <= 0.0 {
f64::NEG_INFINITY
} else {
LOUDNESS_OFFSET + 10.0 * libm::log10(mean_sq)
}
}
#[inline]
fn lkfs_to_mean_sq(lkfs: f64) -> f64 {
libm::pow(10.0, (lkfs - LOUDNESS_OFFSET) / 10.0)
}
#[derive(Debug, Clone)]
struct ChannelFilter {
stage1: BiquadState,
stage2: BiquadState,
coeffs: BiquadCoeffsPair,
}
#[derive(Debug, Clone, Copy)]
struct BiquadCoeffsPair {
stage1: BiquadCoeffs,
stage2: BiquadCoeffs,
}
impl ChannelFilter {
fn new(stage1: BiquadCoeffs, stage2: BiquadCoeffs) -> Self {
Self {
stage1: BiquadState::new(),
stage2: BiquadState::new(),
coeffs: BiquadCoeffsPair { stage1, stage2 },
}
}
#[inline]
fn process(&mut self, sample: f64) -> f64 {
let y1 = apply_biquad(sample, &self.coeffs.stage1, &mut self.stage1);
apply_biquad(y1, &self.coeffs.stage2, &mut self.stage2)
}
}
#[derive(Debug, Clone, Copy)]
struct GatingBlock {
lkfs: f64,
}
#[derive(Debug, Clone)]
pub struct LoudnessMeter {
sample_rate: u32,
layout: ChannelLayout,
channel_count: usize,
filters: Vec<ChannelFilter>,
coeffs: BiquadCoeffsPair,
weighted_power: Vec<f64>,
gating_blocks: Vec<GatingBlock>,
integrated: f64,
lra: f64,
max_momentary: f64,
max_short_term: f64,
finished: bool,
frame_count: usize,
}
impl LoudnessMeter {
pub fn new(sample_rate: u32, layout: ChannelLayout) -> Result<Self, crate::Error> {
if sample_rate == 0 {
return Err(crate::Error::InvalidSampleRate { got: sample_rate });
}
let (stage1, stage2) = k_weighting_coeffs(sample_rate);
let coeffs = BiquadCoeffsPair { stage1, stage2 };
let channel_count = layout.channel_count();
Ok(Self {
sample_rate,
layout,
channel_count,
filters: (0..channel_count)
.map(|_| ChannelFilter::new(stage1, stage2))
.collect(),
coeffs,
weighted_power: Vec::new(),
gating_blocks: Vec::new(),
integrated: f64::NEG_INFINITY,
lra: 0.0,
max_momentary: f64::NEG_INFINITY,
max_short_term: f64::NEG_INFINITY,
finished: false,
frame_count: 0,
})
}
pub fn reset(&mut self) {
for f in &mut self.filters {
*f = ChannelFilter::new(self.coeffs.stage1, self.coeffs.stage2);
}
self.weighted_power.clear();
self.gating_blocks.clear();
self.integrated = f64::NEG_INFINITY;
self.lra = 0.0;
self.max_momentary = f64::NEG_INFINITY;
self.max_short_term = f64::NEG_INFINITY;
self.finished = false;
self.frame_count = 0;
}
pub fn push_f32(&mut self, channels: &[f32]) -> Result<(), crate::Error> {
if self.finished {
return Err(crate::Error::Finished);
}
if channels.len() != self.channel_count {
return Err(crate::Error::ChannelMismatch {
expected: self.channel_count,
got: channels.len(),
});
}
let mut sum_sq = 0.0f64;
for (i, &sample) in channels.iter().enumerate() {
let sample_f64 = f64::from(sample);
if !sample_f64.is_finite() {
return Err(crate::Error::NonFiniteSample {
index: self.frame_count,
channel: i,
value: sample_f64,
});
}
let weight = self.layout.weight(i);
if weight == 0.0 {
continue;
}
let filtered = self.filters[i].process(sample_f64);
sum_sq += weight * filtered * filtered;
}
self.weighted_power.push(sum_sq);
self.frame_count += 1;
Ok(())
}
pub fn push_f64(&mut self, channels: &[f64]) -> Result<(), crate::Error> {
if self.finished {
return Err(crate::Error::Finished);
}
if channels.len() != self.channel_count {
return Err(crate::Error::ChannelMismatch {
expected: self.channel_count,
got: channels.len(),
});
}
let mut sum_sq = 0.0f64;
for (i, &sample) in channels.iter().enumerate() {
if !sample.is_finite() {
return Err(crate::Error::NonFiniteSample {
index: self.frame_count,
channel: i,
value: sample,
});
}
let weight = self.layout.weight(i);
if weight == 0.0 {
continue;
}
let filtered = self.filters[i].process(sample);
sum_sq += weight * filtered * filtered;
}
self.weighted_power.push(sum_sq);
self.frame_count += 1;
Ok(())
}
pub fn push_interleaved_f32(
&mut self,
left: &[f32],
right: &[f32],
) -> Result<(), crate::Error> {
if self.finished {
return Err(crate::Error::Finished);
}
if left.len() != right.len() {
return Err(crate::Error::ChannelMismatch {
expected: left.len(),
got: right.len(),
});
}
for (frame_idx, (&l, &r)) in left.iter().zip(right.iter()).enumerate() {
let l_f64 = f64::from(l);
let r_f64 = f64::from(r);
if !l_f64.is_finite() {
return Err(crate::Error::NonFiniteSample {
index: self.frame_count + frame_idx,
channel: 0,
value: l_f64,
});
}
if !r_f64.is_finite() {
return Err(crate::Error::NonFiniteSample {
index: self.frame_count + frame_idx,
channel: 1,
value: r_f64,
});
}
let weight_l = self.layout.weight(0);
let weight_r = self.layout.weight(1);
let mut sum_sq = 0.0;
if weight_l != 0.0 {
let f = self.filters[0].process(l_f64);
sum_sq += weight_l * f * f;
}
if weight_r != 0.0 {
let f = self.filters[1].process(r_f64);
sum_sq += weight_r * f * f;
}
self.weighted_power.push(sum_sq);
}
self.frame_count += left.len();
Ok(())
}
pub fn finish(&mut self) {
if self.finished {
return;
}
self.finished = true;
let gating_block_samples = ((GATING_BLOCK_S * self.sample_rate as f64) as usize).max(1);
let step_samples = ((gating_block_samples as f64 * (1.0 - GATING_OVERLAP)) as usize).max(1);
let mut block_idx = 0usize;
loop {
let start = block_idx * step_samples;
let end = (start + gating_block_samples).min(self.weighted_power.len());
if end - start < gating_block_samples / 2 {
break;
}
let n = end - start;
if n == 0 {
break;
}
let mean_sq: f64 = self.weighted_power[start..end].iter().sum::<f64>() / n as f64;
let lkfs = mean_sq_to_lkfs(mean_sq);
self.gating_blocks.push(GatingBlock { lkfs });
block_idx += 1;
}
let abs_gated: Vec<f64> = self
.gating_blocks
.iter()
.filter(|b| b.lkfs > ABSOLUTE_GATE)
.map(|b| b.lkfs)
.collect();
let integrated = if abs_gated.is_empty() {
f64::NEG_INFINITY
} else {
let abs_gated_loudness = mean_of_lkfs(&abs_gated);
let rel_threshold = abs_gated_loudness + RELATIVE_GATE;
let rel_gated: Vec<f64> = abs_gated
.iter()
.filter(|&&l| l > rel_threshold)
.copied()
.collect();
if rel_gated.is_empty() {
f64::NEG_INFINITY
} else {
mean_of_lkfs(&rel_gated)
}
};
self.integrated = integrated;
self.lra = compute_lra(&self.gating_blocks);
self.max_momentary = self.compute_max_sliding(MOMENTARY_S);
self.max_short_term = self.compute_max_sliding(SHORT_TERM_S);
}
fn compute_max_sliding(&self, window_s: f64) -> f64 {
let window_samples = ((window_s * self.sample_rate as f64) as usize).max(1);
let n = self.weighted_power.len();
if n == 0 || n < window_samples {
return f64::NEG_INFINITY;
}
let mut window_sum: f64 = self.weighted_power[..window_samples].iter().sum();
let mut max_lkfs = mean_sq_to_lkfs(window_sum / window_samples as f64);
for i in 1..=(n - window_samples) {
window_sum -= self.weighted_power[i - 1];
window_sum += self.weighted_power[i + window_samples - 1];
let lkfs = mean_sq_to_lkfs(window_sum / window_samples as f64);
if lkfs > max_lkfs {
max_lkfs = lkfs;
}
}
max_lkfs
}
#[must_use]
pub fn integrated_lufs(&self) -> f64 {
self.integrated
}
#[must_use]
pub fn integrated_lu(&self) -> f64 {
if self.integrated.is_finite() {
self.integrated + 23.0
} else {
f64::NEG_INFINITY
}
}
#[must_use]
pub fn max_momentary_lufs(&self) -> f64 {
self.max_momentary
}
#[must_use]
pub fn max_short_term_lufs(&self) -> f64 {
self.max_short_term
}
#[must_use]
pub fn loudness_range(&self) -> f64 {
self.lra
}
#[must_use]
pub fn frame_count(&self) -> usize {
self.frame_count
}
#[must_use]
pub fn duration_seconds(&self) -> f64 {
self.frame_count as f64 / self.sample_rate as f64
}
}
fn mean_of_lkfs(values: &[f64]) -> f64 {
if values.is_empty() {
return f64::NEG_INFINITY;
}
let n = values.len() as f64;
let sum_power: f64 = values.iter().map(|&l| lkfs_to_mean_sq(l)).sum();
mean_sq_to_lkfs(sum_power / n)
}
fn compute_lra(blocks: &[GatingBlock]) -> f64 {
let abs_gated: Vec<f64> = blocks
.iter()
.filter(|b| b.lkfs >= ABSOLUTE_GATE)
.map(|b| b.lkfs)
.collect();
if abs_gated.is_empty() {
return 0.0;
}
let abs_integrated = mean_of_lkfs(&abs_gated);
let rel_threshold = abs_integrated - 20.0;
let mut rel_gated: Vec<f64> = abs_gated
.iter()
.filter(|&&l| l >= rel_threshold)
.copied()
.collect();
if rel_gated.is_empty() {
return 0.0;
}
rel_gated.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let n = rel_gated.len();
let low_idx = (libm::round((n as f64 - 1.0) * 10.0 / 100.0 + 1.0) as usize).saturating_sub(1);
let high_idx = (libm::round((n as f64 - 1.0) * 95.0 / 100.0 + 1.0) as usize).saturating_sub(1);
let low_idx = low_idx.min(n - 1);
let high_idx = high_idx.min(n - 1);
let perc_low = rel_gated[low_idx];
let perc_high = rel_gated[high_idx];
perc_high - perc_low
}
#[cfg(test)]
mod tests {
use super::LoudnessMeter;
use crate::channel_layout::ChannelLayout;
#[test]
fn stereo_1khz_minus_23_lufs_is_minus_23() {
let sample_rate = 48_000;
let duration = 2.0;
let n = (duration * sample_rate as f64) as usize;
let amplitude = 10.0f64.powf(-23.0 / 20.0) as f32;
let mut left = alloc::vec![0.0f32; n];
let mut right = alloc::vec![0.0f32; n];
for i in 0..n {
let t = i as f64 / sample_rate as f64;
let val = (amplitude as f64 * (2.0 * core::f64::consts::PI * 1000.0 * t).sin()) as f32;
left[i] = val;
right[i] = val;
}
let mut meter = LoudnessMeter::new(sample_rate, ChannelLayout::Stereo).unwrap();
meter.push_interleaved_f32(&left, &right).unwrap();
meter.finish();
let lufs = meter.integrated_lufs();
assert!((lufs - (-23.0)).abs() < 0.2, "got {lufs}, expected -23.0");
}
#[test]
fn absolute_gate_excludes_silence() {
let sample_rate = 48_000;
let silence_s = 2.0;
let tone_s = 3.0;
let n_silence = (silence_s * sample_rate as f64) as usize;
let n_tone = (tone_s * sample_rate as f64) as usize;
let amplitude = 10.0f64.powf(-23.0 / 20.0) as f32;
let mut left = alloc::vec![0.0f32; n_silence + n_tone];
let mut right = alloc::vec![0.0f32; n_silence + n_tone];
for i in n_silence..(n_silence + n_tone) {
let t = (i - n_silence) as f64 / sample_rate as f64;
let val = (amplitude as f64 * (2.0 * core::f64::consts::PI * 1000.0 * t).sin()) as f32;
left[i] = val;
right[i] = val;
}
let mut meter = LoudnessMeter::new(sample_rate, ChannelLayout::Stereo).unwrap();
meter.push_interleaved_f32(&left, &right).unwrap();
meter.finish();
let lufs = meter.integrated_lufs();
assert!(
(lufs - (-23.0)).abs() < 0.5,
"got {lufs}, expected ~-23.0 (gating should exclude silence)"
);
}
#[test]
fn low_signal_below_absolute_gate_does_not_drag_integrated() {
let sample_rate = 48_000;
let tone_amplitude = 10.0f64.powf(-23.0 / 20.0) as f32;
let low_amplitude = 10.0f64.powf(-80.0 / 20.0) as f32;
let seg_low1_s = 2.0;
let seg_tone_s = 3.0;
let seg_low2_s = 2.0;
let total = (seg_low1_s + seg_tone_s + seg_low2_s) * sample_rate as f64;
let n = total as usize;
let mut left = alloc::vec![0.0f32; n];
let mut right = alloc::vec![0.0f32; n];
let n_low1 = (seg_low1_s * sample_rate as f64) as usize;
let n_tone = (seg_tone_s * sample_rate as f64) as usize;
for i in 0..n_low1 {
let t = i as f64 / sample_rate as f64;
let val =
(low_amplitude as f64 * (2.0 * core::f64::consts::PI * 1000.0 * t).sin()) as f32;
left[i] = val;
right[i] = val;
}
for i in 0..n_tone {
let t = i as f64 / sample_rate as f64;
let val =
(tone_amplitude as f64 * (2.0 * core::f64::consts::PI * 1000.0 * t).sin()) as f32;
left[n_low1 + i] = val;
right[n_low1 + i] = val;
}
let offset = n_low1 + n_tone;
for i in 0..(n - offset) {
let t = i as f64 / sample_rate as f64;
let val =
(low_amplitude as f64 * (2.0 * core::f64::consts::PI * 1000.0 * t).sin()) as f32;
left[offset + i] = val;
right[offset + i] = val;
}
let mut meter = LoudnessMeter::new(sample_rate, ChannelLayout::Stereo).unwrap();
meter.push_interleaved_f32(&left, &right).unwrap();
meter.finish();
let lufs = meter.integrated_lufs();
assert!(
(lufs - (-23.0)).abs() < 0.5,
"got {lufs}, expected ~-23.0 (low segments should be gated out)"
);
}
#[test]
fn accepts_441_khz() {
assert!(LoudnessMeter::new(44_100, ChannelLayout::Stereo).is_ok());
}
#[test]
fn rejects_zero_rate() {
let err = LoudnessMeter::new(0, ChannelLayout::Stereo).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("sample rate") && msg.contains("0"),
"expected invalid sample rate error, got: {msg}"
);
}
#[test]
fn coeffs_at_48k_match_tabulated() {
let (stage1, stage2) = crate::filter::k_weighting_coeffs(48_000);
let shelf_ref = crate::filter::BiquadCoeffs {
b0: 1.535_124_859_586_97,
b1: -2.691_696_189_406_38,
b2: 1.198_392_810_852_85,
a1: -1.690_659_293_182_41,
a2: 0.732_480_774_215_85,
};
let hp_ref = crate::filter::BiquadCoeffs {
b0: 1.0,
b1: -2.0,
b2: 1.0,
a1: -1.990_047_454_833_98,
a2: 0.990_072_250_366_21,
};
for (got, want) in [
(stage1.b0, shelf_ref.b0),
(stage1.b1, shelf_ref.b1),
(stage1.b2, shelf_ref.b2),
(stage1.a1, shelf_ref.a1),
(stage1.a2, shelf_ref.a2),
(stage2.b0, hp_ref.b0),
(stage2.b1, hp_ref.b1),
(stage2.b2, hp_ref.b2),
(stage2.a1, hp_ref.a1),
(stage2.a2, hp_ref.a2),
] {
assert!(
(got - want).abs() < 1e-12,
"expected {want}, got {got} (diff {})",
(got - want).abs()
);
}
}
#[test]
fn rejects_nan_in_planar_f32() {
let mut meter = LoudnessMeter::new(48_000, ChannelLayout::Stereo).unwrap();
let err = meter.push_f32(&[f32::NAN, 0.5]).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("non-finite"), "got: {msg}");
}
#[test]
fn rejects_inf_in_planar_f32() {
let mut meter = LoudnessMeter::new(48_000, ChannelLayout::Stereo).unwrap();
let err = meter.push_f32(&[0.5, f32::INFINITY]).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("non-finite"), "got: {msg}");
}
#[test]
fn rejects_non_finite_in_planar_f64() {
let mut meter = LoudnessMeter::new(48_000, ChannelLayout::Stereo).unwrap();
let err = meter.push_f64(&[f64::NEG_INFINITY, 0.5]).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("non-finite"), "got: {msg}");
}
#[test]
fn rejects_non_finite_in_interleaved_f32() {
let mut meter = LoudnessMeter::new(48_000, ChannelLayout::Stereo).unwrap();
let err = meter.push_interleaved_f32(&[0.5], &[f32::NAN]).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("non-finite"), "got: {msg}");
}
#[test]
fn meter_not_poisoned_after_non_finite_rejection() {
let mut meter = LoudnessMeter::new(48_000, ChannelLayout::Stereo).unwrap();
meter.push_f32(&[0.5, 0.5]).unwrap();
let _ = meter.push_f32(&[f32::NAN, 0.5]);
for _ in 0..192_000 {
meter.push_f32(&[0.1, 0.1]).unwrap();
}
meter.finish();
let lufs = meter.integrated_lufs();
assert!(lufs.is_finite(), "meter was poisoned: got {lufs}");
assert!(lufs < -10.0, "unexpectedly loud: {lufs}");
}
#[test]
fn non_finite_sample_error_carries_metadata() {
let mut meter = LoudnessMeter::new(48_000, ChannelLayout::Stereo).unwrap();
meter.push_f32(&[1.0, 1.0]).unwrap();
let err = meter.push_f32(&[f32::NEG_INFINITY, 0.5]).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("channel 0"), "got: {msg}");
assert!(msg.contains("index 1"), "got: {msg}");
}
}