#![allow(dead_code)]
use crate::math::dsp::fft::FftPlanner;
pub const A2ESR_A1_STANDARD_MEDIAN: f64 = 0.00623;
pub const A2ESR_A2_FULL_MEDIAN: f64 = 0.00334;
pub const A2ESR_A2_LITE_MEDIAN: f64 = 0.005;
pub const A2ESR_A2_LITE_Q1: f64 = 0.0015;
pub const A2ESR_A2_LITE_Q3: f64 = 0.012;
pub fn compute_esr(reference: &[f32], test: &[f32]) -> f64 {
assert_eq!(
reference.len(),
test.len(),
"compute_esr: vectors must have same length"
);
let mut signal_power = 0.0f64;
let mut noise_power = 0.0f64;
for (&r, &t) in reference.iter().zip(test.iter()) {
let d = r as f64 - t as f64;
signal_power += (r as f64) * (r as f64);
noise_power += d * d;
}
if signal_power <= f64::EPSILON {
if noise_power <= f64::EPSILON {
return 0.0;
}
return f64::INFINITY;
}
noise_power / signal_power
}
pub fn compute_esr_blockwise(reference: &[f32], test: &[f32], block_size: usize) -> Vec<f64> {
assert_eq!(
reference.len(),
test.len(),
"compute_esr_blockwise: vectors must have same length"
);
assert!(
block_size > 0,
"compute_esr_blockwise: block_size must be greater than zero"
);
reference
.chunks(block_size)
.zip(test.chunks(block_size))
.map(|(ref_chunk, test_chunk)| compute_esr(ref_chunk, test_chunk))
.collect()
}
pub fn esr_to_db(esr: f64) -> f64 {
if esr <= f64::EPSILON {
f64::NEG_INFINITY
} else {
10.0 * esr.log10()
}
}
pub fn compute_snr_db(reference: &[f32], test: &[f32]) -> f64 {
assert_eq!(
reference.len(),
test.len(),
"compute_snr_db: vectors must have same length"
);
let mut signal_power = 0.0f64;
let mut noise_power = 0.0f64;
for (&r, &t) in reference.iter().zip(test.iter()) {
let d = r as f64 - t as f64;
signal_power += (r as f64) * (r as f64);
noise_power += d * d;
}
if noise_power <= f64::EPSILON {
return f64::INFINITY;
}
10.0 * (signal_power / noise_power).log10()
}
pub const MRSTFT_WINDOW_SIZES: [usize; 3] = [256, 1024, 4096];
pub const MRSTFT_WEIGHTS: [f64; 3] = [0.1, 0.3, 0.5];
pub fn compute_mr_stft(reference: &[f32], test: &[f32]) -> f64 {
assert_eq!(
reference.len(),
test.len(),
"compute_mr_stft: vectors must have same length"
);
if reference.is_empty() {
return 0.0;
}
let eps_abs = 1e-8f64;
let mut total_loss = 0.0f64;
for (&ws, &weight) in MRSTFT_WINDOW_SIZES.iter().zip(MRSTFT_WEIGHTS.iter()) {
let hop = ws / 4;
if ws > reference.len() {
continue;
}
let fft = FftPlanner::<f64>::new(ws);
let window: Vec<f64> = (0..ws)
.map(|n| 0.5 * (1.0 - (2.0 * std::f64::consts::PI * n as f64 / (ws - 1) as f64).cos()))
.collect();
let num_frames = (reference.len() - ws) / hop + 1;
if num_frames == 0 {
continue;
}
let num_bins = ws / 2 + 1;
let mut buf_ref_re = vec![0.0f64; ws];
let mut buf_ref_im = vec![0.0f64; ws];
let mut buf_test_re = vec![0.0f64; ws];
let mut buf_test_im = vec![0.0f64; ws];
let mut mag_ref = vec![0.0f64; num_bins];
let mut mag_test = vec![0.0f64; num_bins];
let mut window_loss_sum = 0.0f64;
for frame in 0..num_frames {
let offset = frame * hop;
for i in 0..ws {
buf_ref_re[i] = reference[offset + i] as f64 * window[i];
buf_test_re[i] = test[offset + i] as f64 * window[i];
buf_ref_im[i] = 0.0;
buf_test_im[i] = 0.0;
}
fft.process(&mut buf_ref_re, &mut buf_ref_im);
fft.process(&mut buf_test_re, &mut buf_test_im);
let mut frame_peak = 0.0f64;
for i in 0..num_bins {
mag_ref[i] = (buf_ref_re[i] * buf_ref_re[i] + buf_ref_im[i] * buf_ref_im[i]).sqrt();
mag_test[i] =
(buf_test_re[i] * buf_test_re[i] + buf_test_im[i] * buf_test_im[i]).sqrt();
frame_peak = frame_peak.max(mag_ref[i]).max(mag_test[i]);
}
let eps_frame = if frame_peak > eps_abs {
frame_peak * 1e-4 } else {
eps_abs
};
for i in 0..num_bins {
mag_ref[i] = mag_ref[i].max(eps_frame).ln();
mag_test[i] = mag_test[i].max(eps_frame).ln();
}
let mut l1 = 0.0f64;
let mut l2_sq = 0.0f64;
for i in 0..num_bins {
let diff = (mag_ref[i] - mag_test[i]).abs();
l1 += diff;
l2_sq += diff * diff;
}
let l1_sc = l1 / num_bins as f64;
let l2_sc = (l2_sq / num_bins as f64).sqrt();
window_loss_sum += l1_sc + l2_sc;
}
let window_loss = window_loss_sum / num_frames as f64;
total_loss += weight * window_loss;
}
total_loss
}
const KW_PRE_B: (f64, f64, f64) = (1.0, -2.0, 1.0);
const KW_PRE_A: (f64, f64) = (1.99004745483398, -0.99007225036621);
const KW_SHELF_B: (f64, f64, f64) = (1.53512485958697, -2.69169618940638, 1.19839281085285);
const KW_SHELF_A: (f64, f64) = (1.69065929318241, -0.73248077421585);
pub(crate) fn apply_k_weighting(samples: &[f32]) -> Vec<f32> {
let pre_filtered = apply_biquad(
samples, KW_PRE_B.0, KW_PRE_B.1, KW_PRE_B.2, KW_PRE_A.0, KW_PRE_A.1,
);
apply_biquad(
&pre_filtered,
KW_SHELF_B.0,
KW_SHELF_B.1,
KW_SHELF_B.2,
KW_SHELF_A.0,
KW_SHELF_A.1,
)
}
const LUFS_BLOCK_MS: u64 = 400;
const LUFS_OVERLAP_NUM: u64 = 3;
const LUFS_OVERLAP_DEN: u64 = 4;
const LUFS_ABS_GATE: f64 = -70.0;
const LUFS_REL_GATE: f64 = -10.0;
const LRA_BLOCK_MS: u64 = 3000;
const LRA_REL_GATE: f64 = -20.0;
const LUFS_OFFSET: f64 = -0.691;
#[inline]
fn power_to_lkfs(power: f64) -> f64 {
if power <= f64::EPSILON {
f64::NEG_INFINITY
} else {
LUFS_OFFSET + 10.0 * power.log10()
}
}
#[inline]
fn lkfs_to_power(lkfs: f64) -> f64 {
10.0f64.powf((lkfs - LUFS_OFFSET) / 10.0)
}
fn compute_block_powers(
k_weighted: &[f32],
sample_rate: u32,
block_ms: u64,
overlap_num: u64,
overlap_den: u64,
) -> (Vec<f64>, usize) {
let block_samples = (sample_rate as usize * block_ms as usize) / 1000;
let hop = block_samples * (overlap_den - overlap_num) as usize / overlap_den as usize;
if block_samples > k_weighted.len() || hop == 0 {
return (Vec::new(), hop);
}
let num_blocks = (k_weighted.len() - block_samples) / hop + 1;
let mut powers = Vec::with_capacity(num_blocks);
for b in 0..num_blocks {
let start = b * hop;
let sum_sq: f64 = k_weighted[start..start + block_samples]
.iter()
.map(|&x| (x as f64).powi(2))
.sum();
powers.push(sum_sq / block_samples as f64);
}
(powers, hop)
}
pub fn compute_integrated_lufs(samples: &[f32], sample_rate: u32) -> f64 {
if samples.is_empty() {
return f64::NEG_INFINITY;
}
let k_weighted = apply_k_weighting(samples);
let (block_powers, _hop) = compute_block_powers(
&k_weighted,
sample_rate,
LUFS_BLOCK_MS,
LUFS_OVERLAP_NUM,
LUFS_OVERLAP_DEN,
);
if block_powers.is_empty() {
return f64::NEG_INFINITY;
}
let block_lkfs: Vec<f64> = block_powers.iter().map(|&p| power_to_lkfs(p)).collect();
let gated_1: Vec<f64> = (0..block_powers.len())
.filter(|&i| block_lkfs[i] > LUFS_ABS_GATE)
.map(|i| block_powers[i])
.collect();
if gated_1.is_empty() {
return f64::NEG_INFINITY;
}
let mean_power_1: f64 = gated_1.iter().sum::<f64>() / gated_1.len() as f64;
let ungated_lkfs = power_to_lkfs(mean_power_1);
let rel_threshold_lk = ungated_lkfs + LUFS_REL_GATE;
let gated_2: Vec<f64> = (0..block_powers.len())
.filter(|&i| block_lkfs[i] > LUFS_ABS_GATE && block_lkfs[i] > rel_threshold_lk)
.map(|i| block_powers[i])
.collect();
if gated_2.is_empty() {
return ungated_lkfs;
}
let mean_power_2: f64 = gated_2.iter().sum::<f64>() / gated_2.len() as f64;
power_to_lkfs(mean_power_2)
}
#[inline]
pub fn compute_lufs(samples: &[f32], sample_rate: u32) -> f64 {
compute_integrated_lufs(samples, sample_rate)
}
pub fn compute_lra(samples: &[f32], sample_rate: u32) -> f64 {
if samples.is_empty() {
return 0.0;
}
let k_weighted = apply_k_weighting(samples);
let block_samples = (sample_rate as usize * LRA_BLOCK_MS as usize) / 1000;
if block_samples > k_weighted.len() {
return 0.0;
}
let num_blocks = (k_weighted.len() - block_samples) / block_samples + 1;
let hop = block_samples;
let mut short_term_lk: Vec<f64> = Vec::with_capacity(num_blocks);
for b in 0..num_blocks {
let start = b * hop;
let sum_sq: f64 = k_weighted[start..start + block_samples]
.iter()
.map(|&x| (x as f64).powi(2))
.sum();
let power = sum_sq / block_samples as f64;
short_term_lk.push(power_to_lkfs(power));
}
let abs_gated: Vec<f64> = short_term_lk
.iter()
.filter(|&&lk| lk > LUFS_ABS_GATE && lk.is_finite())
.copied()
.collect();
if abs_gated.len() < 2 {
return 0.0;
}
let l_asg = abs_gated.iter().sum::<f64>() / abs_gated.len() as f64;
let rel_threshold = l_asg + LRA_REL_GATE;
let mut gated: Vec<f64> = abs_gated
.into_iter()
.filter(|&lk| lk > rel_threshold)
.collect();
if gated.len() < 2 {
return 0.0;
}
gated.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let p10 = percentile_by_interpolation(&gated, 10.0);
let p95 = percentile_by_interpolation(&gated, 95.0);
p95 - p10
}
fn percentile_by_interpolation(sorted: &[f64], pct: f64) -> f64 {
let n = sorted.len();
if n == 0 {
return 0.0;
}
if n == 1 {
return sorted[0];
}
let rank = (pct / 100.0) * (n - 1) as f64;
let lo = rank.floor() as usize;
let hi = rank.ceil() as usize;
if hi >= n {
return sorted[n - 1];
}
let frac = rank - lo as f64;
sorted[lo] + frac * (sorted[hi] - sorted[lo])
}
pub fn short_term_loudness(samples: &[f32], sample_rate: u32) -> Vec<f64> {
if samples.is_empty() {
return Vec::new();
}
let k_weighted = apply_k_weighting(samples);
let block_samples = (sample_rate as usize * LRA_BLOCK_MS as usize) / 1000;
if block_samples > k_weighted.len() {
return Vec::new();
}
let num_blocks = (k_weighted.len() - block_samples) / block_samples + 1;
let mut values = Vec::with_capacity(num_blocks);
for b in 0..num_blocks {
let start = b * block_samples;
let sum_sq: f64 = k_weighted[start..start + block_samples]
.iter()
.map(|&x| (x as f64).powi(2))
.sum();
values.push(power_to_lkfs(sum_sq / block_samples as f64));
}
values
}
#[derive(Debug, Clone, PartialEq)]
pub struct LoudnessResult {
pub integrated_lufs: f64,
pub lra: f64,
pub true_peak_db: f64,
pub short_term: Vec<f64>,
}
pub fn measure_loudness(samples: &[f32], sample_rate: u32) -> LoudnessResult {
let integrated_lufs = compute_integrated_lufs(samples, sample_rate);
let lra = compute_lra(samples, sample_rate);
let true_peak_db = compute_true_peak_db(samples);
let short_term = short_term_loudness(samples, sample_rate);
LoudnessResult {
integrated_lufs,
lra,
true_peak_db,
short_term,
}
}
fn apply_biquad(samples: &[f32], b0: f64, b1: f64, b2: f64, a1: f64, a2: f64) -> Vec<f32> {
let mut out = Vec::with_capacity(samples.len());
let mut s1: f64 = 0.0;
let mut s2: f64 = 0.0;
for &x in samples {
let xf = x as f64;
let y = b0 * xf + s1;
s1 = b1 * xf + a1 * y + s2;
s2 = b2 * xf + a2 * y;
out.push(y as f32);
}
out
}
const TP_OVERSAMPLE: usize = 4;
const TP_FIR_LEN: usize = 48;
const TP_TAPS: usize = TP_FIR_LEN / TP_OVERSAMPLE;
#[rustfmt::skip]
const BS1770_PHASES: [[f64; TP_TAPS]; TP_OVERSAMPLE] = [
[
0.0017089843750, 0.0109863281250, -0.0196533203125, 0.0332031250000,
-0.0594482421875, 0.1373291015625, 0.9721679687500, -0.1022949218750,
0.0476074218750, -0.0266113281250, 0.0148925781250, -0.0083007812500,
],
[
-0.0291748046875, 0.0292968750000, -0.0517578125000, 0.0891113281250,
-0.1665039062500, 0.4650878906250, 0.7797851562500, -0.2003173828125,
0.1015625000000, -0.0582275390625, 0.0330810546875, -0.0189208984375,
],
[
-0.0189208984375, 0.0330810546875, -0.0582275390625, 0.1015625000000,
-0.2003173828125, 0.7797851562500, 0.4650878906250, -0.1665039062500,
0.0891113281250, -0.0517578125000, 0.0292968750000, -0.0291748046875,
],
[
-0.0083007812500, 0.0148925781250, -0.0266113281250, 0.0476074218750,
-0.1022949218750, 0.9721679687500, 0.1373291015625, -0.0594482421875,
0.0332031250000, -0.0196533203125, 0.0109863281250, 0.0017089843750,
],
];
fn oversample_4x_bs1770(samples: &[f32]) -> Vec<f64> {
let in_len = samples.len();
if in_len == 0 {
return Vec::new();
}
let out_len = in_len * TP_OVERSAMPLE;
let mut out = vec![0.0f64; out_len];
for n in 0..in_len {
let base = n * TP_OVERSAMPLE;
for p in 0..TP_OVERSAMPLE {
let phase = &BS1770_PHASES[p];
let mut acc = 0.0f64;
for k in 0..TP_TAPS {
if k > n {
break;
}
acc += (samples[n - k] as f64) * phase[k];
}
out[base + p] = acc;
}
}
out
}
pub fn compute_true_peak_db(samples: &[f32]) -> f64 {
if samples.is_empty() {
return f64::NEG_INFINITY;
}
let upsampled = oversample_4x_bs1770(samples);
let peak_abs = upsampled.iter().fold(0.0f64, |max, &x| max.max(x.abs()));
if peak_abs <= 1e-15 {
f64::NEG_INFINITY
} else {
20.0 * peak_abs.log10()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TruePeakOver {
pub position: usize,
pub dbtp: f64,
}
pub fn find_true_peak_overs(samples: &[f32]) -> Vec<TruePeakOver> {
let upsampled = oversample_4x_bs1770(samples);
let mut overs = Vec::new();
let len = upsampled.len();
let mut i = 0;
while i < len {
if upsampled[i].abs() > 1.0 {
let start_sample = i / TP_OVERSAMPLE;
let mut peak = upsampled[i].abs();
i += 1;
while i < len && i / TP_OVERSAMPLE == start_sample {
if upsampled[i].abs() > 1.0 {
peak = peak.max(upsampled[i].abs());
}
i += 1;
}
overs.push(TruePeakOver {
position: start_sample,
dbtp: 20.0 * peak.log10(),
});
} else {
i += 1;
}
}
overs
}
pub fn oversample_4x(samples: &[f32]) -> Vec<f64> {
oversample_4x_bs1770(samples)
}
#[cfg(test)]
#[path = "perceptual_test.rs"]
mod perceptual_test;