use crate::{MeteringError, MeteringResult};
pub const SHORT_FORM_THRESHOLD_SECS: f64 = 30.0;
pub const SHORT_FORM_RELATIVE_GATE_LU: f64 = -20.0;
pub const STANDARD_RELATIVE_GATE_LU: f64 = -10.0;
pub const ABSOLUTE_GATE_LKFS: f64 = -70.0;
pub const BLOCK_DURATION_SECS: f64 = 0.4;
#[derive(Debug, Clone, Copy)]
pub struct LoudnessBlock {
pub mean_square: f64,
pub lkfs: f64,
}
impl LoudnessBlock {
#[must_use]
pub fn from_mean_square(mean_square: f64) -> Option<Self> {
if mean_square <= 0.0 {
return None;
}
Some(Self {
mean_square,
lkfs: 10.0 * mean_square.log10(),
})
}
}
#[derive(Debug, Clone)]
pub struct Bs2132Config {
pub sample_rate: f64,
pub channels: usize,
pub content_duration_secs: f64,
}
impl Bs2132Config {
#[must_use]
pub fn new(sample_rate: f64, channels: usize, content_duration_secs: f64) -> Self {
Self {
sample_rate,
channels,
content_duration_secs,
}
}
#[must_use]
pub fn is_short_form(&self) -> bool {
self.content_duration_secs < SHORT_FORM_THRESHOLD_SECS
}
#[must_use]
pub fn relative_gate_lu(&self) -> f64 {
if self.is_short_form() {
SHORT_FORM_RELATIVE_GATE_LU
} else {
STANDARD_RELATIVE_GATE_LU
}
}
}
#[derive(Debug, Clone)]
pub struct Bs2132Result {
pub integrated_lufs: f64,
pub blocks_absolute: usize,
pub blocks_relative: usize,
pub short_form_applied: bool,
pub valid: bool,
}
pub fn compute_bs2132_loudness(
blocks: &[f64],
config: &Bs2132Config,
) -> MeteringResult<Bs2132Result> {
if config.channels == 0 {
return Err(MeteringError::InvalidConfig(
"Channel count must be > 0".to_string(),
));
}
if blocks.is_empty() {
return Ok(Bs2132Result {
integrated_lufs: f64::NEG_INFINITY,
blocks_absolute: 0,
blocks_relative: 0,
short_form_applied: config.is_short_form(),
valid: false,
});
}
let abs_gate = ABSOLUTE_GATE_LKFS;
let loudness_blocks: Vec<LoudnessBlock> = blocks
.iter()
.filter_map(|&ms| LoudnessBlock::from_mean_square(ms))
.filter(|b| b.lkfs >= abs_gate)
.collect();
if loudness_blocks.is_empty() {
return Ok(Bs2132Result {
integrated_lufs: f64::NEG_INFINITY,
blocks_absolute: 0,
blocks_relative: 0,
short_form_applied: config.is_short_form(),
valid: false,
});
}
let n_abs = loudness_blocks.len();
let mean_abs: f64 = loudness_blocks.iter().map(|b| b.mean_square).sum::<f64>() / n_abs as f64;
let lufs_ungated = 10.0 * mean_abs.log10() - 0.691;
if n_abs < 3 && config.is_short_form() {
return Ok(Bs2132Result {
integrated_lufs: lufs_ungated,
blocks_absolute: n_abs,
blocks_relative: n_abs,
short_form_applied: true,
valid: true,
});
}
let rel_gate = lufs_ungated + 0.691 + config.relative_gate_lu();
let gated_blocks: Vec<&LoudnessBlock> = loudness_blocks
.iter()
.filter(|b| b.lkfs >= rel_gate)
.collect();
let n_rel = gated_blocks.len();
if n_rel == 0 {
if config.is_short_form() {
return Ok(Bs2132Result {
integrated_lufs: lufs_ungated,
blocks_absolute: n_abs,
blocks_relative: 0,
short_form_applied: true,
valid: true,
});
}
return Ok(Bs2132Result {
integrated_lufs: f64::NEG_INFINITY,
blocks_absolute: n_abs,
blocks_relative: 0,
short_form_applied: false,
valid: false,
});
}
let mean_gated: f64 = gated_blocks.iter().map(|b| b.mean_square).sum::<f64>() / n_rel as f64;
let integrated = 10.0 * mean_gated.log10() - 0.691;
Ok(Bs2132Result {
integrated_lufs: integrated,
blocks_absolute: n_abs,
blocks_relative: n_rel,
short_form_applied: config.is_short_form(),
valid: true,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_blocks_at_lufs(lufs: f64, count: usize) -> Vec<f64> {
let ms = 10.0_f64.powf((lufs + 0.691) / 10.0);
vec![ms; count]
}
#[test]
fn test_short_form_flag() {
let short = Bs2132Config::new(48000.0, 2, 10.0);
assert!(short.is_short_form());
assert_eq!(short.relative_gate_lu(), SHORT_FORM_RELATIVE_GATE_LU);
let long = Bs2132Config::new(48000.0, 2, 60.0);
assert!(!long.is_short_form());
assert_eq!(long.relative_gate_lu(), STANDARD_RELATIVE_GATE_LU);
}
#[test]
fn test_empty_blocks() {
let config = Bs2132Config::new(48000.0, 2, 5.0);
let result = compute_bs2132_loudness(&[], &config).expect("should return empty result");
assert!(!result.valid);
assert!(result.integrated_lufs.is_infinite());
}
#[test]
fn test_zero_channels_error() {
let config = Bs2132Config::new(48000.0, 0, 5.0);
let blocks = make_blocks_at_lufs(-23.0, 10);
assert!(compute_bs2132_loudness(&blocks, &config).is_err());
}
#[test]
fn test_short_form_known_loudness() {
let config = Bs2132Config::new(48000.0, 2, 5.0);
let blocks = make_blocks_at_lufs(-23.0, 10);
let result = compute_bs2132_loudness(&blocks, &config).expect("should succeed");
assert!(result.valid);
assert!(result.short_form_applied);
assert!(
(result.integrated_lufs - (-23.0)).abs() < 0.5,
"Expected ≈ -23 LUFS, got {:.2}",
result.integrated_lufs
);
}
#[test]
fn test_short_form_fewer_than_3_blocks_skips_relative_gate() {
let config = Bs2132Config::new(48000.0, 2, 5.0);
let blocks = make_blocks_at_lufs(-20.0, 2);
let result = compute_bs2132_loudness(&blocks, &config).expect("should succeed");
assert!(result.valid);
assert!(result.short_form_applied);
assert_eq!(result.blocks_relative, result.blocks_absolute);
}
#[test]
fn test_long_form_uses_standard_relative_gate() {
let config = Bs2132Config::new(48000.0, 2, 60.0);
let blocks = make_blocks_at_lufs(-23.0, 30);
let result = compute_bs2132_loudness(&blocks, &config).expect("should succeed");
assert!(result.valid);
assert!(!result.short_form_applied);
}
#[test]
fn test_block_from_mean_square_silent() {
assert!(LoudnessBlock::from_mean_square(0.0).is_none());
assert!(LoudnessBlock::from_mean_square(-1.0).is_none());
}
#[test]
fn test_block_lkfs_calculation() {
let b = LoudnessBlock::from_mean_square(1.0).expect("should succeed");
assert!((b.lkfs - 0.0).abs() < 1e-10);
let b2 = LoudnessBlock::from_mean_square(0.1).expect("should succeed");
assert!((b2.lkfs - (-10.0)).abs() < 1e-10);
}
}