const LOUDNESS_OFFSET: f64 = -0.691;
const ABSOLUTE_GATE_LUFS: f64 = -70.0;
const RELATIVE_GATE_FACTOR: f64 = 0.1;
fn energy_to_loudness(energy: f64) -> f64 {
LOUDNESS_OFFSET + 10.0 * energy.log10()
}
fn loudness_to_energy(lufs: f64) -> f64 {
10f64.powf((lufs - LOUDNESS_OFFSET) / 10.0)
}
#[derive(Clone)]
struct Biquad {
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
z1: f64,
z2: f64,
}
impl Biquad {
fn new(coeffs: (f64, f64, f64, f64, f64)) -> Self {
let (b0, b1, b2, a1, a2) = coeffs;
Self {
b0,
b1,
b2,
a1,
a2,
z1: 0.0,
z2: 0.0,
}
}
#[inline]
fn process(&mut self, x: f64) -> f64 {
let y = self.b0 * x + self.z1;
self.z1 = self.b1 * x - self.a1 * y + self.z2;
self.z2 = self.b2 * x - self.a2 * y;
y
}
}
fn shelf_coefficients(sample_rate: f64) -> (f64, f64, f64, f64, f64) {
let f0 = 1681.974450955533;
let gain_db = 3.999843853973347;
let q = 0.7071752369554196;
let k = (std::f64::consts::PI * f0 / sample_rate).tan();
let vh = 10f64.powf(gain_db / 20.0);
let vb = vh.powf(0.4996667741545416);
let a0 = 1.0 + k / q + k * k;
(
(vh + vb * k / q + k * k) / a0,
2.0 * (k * k - vh) / a0,
(vh - vb * k / q + k * k) / a0,
2.0 * (k * k - 1.0) / a0,
(1.0 - k / q + k * k) / a0,
)
}
fn highpass_coefficients(sample_rate: f64) -> (f64, f64, f64, f64, f64) {
let f0 = 38.13547087602444;
let q = 0.5003270373238773;
let k = (std::f64::consts::PI * f0 / sample_rate).tan();
let a0 = 1.0 + k / q + k * k;
(
1.0,
-2.0,
1.0,
2.0 * (k * k - 1.0) / a0,
(1.0 - k / q + k * k) / a0,
)
}
#[derive(Clone)]
struct KWeightingFilter {
shelf: Biquad,
highpass: Biquad,
}
impl KWeightingFilter {
fn new(sample_rate: u32) -> Self {
let fs = sample_rate as f64;
Self {
shelf: Biquad::new(shelf_coefficients(fs)),
highpass: Biquad::new(highpass_coefficients(fs)),
}
}
#[inline]
fn process(&mut self, sample: f64) -> f64 {
self.highpass.process(self.shelf.process(sample))
}
}
fn channel_weights(channels: usize) -> Vec<f64> {
match channels {
4 => vec![1.0, 1.0, 1.41, 1.41],
5 => vec![1.0, 1.0, 1.0, 1.41, 1.41],
6 => vec![1.0, 1.0, 1.0, 0.0, 1.41, 1.41],
n => vec![1.0; n],
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BlockEnergies {
energies: Vec<f64>,
}
impl BlockEnergies {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.energies.len()
}
pub fn is_empty(&self) -> bool {
self.energies.is_empty()
}
pub fn accumulate(&mut self, other: &BlockEnergies) {
self.energies.extend_from_slice(&other.energies);
}
pub fn integrated_lufs(&self) -> f64 {
let absolute_gate = loudness_to_energy(ABSOLUTE_GATE_LUFS);
let Some(ungated_mean) = gated_mean(&self.energies, absolute_gate) else {
return f64::NEG_INFINITY;
};
let gate = absolute_gate.max(ungated_mean * RELATIVE_GATE_FACTOR);
match gated_mean(&self.energies, gate) {
Some(mean) => energy_to_loudness(mean),
None => f64::NEG_INFINITY,
}
}
}
fn gated_mean(energies: &[f64], gate: f64) -> Option<f64> {
let mut sum = 0.0;
let mut count = 0usize;
for &e in energies {
if e > gate {
sum += e;
count += 1;
}
}
(count > 0).then(|| sum / count as f64)
}
pub struct TruePeakMeter {
phases: Vec<Vec<f64>>,
history: Vec<Vec<f64>>,
pos: usize,
peak: f64,
}
impl TruePeakMeter {
const TAPS: usize = 49;
pub fn new(sample_rate: u32, channels: usize) -> Self {
let factor: usize = if sample_rate >= 88_200 { 2 } else { 4 };
let mut phases = vec![Vec::new(); factor];
for j in 0..Self::TAPS {
let m = j as f64 - (Self::TAPS - 1) as f64 / 2.0;
let sinc = if m.abs() > 1e-9 {
let x = m * std::f64::consts::PI / factor as f64;
x.sin() / x
} else {
1.0
};
let window = 0.5
* (1.0 - (2.0 * std::f64::consts::PI * j as f64 / (Self::TAPS - 1) as f64).cos());
phases[j % factor].push(sinc * window);
}
let history_len = phases.iter().map(Vec::len).max().unwrap_or(0);
Self {
phases,
history: vec![vec![0.0; history_len]; channels.max(1)],
pos: 0,
peak: 0.0,
}
}
#[inline]
pub fn add_frame(&mut self, frame: &[f64]) {
let len = self.history.first().map_or(0, Vec::len);
if len == 0 {
return;
}
self.pos = (self.pos + len - 1) % len;
let pos = self.pos;
for (history, &sample) in self.history.iter_mut().zip(frame) {
history[pos] = sample;
for phase in &self.phases {
let mut acc = 0.0;
for (k, &c) in phase.iter().enumerate() {
let idx = pos + k;
let idx = if idx >= len { idx - len } else { idx };
acc += c * history[idx];
}
self.peak = self.peak.max(acc.abs());
}
}
}
pub fn peak(&self) -> f64 {
self.peak
}
}
pub struct Bs1770Analyzer {
filters: Vec<KWeightingFilter>,
weights: Vec<f64>,
true_peak: Option<TruePeakMeter>,
subblock_len: usize,
subblock_sum: f64,
subblock_samples: usize,
recent: [f64; 3],
recent_len: usize,
blocks: BlockEnergies,
}
impl Bs1770Analyzer {
pub fn new(sample_rate: u32, channels: usize) -> Self {
let channels = channels.max(1);
Self {
filters: vec![KWeightingFilter::new(sample_rate); channels],
weights: channel_weights(channels),
true_peak: None,
subblock_len: (sample_rate as usize + 5) / 10,
subblock_sum: 0.0,
subblock_samples: 0,
recent: [0.0; 3],
recent_len: 0,
blocks: BlockEnergies::new(),
}
}
pub fn new_with_true_peak(sample_rate: u32, channels: usize) -> Self {
let mut analyzer = Self::new(sample_rate, channels);
analyzer.true_peak = Some(TruePeakMeter::new(sample_rate, channels.max(1)));
analyzer
}
pub fn true_peak(&self) -> Option<f64> {
self.true_peak.as_ref().map(TruePeakMeter::peak)
}
#[inline]
pub fn add_frame(&mut self, frame: &[f64]) {
if let Some(meter) = &mut self.true_peak {
meter.add_frame(frame);
}
let mut acc = 0.0;
for ((filter, &weight), &sample) in self.filters.iter_mut().zip(&self.weights).zip(frame) {
let y = filter.process(sample);
acc += weight * y * y;
}
self.subblock_sum += acc;
self.subblock_samples += 1;
if self.subblock_samples >= self.subblock_len {
self.finish_subblock();
}
}
fn finish_subblock(&mut self) {
let sum = self.subblock_sum;
if self.recent_len == 3 {
let block_sum = self.recent.iter().sum::<f64>() + sum;
self.blocks
.energies
.push(block_sum / (4 * self.subblock_len) as f64);
self.recent.rotate_left(1);
self.recent[2] = sum;
} else {
self.recent[self.recent_len] = sum;
self.recent_len += 1;
}
self.subblock_sum = 0.0;
self.subblock_samples = 0;
}
pub fn into_blocks(self) -> BlockEnergies {
self.blocks
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOLERANCE: f64 = 0.1;
#[test]
fn coefficients_match_bs1770_reference_at_48khz() {
let (b0, b1, b2, a1, a2) = shelf_coefficients(48000.0);
assert!((b0 - 1.53512485958697).abs() < 1e-6);
assert!((b1 - -2.69169618940638).abs() < 1e-6);
assert!((b2 - 1.19839281085285).abs() < 1e-6);
assert!((a1 - -1.69065929318241).abs() < 1e-6);
assert!((a2 - 0.73248077421585).abs() < 1e-6);
let (b0, b1, b2, a1, a2) = highpass_coefficients(48000.0);
assert_eq!(b0, 1.0);
assert_eq!(b1, -2.0);
assert_eq!(b2, 1.0);
assert!((a1 - -1.99004745483398).abs() < 1e-5);
assert!((a2 - 0.99007225036621).abs() < 1e-5);
}
fn append_sine(samples: &mut Vec<f64>, level_dbfs: f64, seconds: f64, sample_rate: u32) {
let amplitude = crate::gain::db_to_linear(level_dbfs);
let count = (seconds * sample_rate as f64) as usize;
let step = 2.0 * std::f64::consts::PI * 997.0 / sample_rate as f64;
for n in 0..count {
samples.push(amplitude * (step * n as f64).sin());
}
}
fn integrated_stereo(samples: &[f64], sample_rate: u32) -> f64 {
let mut analyzer = Bs1770Analyzer::new(sample_rate, 2);
for &s in samples {
analyzer.add_frame(&[s, s]);
}
analyzer.into_blocks().integrated_lufs()
}
#[test]
fn tech3341_case1_minus23_sine() {
for &rate in &[48000u32, 44100] {
let mut samples = Vec::new();
append_sine(&mut samples, -23.0, 20.0, rate);
let lufs = integrated_stereo(&samples, rate);
assert!(
(lufs - -23.0).abs() < TOLERANCE,
"expected -23 LUFS at {} Hz, got {:.3}",
rate,
lufs
);
}
}
#[test]
fn tech3341_case2_minus33_sine() {
let mut samples = Vec::new();
append_sine(&mut samples, -33.0, 20.0, 48000);
let lufs = integrated_stereo(&samples, 48000);
assert!((lufs - -33.0).abs() < TOLERANCE, "got {:.3}", lufs);
}
#[test]
fn relative_gate_excludes_quiet_passages() {
let mut samples = Vec::new();
append_sine(&mut samples, -36.0, 2.5, 48000);
append_sine(&mut samples, -23.0, 15.0, 48000);
append_sine(&mut samples, -36.0, 2.5, 48000);
let lufs = integrated_stereo(&samples, 48000);
assert!((lufs - -23.0).abs() < TOLERANCE, "got {:.3}", lufs);
}
#[test]
fn absolute_gate_excludes_silence() {
let mut samples = vec![0.0; 5 * 48000];
append_sine(&mut samples, -23.0, 20.0, 48000);
samples.extend(std::iter::repeat_n(0.0, 5 * 48000));
let lufs = integrated_stereo(&samples, 48000);
assert!((lufs - -23.0).abs() < TOLERANCE, "got {:.3}", lufs);
}
#[test]
fn single_channel_reads_3db_below_stereo() {
let mut samples = Vec::new();
append_sine(&mut samples, -23.0, 20.0, 48000);
let mut analyzer = Bs1770Analyzer::new(48000, 2);
for &s in &samples {
analyzer.add_frame(&[s, 0.0]);
}
let lufs = analyzer.into_blocks().integrated_lufs();
assert!((lufs - -26.01).abs() < TOLERANCE, "got {:.3}", lufs);
}
#[test]
fn silence_has_no_measurable_loudness() {
let samples = vec![0.0; 10 * 48000];
let lufs = integrated_stereo(&samples, 48000);
assert!(lufs.is_infinite() && lufs < 0.0);
}
#[test]
fn accumulate_merges_tracks_like_concatenation() {
let mut a = Vec::new();
append_sine(&mut a, -23.0, 20.0, 48000);
let mut b = Vec::new();
append_sine(&mut b, -33.0, 20.0, 48000);
let mut analyzer_a = Bs1770Analyzer::new(48000, 2);
for &s in &a {
analyzer_a.add_frame(&[s, s]);
}
let mut analyzer_b = Bs1770Analyzer::new(48000, 2);
for &s in &b {
analyzer_b.add_frame(&[s, s]);
}
let mut album = analyzer_a.into_blocks();
album.accumulate(&analyzer_b.into_blocks());
let expected = -23.0 + 10.0 * (1.1f64 / 2.0).log10();
let lufs = album.integrated_lufs();
assert!(
(lufs - expected).abs() < TOLERANCE,
"expected {:.3}, got {:.3}",
expected,
lufs
);
}
#[test]
fn odd_sample_rate_11025() {
let mut samples = Vec::new();
append_sine(&mut samples, -23.0, 20.0, 11025);
let lufs = integrated_stereo(&samples, 11025);
assert!((lufs - -23.0).abs() < 0.2, "got {:.3}", lufs);
}
#[test]
fn true_peak_recovers_intersample_peak() {
for &rate in &[44100u32, 48000] {
let mut meter = TruePeakMeter::new(rate, 1);
let mut sample_peak = 0.0f64;
let step = 2.0 * std::f64::consts::PI / 4.0; for n in 0..rate as usize {
let s = (step * n as f64 + std::f64::consts::PI / 4.0).sin();
sample_peak = sample_peak.max(s.abs());
meter.add_frame(&[s]);
}
assert!((sample_peak - 0.707).abs() < 0.01);
let tp_db = 20.0 * meter.peak().log10();
assert!(
tp_db > -0.4 && tp_db < 0.2,
"expected ~0 dBTP at {} Hz, got {:.3} dBTP",
rate,
tp_db
);
}
}
#[test]
fn true_peak_never_below_sample_peak() {
let mut meter = TruePeakMeter::new(48000, 2);
let mut sample_peak = 0.0f64;
let mut x = 0.123f64;
for _ in 0..48000 {
x = (x * 997.0).sin();
sample_peak = sample_peak.max(x.abs());
meter.add_frame(&[x, -x]);
}
assert!(meter.peak() >= sample_peak - 1e-12);
}
#[test]
fn true_peak_2x_at_high_rates() {
let mut meter = TruePeakMeter::new(96000, 1);
let mut samples = Vec::new();
append_sine(&mut samples, -6.0, 1.0, 96000);
for &s in &samples {
meter.add_frame(&[s]);
}
let expected = crate::gain::db_to_linear(-6.0);
assert!((meter.peak() - expected).abs() / expected < 0.01);
}
#[test]
fn true_peak_silence_is_zero() {
let mut meter = TruePeakMeter::new(44100, 2);
for _ in 0..44100 {
meter.add_frame(&[0.0, 0.0]);
}
assert_eq!(meter.peak(), 0.0);
}
}