#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelLayout {
Mono,
Stereo,
Surround50,
Surround51,
}
impl ChannelLayout {
#[must_use]
pub const fn channel_count(self) -> usize {
match self {
Self::Mono => 1,
Self::Stereo => 2,
Self::Surround50 => 5,
Self::Surround51 => 6,
}
}
#[must_use]
pub fn channel_weight(self, idx: usize) -> f64 {
match self {
Self::Mono => 1.0,
Self::Stereo => 1.0,
Self::Surround50 => match idx {
0..=2 => 1.0, 3 | 4 => 1.41, _ => 0.0,
},
Self::Surround51 => match idx {
0..=2 => 1.0, 3 => 0.0, 4 | 5 => 1.41, _ => 0.0,
},
}
}
}
#[derive(Debug, Clone)]
pub struct LufsMeterConfig {
pub sample_rate: f32,
pub layout: ChannelLayout,
}
impl Default for LufsMeterConfig {
fn default() -> Self {
Self {
sample_rate: 48_000.0,
layout: ChannelLayout::Stereo,
}
}
}
#[derive(Debug, Clone)]
struct Biquad {
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
x1: f64,
x2: f64,
y1: f64,
y2: f64,
}
impl Biquad {
fn new(b0: f64, b1: f64, b2: f64, a1: f64, a2: f64) -> Self {
Self {
b0,
b1,
b2,
a1,
a2,
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
}
}
#[inline]
fn process(&mut self, x: f64) -> f64 {
let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = x;
self.y2 = self.y1;
self.y1 = y;
y
}
fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
self.y1 = 0.0;
self.y2 = 0.0;
}
}
#[derive(Debug, Clone)]
struct KWeightingFilter {
pre_filter: Biquad,
highpass: Biquad,
}
impl KWeightingFilter {
fn new(sample_rate: f64) -> Self {
let fs = sample_rate;
let f0 = 1681.974450955533;
let db = 3.999_843_853_973_347;
let k_h = f64::tan(std::f64::consts::PI * f0 / fs);
let vh = f64::powf(10.0_f64, db / 20.0);
let vb = f64::powf(vh, 0.499_666_78);
let sq2 = std::f64::consts::SQRT_2;
let a0_pre = 1.0 + (sq2 / vb) * k_h + k_h * k_h;
let b0_pre = (vh + (sq2 * vb) * k_h + k_h * k_h) / a0_pre;
let b1_pre = (2.0 * (k_h * k_h - vh)) / a0_pre;
let b2_pre = (vh - (sq2 * vb) * k_h + k_h * k_h) / a0_pre;
let a1_pre = (2.0 * (k_h * k_h - 1.0)) / a0_pre;
let a2_pre = (1.0 - (sq2 / vb) * k_h + k_h * k_h) / a0_pre;
let f_hp = 38.134_496_945_404_14;
let k_hp = f64::tan(std::f64::consts::PI * f_hp / fs);
let sq2_hp = std::f64::consts::SQRT_2;
let a0_hp = 1.0 + sq2_hp * k_hp + k_hp * k_hp;
let b0_hp = 1.0 / a0_hp;
let b1_hp = -2.0 / a0_hp;
let b2_hp = 1.0 / a0_hp;
let a1_hp = (2.0 * (k_hp * k_hp - 1.0)) / a0_hp;
let a2_hp = (1.0 - sq2_hp * k_hp + k_hp * k_hp) / a0_hp;
Self {
pre_filter: Biquad::new(b0_pre, b1_pre, b2_pre, a1_pre, a2_pre),
highpass: Biquad::new(b0_hp, b1_hp, b2_hp, a1_hp, a2_hp),
}
}
#[inline]
fn process(&mut self, x: f64) -> f64 {
self.highpass.process(self.pre_filter.process(x))
}
fn reset(&mut self) {
self.pre_filter.reset();
self.highpass.reset();
}
}
#[derive(Debug, Clone)]
struct MsRingBuffer {
buf: Vec<f64>,
write_pos: usize,
capacity: usize,
sum: f64,
filled: bool,
}
impl MsRingBuffer {
fn new(capacity: usize) -> Self {
Self {
buf: vec![0.0; capacity],
write_pos: 0,
capacity,
sum: 0.0,
filled: false,
}
}
fn push(&mut self, sq: f64) -> f64 {
self.sum -= self.buf[self.write_pos];
self.buf[self.write_pos] = sq;
self.sum += sq;
let prev_pos = self.write_pos;
self.write_pos = (self.write_pos + 1) % self.capacity;
if prev_pos == self.capacity - 1 {
self.filled = true;
}
if self.filled {
(self.sum / self.capacity as f64).max(0.0)
} else {
let n = self.write_pos;
if n == 0 {
0.0
} else {
(self.sum / n as f64).max(0.0)
}
}
}
fn reset(&mut self) {
self.buf.fill(0.0);
self.write_pos = 0;
self.sum = 0.0;
self.filled = false;
}
}
#[derive(Debug, Clone)]
struct GatedAccumulator {
block_samples: usize,
hop_samples: usize,
channel_sums: Vec<f64>,
weights: Vec<f64>,
hop_counter: usize,
block_counter: usize,
block_loudnesses: Vec<f64>,
num_channels: usize,
}
impl GatedAccumulator {
fn new(sample_rate: f64, layout: ChannelLayout) -> Self {
let block_samples = (0.4 * sample_rate).round() as usize;
let hop_samples = (0.1 * sample_rate).round() as usize;
let num_channels = layout.channel_count();
let weights: Vec<f64> = (0..num_channels)
.map(|i| layout.channel_weight(i))
.collect();
Self {
block_samples,
hop_samples,
channel_sums: vec![0.0; num_channels],
weights,
hop_counter: 0,
block_counter: 0,
block_loudnesses: Vec::new(),
num_channels,
}
}
fn push_frame(&mut self, sq_per_channel: &[f64]) {
for (ch, &sq) in sq_per_channel.iter().enumerate().take(self.num_channels) {
self.channel_sums[ch] += sq;
}
self.block_counter += 1;
self.hop_counter += 1;
if self.hop_counter >= self.hop_samples {
self.hop_counter = 0;
}
if self.block_counter >= self.block_samples {
let mut z: f64 = 0.0;
for (ch, &w) in self.weights.iter().enumerate() {
z += w * self.channel_sums[ch] / self.block_samples as f64;
}
let loudness = if z > 1e-12 {
-0.691 + 10.0 * f64::log10(z)
} else {
f64::NEG_INFINITY
};
self.block_loudnesses.push(loudness);
let keep_ratio =
(self.block_samples - self.hop_samples) as f64 / self.block_samples as f64;
for sum in &mut self.channel_sums {
*sum *= keep_ratio;
}
self.block_counter = self.block_samples - self.hop_samples;
}
}
fn integrated_lufs(&self) -> f64 {
if self.block_loudnesses.is_empty() {
return f64::NEG_INFINITY;
}
let absolute_threshold = -70.0_f64;
let pass1: Vec<f64> = self
.block_loudnesses
.iter()
.copied()
.filter(|&l| l > absolute_threshold)
.collect();
if pass1.is_empty() {
return f64::NEG_INFINITY;
}
let n1 = pass1.len() as f64;
let mean_lin1: f64 = pass1
.iter()
.map(|&l| f64::powf(10.0, l / 10.0))
.sum::<f64>()
/ n1;
let mean_db1 = 10.0 * f64::log10(mean_lin1);
let relative_threshold = mean_db1 - 10.0;
let pass2: Vec<f64> = pass1
.iter()
.copied()
.filter(|&l| l > relative_threshold)
.collect();
if pass2.is_empty() {
return f64::NEG_INFINITY;
}
let n2 = pass2.len() as f64;
let mean_lin2: f64 = pass2
.iter()
.map(|&l| f64::powf(10.0, l / 10.0))
.sum::<f64>()
/ n2;
-0.691 + 10.0 * f64::log10(mean_lin2)
}
fn loudness_range(&self) -> f64 {
if self.block_loudnesses.len() < 2 {
return 0.0;
}
let abs_gate = -70.0_f64;
let st_gate = -20.0_f64;
let mut filtered: Vec<f64> = self
.block_loudnesses
.iter()
.copied()
.filter(|&l| l > abs_gate && l > st_gate)
.collect();
if filtered.len() < 2 {
return 0.0;
}
filtered.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = filtered.len();
let lo_idx = ((n as f64 * 0.10).round() as usize).min(n - 1);
let hi_idx = ((n as f64 * 0.95).round() as usize).min(n - 1);
(filtered[hi_idx] - filtered[lo_idx]).max(0.0)
}
fn reset(&mut self) {
self.channel_sums.fill(0.0);
self.hop_counter = 0;
self.block_counter = 0;
self.block_loudnesses.clear();
}
}
#[derive(Debug, Clone)]
struct SlidingLoudness {
ring_bufs: Vec<MsRingBuffer>,
weights: Vec<f64>,
num_channels: usize,
}
impl SlidingLoudness {
fn new(window_seconds: f64, sample_rate: f64, layout: ChannelLayout) -> Self {
let window_samples = (window_seconds * sample_rate).round() as usize;
let num_channels = layout.channel_count();
let weights = (0..num_channels)
.map(|i| layout.channel_weight(i))
.collect();
Self {
ring_bufs: vec![MsRingBuffer::new(window_samples); num_channels],
weights,
num_channels,
}
}
fn push_frame(&mut self, sq_per_channel: &[f64]) -> f64 {
let mut z = 0.0_f64;
for (ch, buf) in self
.ring_bufs
.iter_mut()
.enumerate()
.take(self.num_channels)
{
let sq = if ch < sq_per_channel.len() {
sq_per_channel[ch]
} else {
0.0
};
let ms = buf.push(sq);
z += self.weights[ch] * ms;
}
if z > 1e-12 {
-0.691 + 10.0 * f64::log10(z)
} else {
f64::NEG_INFINITY
}
}
fn reset(&mut self) {
for buf in &mut self.ring_bufs {
buf.reset();
}
}
}
#[derive(Debug, Clone)]
pub struct LufsMeter {
config: LufsMeterConfig,
kw_filters: Vec<KWeightingFilter>,
momentary_window: SlidingLoudness,
short_term_window: SlidingLoudness,
gated: GatedAccumulator,
last_momentary: f64,
last_short_term: f64,
true_peak: Vec<f32>,
num_channels: usize,
}
impl LufsMeter {
#[must_use]
pub fn new(config: LufsMeterConfig) -> Self {
let fs = f64::from(config.sample_rate);
let layout = config.layout;
let num_channels = layout.channel_count();
let kw_filters = (0..num_channels)
.map(|_| KWeightingFilter::new(fs))
.collect();
Self {
kw_filters,
momentary_window: SlidingLoudness::new(0.4, fs, layout),
short_term_window: SlidingLoudness::new(3.0, fs, layout),
gated: GatedAccumulator::new(fs, layout),
last_momentary: f64::NEG_INFINITY,
last_short_term: f64::NEG_INFINITY,
true_peak: vec![0.0_f32; num_channels],
num_channels,
config,
}
}
pub fn process_mono(&mut self, samples: &[f32]) {
let mut sq = [0.0_f64; 1];
for &s in samples {
let filtered = self.kw_filters[0].process(f64::from(s));
sq[0] = filtered * filtered;
self.last_momentary = self.momentary_window.push_frame(&sq);
self.last_short_term = self.short_term_window.push_frame(&sq);
self.gated.push_frame(&sq);
if s.abs() > self.true_peak[0] {
self.true_peak[0] = s.abs();
}
}
}
pub fn process_stereo(&mut self, left: &[f32], right: &[f32]) {
let len = left.len().min(right.len());
let mut sq = [0.0_f64; 2];
for i in 0..len {
let fl = self.kw_filters[0].process(f64::from(left[i]));
let fr = if self.num_channels > 1 {
self.kw_filters[1].process(f64::from(right[i]))
} else {
0.0
};
sq[0] = fl * fl;
sq[1] = fr * fr;
self.last_momentary = self.momentary_window.push_frame(&sq);
self.last_short_term = self.short_term_window.push_frame(&sq);
self.gated.push_frame(&sq);
if left[i].abs() > self.true_peak[0] {
self.true_peak[0] = left[i].abs();
}
if self.num_channels > 1 && right[i].abs() > self.true_peak[1] {
self.true_peak[1] = right[i].abs();
}
}
}
pub fn process_multichannel(&mut self, frames: &[Vec<f32>]) {
let mut sq = vec![0.0_f64; self.num_channels];
for frame in frames {
for (ch, filter) in self.kw_filters.iter_mut().enumerate() {
let s = frame.get(ch).copied().unwrap_or(0.0);
let filtered = filter.process(f64::from(s));
sq[ch] = filtered * filtered;
if s.abs() > self.true_peak[ch] {
self.true_peak[ch] = s.abs();
}
}
self.last_momentary = self.momentary_window.push_frame(&sq);
self.last_short_term = self.short_term_window.push_frame(&sq);
self.gated.push_frame(&sq);
}
}
#[must_use]
pub fn momentary_lufs(&self) -> f32 {
self.last_momentary as f32
}
#[must_use]
pub fn short_term_lufs(&self) -> f32 {
self.last_short_term as f32
}
#[must_use]
pub fn integrated_lufs(&self) -> f32 {
self.gated.integrated_lufs() as f32
}
#[must_use]
pub fn loudness_range(&self) -> f32 {
self.gated.loudness_range() as f32
}
#[must_use]
pub fn true_peak(&self) -> &[f32] {
&self.true_peak
}
#[must_use]
pub fn true_peak_db(&self) -> f32 {
let max_peak = self.true_peak.iter().copied().fold(0.0_f32, f32::max);
if max_peak > 1e-10 {
20.0 * max_peak.log10()
} else {
f32::NEG_INFINITY
}
}
pub fn reset(&mut self) {
for f in &mut self.kw_filters {
f.reset();
}
self.momentary_window.reset();
self.short_term_window.reset();
self.gated.reset();
self.last_momentary = f64::NEG_INFINITY;
self.last_short_term = f64::NEG_INFINITY;
self.true_peak.fill(0.0);
}
#[must_use]
pub fn config(&self) -> &LufsMeterConfig {
&self.config
}
#[must_use]
pub fn num_channels(&self) -> usize {
self.num_channels
}
}
#[must_use]
pub fn loudness_deviation(measured_lufs: f32, target_lufs: f32) -> f32 {
measured_lufs - target_lufs
}
#[must_use]
pub fn lufs_to_gain_correction(measured_lufs: f32, target_lufs: f32) -> f32 {
let diff_db = target_lufs - measured_lufs;
f32::powf(10.0, diff_db / 20.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_stereo_meter() -> LufsMeter {
LufsMeter::new(LufsMeterConfig {
sample_rate: 48_000.0,
layout: ChannelLayout::Stereo,
})
}
fn sine_signal(hz: f32, amplitude: f32, sample_rate: f32, duration_secs: f32) -> Vec<f32> {
let n = (sample_rate * duration_secs) as usize;
(0..n)
.map(|i| amplitude * (2.0 * std::f32::consts::PI * hz * i as f32 / sample_rate).sin())
.collect()
}
#[test]
fn test_meter_constructs_ok() {
let meter = make_stereo_meter();
assert_eq!(meter.num_channels(), 2);
}
#[test]
fn test_silence_gives_neg_inf_momentary() {
let mut meter = make_stereo_meter();
let silence = vec![0.0_f32; 4800];
meter.process_stereo(&silence, &silence);
let m = meter.momentary_lufs();
assert!(m == f32::NEG_INFINITY || m < -60.0, "got {m}");
}
#[test]
fn test_sine_tone_is_finite() {
let mut meter = make_stereo_meter();
let sig = sine_signal(1000.0, 0.5, 48_000.0, 1.0);
meter.process_stereo(&sig, &sig);
let m = meter.momentary_lufs();
assert!(
m.is_finite(),
"momentary LUFS should be finite for sine, got {m}"
);
}
#[test]
fn test_louder_sine_gives_higher_lufs() {
let mut low_meter = make_stereo_meter();
let mut high_meter = make_stereo_meter();
let low_sig = sine_signal(1000.0, 0.1, 48_000.0, 1.0);
let high_sig = sine_signal(1000.0, 0.9, 48_000.0, 1.0);
low_meter.process_stereo(&low_sig, &low_sig);
high_meter.process_stereo(&high_sig, &high_sig);
let low_m = low_meter.momentary_lufs();
let high_m = high_meter.momentary_lufs();
assert!(
high_m > low_m,
"louder signal should have higher LUFS: {high_m} vs {low_m}"
);
}
#[test]
fn test_true_peak_tracks_maximum() {
let mut meter = make_stereo_meter();
let signal = vec![0.0_f32, 0.5, -0.8, 0.3, 0.0];
meter.process_stereo(&signal, &signal);
let tp = meter.true_peak();
assert!(
(tp[0] - 0.8).abs() < 1e-5,
"true peak L should be 0.8, got {}",
tp[0]
);
assert!(
(tp[1] - 0.8).abs() < 1e-5,
"true peak R should be 0.8, got {}",
tp[1]
);
}
#[test]
fn test_true_peak_db_neg_inf_for_silence() {
let meter = make_stereo_meter();
let tp_db = meter.true_peak_db();
assert_eq!(tp_db, f32::NEG_INFINITY, "silence → NEG_INFINITY dBFS");
}
#[test]
fn test_reset_clears_state() {
let mut meter = make_stereo_meter();
let sig = sine_signal(1000.0, 0.5, 48_000.0, 1.0);
meter.process_stereo(&sig, &sig);
meter.reset();
assert_eq!(meter.momentary_lufs(), f32::NEG_INFINITY);
assert_eq!(meter.true_peak_db(), f32::NEG_INFINITY);
}
#[test]
fn test_mono_processing() {
let mut meter = LufsMeter::new(LufsMeterConfig {
sample_rate: 48_000.0,
layout: ChannelLayout::Mono,
});
let sig = sine_signal(440.0, 0.3, 48_000.0, 0.5);
meter.process_mono(&sig);
let m = meter.momentary_lufs();
assert!(m.is_finite() || m == f32::NEG_INFINITY);
}
#[test]
fn test_loudness_deviation() {
let dev = loudness_deviation(-18.0, -23.0);
assert!(
(dev - 5.0).abs() < 1e-5,
"deviation should be +5 LU, got {dev}"
);
}
#[test]
fn test_lufs_to_gain_correction_unity() {
let gain = lufs_to_gain_correction(-23.0, -23.0);
assert!((gain - 1.0).abs() < 1e-5, "unity gain expected, got {gain}");
}
#[test]
fn test_lufs_to_gain_correction_boost() {
let gain = lufs_to_gain_correction(-30.0, -23.0);
assert!(gain > 1.0, "gain should be >1 for boost, got {gain}");
}
#[test]
fn test_integrated_lufs_after_long_signal() {
let mut meter = make_stereo_meter();
let amp = f32::powf(10.0, -18.0 / 20.0);
let sig = sine_signal(1000.0, amp, 48_000.0, 6.0);
meter.process_stereo(&sig, &sig);
let integrated = meter.integrated_lufs();
assert!(
integrated.is_finite(),
"integrated LUFS should be finite after 6 s of tone, got {integrated}"
);
}
#[test]
fn test_channel_layout_weights() {
assert_eq!(ChannelLayout::Stereo.channel_weight(0), 1.0);
assert_eq!(ChannelLayout::Stereo.channel_weight(1), 1.0);
assert_eq!(ChannelLayout::Surround51.channel_weight(3), 0.0); assert_eq!(ChannelLayout::Surround51.channel_weight(4), 1.41); }
#[test]
fn test_channel_layout_counts() {
assert_eq!(ChannelLayout::Mono.channel_count(), 1);
assert_eq!(ChannelLayout::Stereo.channel_count(), 2);
assert_eq!(ChannelLayout::Surround50.channel_count(), 5);
assert_eq!(ChannelLayout::Surround51.channel_count(), 6);
}
#[test]
fn test_surround50_weights() {
for ch in 0..=2 {
assert_eq!(
ChannelLayout::Surround50.channel_weight(ch),
1.0,
"channel {ch} weight wrong"
);
}
assert_eq!(ChannelLayout::Surround50.channel_weight(3), 1.41);
assert_eq!(ChannelLayout::Surround50.channel_weight(4), 1.41);
}
#[test]
fn test_meter_short_term_lufs_after_signal() {
let mut meter = make_stereo_meter();
let amp = f32::powf(10.0, -18.0 / 20.0);
let sig = sine_signal(1000.0, amp, 48_000.0, 4.0);
meter.process_stereo(&sig, &sig);
let st = meter.short_term_lufs();
assert!(
st.is_finite(),
"short-term LUFS should be finite after 4 s tone, got {st}"
);
}
#[test]
fn test_meter_momentary_higher_than_silence() {
let mut silent = make_stereo_meter();
let mut loud = make_stereo_meter();
let silence = vec![0.0_f32; 19200]; let tone = sine_signal(1000.0, 0.5, 48_000.0, 0.4);
silent.process_stereo(&silence, &silence);
loud.process_stereo(&tone, &tone);
let m_silent = silent.momentary_lufs();
let m_loud = loud.momentary_lufs();
assert!(
m_loud > m_silent,
"loud ({m_loud}) should be greater than silent ({m_silent})"
);
}
#[test]
fn test_meter_loudness_deviation_negative() {
let dev = loudness_deviation(-30.0, -23.0);
assert!(
dev < 0.0,
"deviation should be negative when quieter: {dev}"
);
assert!((dev - (-7.0)).abs() < 1e-5, "expected -7 LU, got {dev}");
}
#[test]
fn test_meter_gain_correction_cut() {
let gain = lufs_to_gain_correction(-20.0, -23.0);
assert!(gain < 1.0, "gain should be < 1 for a cut, got {gain}");
}
#[test]
fn test_meter_true_peak_monotonically_increases() {
let mut meter = make_stereo_meter();
let mut last_peak = f32::NEG_INFINITY;
for amp in [0.1_f32, 0.3, 0.5, 0.7, 0.9] {
let sig = sine_signal(1000.0, amp, 48_000.0, 0.1);
meter.process_stereo(&sig, &sig);
let tp = meter.true_peak_db();
assert!(
tp >= last_peak - 0.01,
"true peak should not decrease: {tp} < {last_peak}"
);
if tp.is_finite() {
last_peak = tp;
}
}
}
#[test]
fn test_meter_process_stereo_then_reset() {
let mut meter = make_stereo_meter();
let sig = sine_signal(1000.0, 0.5, 48_000.0, 2.0);
meter.process_stereo(&sig, &sig);
let before = meter.momentary_lufs();
assert!(before.is_finite() || before == f32::NEG_INFINITY);
meter.reset();
assert_eq!(
meter.momentary_lufs(),
f32::NEG_INFINITY,
"momentary should be NEG_INFINITY after reset"
);
assert_eq!(
meter.integrated_lufs(),
f32::NEG_INFINITY,
"integrated should be NEG_INFINITY after reset"
);
}
#[test]
fn test_meter_config_getter() {
let config = LufsMeterConfig {
sample_rate: 44_100.0,
layout: ChannelLayout::Mono,
};
let meter = LufsMeter::new(config.clone());
assert_eq!(meter.config().sample_rate, 44_100.0);
assert_eq!(meter.config().layout, ChannelLayout::Mono);
}
}