Correct RTA detector timing and peak integration
This commit is contained in:
1 parent
3478bfb495
commit
9b5250d42b
10 files changed
+312
-112
No files matched your search
+90
@@ -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"] {
|
||||
|
||||
Reference in new issue
Block a user