Correct RTA detector timing and peak integration

This commit is contained in:
Mikei386 committed 2026-07-21 21:29:41 +02:00
1 parent 3478bfb495
commit 9b5250d42b
10 files changed
+312 -112

No files matched your search

+90
View File
@@ -3,6 +3,8 @@
//! Provides both the original repeated-RBJ Phoenix characteristic and complete
//! Butterworth fractional-octave bandpasses for controlled comparison.
use std::collections::VecDeque;
pub const RTW_THIRD_OCTAVE_CENTERS: &[f32] = &[
20.0, 25.0, 31.5, 40.0, 50.0, 63.0, 80.0, 100.0, 125.0, 160.0, 200.0, 250.0, 315.0, 400.0,
500.0, 630.0, 800.0, 1_000.0, 1_250.0, 1_600.0, 2_000.0, 2_500.0, 3_150.0, 4_000.0, 5_000.0,
@@ -218,6 +220,58 @@ pub fn integrate_power(
previous + alpha * (block_power - previous)
}
pub fn integrate_power_asymmetric(
previous: f64,
block_power: f64,
block_samples: usize,
sample_rate: u32,
rise_tau_seconds: f32,
fall_tau_seconds: f32,
) -> f64 {
let tau = if block_power >= previous {
rise_tau_seconds
} else {
fall_tau_seconds
};
integrate_power(previous, block_power, block_samples, sample_rate, tau)
}
pub struct SlidingPeak {
window_samples: u64,
sample_index: u64,
candidates: VecDeque<(u64, f64)>,
}
impl SlidingPeak {
pub fn new(window_samples: usize) -> Self {
let window_samples = window_samples.max(1);
Self {
window_samples: window_samples as u64,
sample_index: 0,
candidates: VecDeque::with_capacity(window_samples + 1),
}
}
pub fn push(&mut self, power: f64) -> f64 {
while self.candidates.back().is_some_and(|entry| entry.1 <= power) {
self.candidates.pop_back();
}
self.candidates.push_back((self.sample_index, power));
let oldest = self
.sample_index
.saturating_sub(self.window_samples.saturating_sub(1));
while self
.candidates
.front()
.is_some_and(|entry| entry.0 < oldest)
{
self.candidates.pop_front();
}
self.sample_index = self.sample_index.wrapping_add(1);
self.candidates.front().map_or(0.0, |entry| entry.1)
}
}
#[derive(Clone, Copy)]
struct StereoBiquad {
coeffs: BiquadCoeffs,
@@ -492,6 +546,42 @@ mod tests {
}
}
#[test]
fn impulse_integration_uses_fast_rise_and_slow_fall() {
let sample_rate = 48_000;
let risen = integrate_power_asymmetric(0.0, 1.0, 1_680, sample_rate, 0.035, 1.5);
let fallen = integrate_power_asymmetric(risen, 0.0, 1_680, sample_rate, 0.035, 1.5);
assert!((risen - (1.0 - (-1.0f64).exp())).abs() < 1.0e-6);
assert!(fallen > risen * 0.97);
}
#[test]
fn ten_millisecond_power_integration_is_block_size_independent() {
fn run(block_size: usize) -> f64 {
let sample_rate = 48_000;
let mut value = 0.0;
let mut processed = 0usize;
while processed < 480 {
let count = block_size.min(480 - processed);
value = integrate_power(value, 1.0, count, sample_rate, 0.010);
processed += count;
}
value
}
assert!((run(48) - run(128)).abs() < 1.0e-12);
assert!((run(128) - (1.0 - (-1.0f64).exp())).abs() < 1.0e-6);
}
#[test]
fn sliding_peak_retains_a_transient_for_exact_window() {
let mut detector = SlidingPeak::new(4);
assert_eq!(detector.push(1.0), 1.0);
assert_eq!(detector.push(0.1), 1.0);
assert_eq!(detector.push(0.2), 1.0);
assert_eq!(detector.push(0.3), 1.0);
assert_eq!(detector.push(0.4), 0.4);
}
#[test]
fn weighting_is_normalized_and_directionally_correct() {
for mode in ["a", "c"] {