#![forbid(unsafe_code)]
use std::f64::consts::PI;
#[derive(Clone, Debug)]
pub struct LoudnessMeasurement {
pub integrated_lufs: f64,
pub short_term_lufs: f64,
pub momentary_lufs: f64,
pub loudness_range_lu: f64,
pub true_peak_dbtp: f64,
}
pub struct GatedLoudnessMeter {
pub sample_rate: u32,
pub channels: usize,
pub channel_weights: Vec<f32>,
block_size: usize,
hop_size: usize,
blocks: Vec<f64>,
filters: Vec<KWeightState>,
ring: Vec<f32>,
ring_pos: usize,
true_peak_linear: f64,
}
#[derive(Clone, Default)]
struct KWeightState {
x1_s1: f64,
x2_s1: f64,
y1_s1: f64,
y2_s1: f64,
x1_s2: f64,
x2_s2: f64,
y1_s2: f64,
y2_s2: f64,
}
#[derive(Clone, Debug)]
struct BiquadCoeffs {
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
}
fn k_stage1_coeffs(sample_rate: u32) -> BiquadCoeffs {
let f0 = 1_681.974_450_955_533_f64;
let g_db = 3.999_843_853_973_347_f64;
let q = 0.707_175_236_955_419_6_f64;
let k = (PI * f0 / sample_rate as f64).tan();
let k2 = k * k;
let vh = 10.0_f64.powf(g_db / 20.0);
let vb = vh.powf(0.499_666_774_154_541_6_f64);
let norm = 1.0 / (1.0 + k / q + k2);
BiquadCoeffs {
b0: (vh + vb * k / q + k2) * norm,
b1: 2.0 * (k2 - vh) * norm,
b2: (vh - vb * k / q + k2) * norm,
a1: 2.0 * (k2 - 1.0) * norm,
a2: (1.0 - k / q + k2) * norm,
}
}
fn k_stage2_coeffs(sample_rate: u32) -> BiquadCoeffs {
let f0 = 38.135_470_876_024_44_f64;
let q = 0.500_327_037_323_877_3_f64;
let k = (PI * f0 / sample_rate as f64).tan();
let k2 = k * k;
let norm = 1.0 / (1.0 + k / q + k2);
BiquadCoeffs {
b0: norm,
b1: -2.0 * norm,
b2: norm,
a1: 2.0 * (k2 - 1.0) * norm,
a2: (1.0 - k / q + k2) * norm,
}
}
#[inline]
fn biquad_process(
c: &BiquadCoeffs,
x: f64,
x1: &mut f64,
x2: &mut f64,
y1: &mut f64,
y2: &mut f64,
) -> f64 {
let y = c.b0 * x + c.b1 * *x1 + c.b2 * *x2 - c.a1 * *y1 - c.a2 * *y2;
*x2 = *x1;
*x1 = x;
*y2 = *y1;
*y1 = y;
y
}
impl GatedLoudnessMeter {
#[must_use]
pub fn new(sample_rate: u32, channels: usize) -> Self {
let channels = channels.max(1);
let block_size = sample_rate as usize * 4 / 10; let hop_size = sample_rate as usize / 10;
let channel_weights = vec![1.0f32; channels];
let filters = vec![KWeightState::default(); channels];
let ring = vec![0.0f32; block_size * channels];
Self {
sample_rate,
channels,
channel_weights,
block_size,
hop_size,
blocks: Vec::new(),
filters,
ring,
ring_pos: 0,
true_peak_linear: 0.0,
}
}
#[must_use]
pub fn k_weighted_filter(samples: &[f32], sample_rate: u32) -> Vec<f32> {
let c1 = k_stage1_coeffs(sample_rate);
let c2 = k_stage2_coeffs(sample_rate);
let mut x1_s1 = 0.0f64;
let mut x2_s1 = 0.0f64;
let mut y1_s1 = 0.0f64;
let mut y2_s1 = 0.0f64;
let mut x1_s2 = 0.0f64;
let mut x2_s2 = 0.0f64;
let mut y1_s2 = 0.0f64;
let mut y2_s2 = 0.0f64;
samples
.iter()
.map(|&s| {
let s_f64 = s as f64;
let s1 = biquad_process(&c1, s_f64, &mut x1_s1, &mut x2_s1, &mut y1_s1, &mut y2_s1);
let s2 = biquad_process(&c2, s1, &mut x1_s2, &mut x2_s2, &mut y1_s2, &mut y2_s2);
s2 as f32
})
.collect()
}
pub fn process_block(&mut self, samples: &[f32]) {
let ch = self.channels;
let c1 = k_stage1_coeffs(self.sample_rate);
let c2 = k_stage2_coeffs(self.sample_rate);
let frame_count = samples.len() / ch;
for frame in 0..frame_count {
for c in 0..ch {
let s = samples[frame * ch + c].abs() as f64;
if s > self.true_peak_linear {
self.true_peak_linear = s;
}
}
for c in 0..ch {
let raw = samples[frame * ch + c] as f64;
let s = &mut self.filters[c];
let w1 = biquad_process(
&c1,
raw,
&mut s.x1_s1,
&mut s.x2_s1,
&mut s.y1_s1,
&mut s.y2_s1,
);
let w2 = biquad_process(
&c2,
w1,
&mut s.x1_s2,
&mut s.x2_s2,
&mut s.y1_s2,
&mut s.y2_s2,
);
let ring_idx = self.ring_pos * ch + c;
self.ring[ring_idx] = w2 as f32;
}
self.ring_pos += 1;
if self.ring_pos >= self.block_size {
let block_loudness = self.compute_block_loudness();
self.blocks.push(block_loudness);
let keep = self.block_size - self.hop_size;
let src_start = self.hop_size * ch;
self.ring.copy_within(src_start.., 0);
let zero_start = keep * ch;
for v in &mut self.ring[zero_start..] {
*v = 0.0;
}
self.ring_pos = keep;
}
}
}
fn compute_block_loudness(&self) -> f64 {
let ch = self.channels;
let mut weighted_ms = 0.0f64;
for c in 0..ch {
let weight = self.channel_weights[c] as f64;
if weight == 0.0 {
continue;
}
let ms: f64 = (0..self.block_size)
.map(|f| {
let s = self.ring[f * ch + c] as f64;
s * s
})
.sum::<f64>()
/ self.block_size as f64;
weighted_ms += weight * ms;
}
if weighted_ms < 1e-15 {
return f64::NEG_INFINITY;
}
-0.691 + 10.0 * weighted_ms.log10()
}
#[must_use]
pub fn integrated_loudness(&self) -> f64 {
if self.blocks.is_empty() {
return f64::NEG_INFINITY;
}
let abs_gate = -70.0_f64;
let stage1: Vec<f64> = self
.blocks
.iter()
.copied()
.filter(|&b| b > abs_gate)
.collect();
if stage1.is_empty() {
return f64::NEG_INFINITY;
}
let mean_power_1: f64 =
stage1.iter().map(|&l| 10.0_f64.powf(l / 10.0)).sum::<f64>() / stage1.len() as f64;
let j = -0.691 + 10.0 * mean_power_1.log10();
let rel_gate = j - 10.0;
let stage2: Vec<f64> = stage1.iter().copied().filter(|&b| b > rel_gate).collect();
if stage2.is_empty() {
return f64::NEG_INFINITY;
}
let mean_power_2: f64 =
stage2.iter().map(|&l| 10.0_f64.powf(l / 10.0)).sum::<f64>() / stage2.len() as f64;
-0.691 + 10.0 * mean_power_2.log10()
}
#[must_use]
pub fn loudness_range(&self) -> f64 {
if self.blocks.len() < 2 {
return 0.0;
}
let abs_gate = -70.0_f64;
let mut gated: Vec<f64> = self
.blocks
.iter()
.copied()
.filter(|&b| b > abs_gate)
.collect();
if gated.is_empty() {
return 0.0;
}
let mean_power: f64 =
gated.iter().map(|&l| 10.0_f64.powf(l / 10.0)).sum::<f64>() / gated.len() as f64;
let mean_lufs = -0.691 + 10.0 * mean_power.log10();
let rel_gate = mean_lufs - 20.0;
gated.retain(|&b| b > rel_gate);
if gated.len() < 2 {
return 0.0;
}
gated.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = gated.len();
let p10_idx = ((n as f64 - 1.0) * 0.10).round() as usize;
let p95_idx = ((n as f64 - 1.0) * 0.95).round() as usize;
gated[p95_idx] - gated[p10_idx]
}
#[must_use]
pub fn momentary_loudness(&self) -> f64 {
self.blocks.last().copied().unwrap_or(f64::NEG_INFINITY)
}
#[must_use]
pub fn short_term_loudness(&self) -> f64 {
const SHORT_TERM_BLOCKS: usize = 30;
let start = self.blocks.len().saturating_sub(SHORT_TERM_BLOCKS);
let window: Vec<f64> = self.blocks[start..].iter().copied().collect();
if window.is_empty() {
return f64::NEG_INFINITY;
}
let finite: Vec<f64> = window.iter().copied().filter(|x| x.is_finite()).collect();
if finite.is_empty() {
return f64::NEG_INFINITY;
}
let mean_power: f64 =
finite.iter().map(|&l| 10.0_f64.powf(l / 10.0)).sum::<f64>() / finite.len() as f64;
-0.691 + 10.0 * mean_power.log10()
}
#[must_use]
pub fn true_peak_dbtp(&self) -> f64 {
if self.true_peak_linear < 1e-15 {
return f64::NEG_INFINITY;
}
20.0 * self.true_peak_linear.log10()
}
#[must_use]
pub fn measure(samples: &[f32], sample_rate: u32, channels: usize) -> LoudnessMeasurement {
let mut meter = Self::new(sample_rate, channels);
meter.process_block(samples);
LoudnessMeasurement {
integrated_lufs: meter.integrated_loudness(),
short_term_lufs: meter.short_term_loudness(),
momentary_lufs: meter.momentary_loudness(),
loudness_range_lu: meter.loudness_range(),
true_peak_dbtp: meter.true_peak_dbtp(),
}
}
pub fn reset(&mut self) {
self.blocks.clear();
self.filters = vec![KWeightState::default(); self.channels];
let ring_len = self.ring.len();
self.ring = vec![0.0f32; ring_len];
self.ring_pos = 0;
self.true_peak_linear = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::PI;
fn sine_wave(freq_hz: f32, amplitude: f32, sample_rate: u32, num_samples: usize) -> Vec<f32> {
let sr = sample_rate as f32;
(0..num_samples)
.map(|i| amplitude * (2.0 * PI * freq_hz * i as f32 / sr).sin())
.collect()
}
fn silence(num_samples: usize) -> Vec<f32> {
vec![0.0f32; num_samples]
}
#[test]
fn test_new_default_weights() {
let meter = GatedLoudnessMeter::new(48000, 2);
assert_eq!(meter.channel_weights.len(), 2);
assert_eq!(meter.channel_weights[0], 1.0);
assert_eq!(meter.channel_weights[1], 1.0);
}
#[test]
fn test_new_block_sizes() {
let meter = GatedLoudnessMeter::new(48000, 1);
assert_eq!(meter.block_size, 19200);
assert_eq!(meter.hop_size, 4800);
}
#[test]
fn test_k_weighted_filter_length() {
let samples = vec![0.1f32; 1024];
let output = GatedLoudnessMeter::k_weighted_filter(&samples, 48000);
assert_eq!(output.len(), samples.len());
}
#[test]
fn test_k_weighted_filter_silence() {
let samples = silence(1024);
let output = GatedLoudnessMeter::k_weighted_filter(&samples, 48000);
assert!(
output.iter().all(|x| *x == 0.0),
"K-weighted silence should remain silent"
);
}
#[test]
fn test_k_weighted_filter_finite() {
let samples = sine_wave(1000.0, 0.5, 48000, 4800);
let output = GatedLoudnessMeter::k_weighted_filter(&samples, 48000);
assert!(
output.iter().all(|x| x.is_finite()),
"K-weighted output must be finite"
);
}
#[test]
fn test_silence_integrated_loudness() {
let samples = silence(48000 * 5); let m = GatedLoudnessMeter::measure(&samples, 48000, 1);
assert!(
m.integrated_lufs.is_infinite() || m.integrated_lufs < -70.0,
"Silence should yield < -70 LUFS or -inf"
);
}
#[test]
fn test_loud_signal_integrated_loudness() {
let samples = sine_wave(1000.0, 0.9, 48000, 48000 * 5);
let mut meter = GatedLoudnessMeter::new(48000, 1);
meter.process_block(&samples);
let il = meter.integrated_loudness();
assert!(
il.is_finite() && il > -50.0,
"Loud signal should have a finite integrated loudness, got {il}"
);
}
#[test]
fn test_momentary_loudness_before_block() {
let meter = GatedLoudnessMeter::new(48000, 1);
let m = meter.momentary_loudness();
assert!(m.is_infinite(), "No blocks yet → -inf");
}
#[test]
fn test_short_term_loudness_before_block() {
let meter = GatedLoudnessMeter::new(48000, 1);
let st = meter.short_term_loudness();
assert!(st.is_infinite(), "No blocks yet → -inf");
}
#[test]
fn test_true_peak_tracks_maximum() {
let samples = vec![0.5f32, 0.8f32, -0.9f32, 0.3f32];
let m = GatedLoudnessMeter::measure(&samples, 48000, 1);
let expected_dbtp = 20.0 * 0.9_f64.log10();
assert!(
(m.true_peak_dbtp - expected_dbtp).abs() < 0.01,
"true_peak_dbtp mismatch: expected {expected_dbtp:.3}, got {:.3}",
m.true_peak_dbtp
);
}
#[test]
fn test_true_peak_silence() {
let samples = silence(100);
let m = GatedLoudnessMeter::measure(&samples, 48000, 1);
assert!(
m.true_peak_dbtp.is_infinite(),
"Silence should have -inf true-peak"
);
}
#[test]
fn test_reset_clears_blocks() {
let mut meter = GatedLoudnessMeter::new(48000, 1);
let samples = sine_wave(1000.0, 0.5, 48000, 48000 * 2);
meter.process_block(&samples);
meter.reset();
assert!(
meter.integrated_loudness().is_infinite(),
"After reset, integrated loudness should be -inf"
);
}
#[test]
fn test_loudness_range_empty() {
let meter = GatedLoudnessMeter::new(48000, 1);
let lra = meter.loudness_range();
assert_eq!(lra, 0.0, "LRA of empty meter should be 0");
}
#[test]
fn test_measure_returns_struct() {
let samples = sine_wave(440.0, 0.5, 48000, 48000 * 3);
let m = GatedLoudnessMeter::measure(&samples, 48000, 1);
assert!(m.integrated_lufs.is_finite() || m.integrated_lufs == f64::NEG_INFINITY);
assert!(m.true_peak_dbtp.is_finite() || m.true_peak_dbtp == f64::NEG_INFINITY);
assert!(m.loudness_range_lu >= 0.0);
}
}