Harden audio meter regression coverage
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
//! Continuous, block-boundary-independent 4x true-peak interpolation.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
const OVERSAMPLE: usize = 4;
|
||||
const RADIUS: usize = 8;
|
||||
const BUFFER_LEN: usize = RADIUS * 2 + 1;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TruePeakDetector {
|
||||
samples: VecDeque<f32>,
|
||||
}
|
||||
|
||||
impl Default for TruePeakDetector {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
samples: VecDeque::with_capacity(BUFFER_LEN + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TruePeakDetector {
|
||||
pub fn process(&mut self, sample: f32) -> f32 {
|
||||
let mut peak = sample.abs();
|
||||
self.samples.push_back(sample);
|
||||
if self.samples.len() < BUFFER_LEN {
|
||||
return peak;
|
||||
}
|
||||
|
||||
let mut contiguous = [0.0f32; BUFFER_LEN];
|
||||
for (target, source) in contiguous.iter_mut().zip(self.samples.iter()) {
|
||||
*target = *source;
|
||||
}
|
||||
for phase in 1..OVERSAMPLE {
|
||||
let position = RADIUS as f32 + phase as f32 / OVERSAMPLE as f32;
|
||||
peak = peak.max(interpolate(&contiguous, position).abs());
|
||||
}
|
||||
self.samples.pop_front();
|
||||
peak
|
||||
}
|
||||
}
|
||||
|
||||
fn sinc(value: f32) -> f32 {
|
||||
if value.abs() < 1.0e-6 {
|
||||
1.0
|
||||
} else {
|
||||
let x = std::f32::consts::PI * value;
|
||||
x.sin() / x
|
||||
}
|
||||
}
|
||||
|
||||
fn blackman(value: f32) -> f32 {
|
||||
let span = (RADIUS * 2) as f32;
|
||||
let phase = 2.0 * std::f32::consts::PI * (value + RADIUS as f32) / span;
|
||||
0.42 - 0.5 * phase.cos() + 0.08 * (2.0 * phase).cos()
|
||||
}
|
||||
|
||||
fn interpolate(samples: &[f32], position: f32) -> f32 {
|
||||
let base = position.floor() as isize;
|
||||
let mut sum = 0.0;
|
||||
let mut norm = 0.0;
|
||||
for index in (base - RADIUS as isize + 1)..=(base + RADIUS as isize) {
|
||||
if !(0..samples.len() as isize).contains(&index) {
|
||||
continue;
|
||||
}
|
||||
let distance = position - index as f32;
|
||||
let weight = sinc(distance) * blackman(distance);
|
||||
sum += samples[index as usize] * weight;
|
||||
norm += weight;
|
||||
}
|
||||
if norm.abs() > 1.0e-6 {
|
||||
sum / norm
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run_in_blocks(samples: &[f32], block_size: usize) -> f32 {
|
||||
let mut detector = TruePeakDetector::default();
|
||||
let mut peak = 0.0f32;
|
||||
for block in samples.chunks(block_size) {
|
||||
for &sample in block {
|
||||
peak = peak.max(detector.process(sample));
|
||||
}
|
||||
}
|
||||
for _ in 0..BUFFER_LEN {
|
||||
peak = peak.max(detector.process(0.0));
|
||||
}
|
||||
peak
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_is_independent_of_capture_block_boundaries() {
|
||||
let samples: Vec<f32> = (0..4_800)
|
||||
.map(|index| {
|
||||
(2.0 * std::f32::consts::PI * 11_025.0 * index as f32 / 48_000.0 + 0.31).sin() * 0.9
|
||||
})
|
||||
.collect();
|
||||
let reference = run_in_blocks(&samples, 1);
|
||||
for size in [64, 127, 128, 192, 511] {
|
||||
assert!((run_in_blocks(&samples, size) - reference).abs() < 1.0e-7);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_an_intersample_peak_above_sample_peak() {
|
||||
let samples: Vec<f32> = (0..4_800)
|
||||
.map(|index| {
|
||||
(2.0 * std::f32::consts::PI * 11_025.0 * index as f32 / 48_000.0 + 0.31).sin() * 0.9
|
||||
})
|
||||
.collect();
|
||||
let sample_peak = samples
|
||||
.iter()
|
||||
.fold(0.0f32, |peak, value| peak.max(value.abs()));
|
||||
let detected = run_in_blocks(&samples, 128);
|
||||
assert!(detected > sample_peak + 0.001);
|
||||
assert!(
|
||||
detected <= 0.91,
|
||||
"unexpected interpolation overshoot: {detected}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user