77 lines
2.4 KiB
Rust
77 lines
2.4 KiB
Rust
use weld_core::{Fit, Section, SMEAR_AUDIBLE_MS};
|
|
|
|
const RATE: f32 = 48000.;
|
|
|
|
fn fit(sections: &[(f32, f32)], flip: bool) -> Fit {
|
|
let mut fit = Fit {
|
|
flip,
|
|
count: sections.len(),
|
|
..Fit::NONE
|
|
};
|
|
for (s, &(fc, q)) in fit.sections.iter_mut().zip(sections) {
|
|
*s = Section { fc, q };
|
|
}
|
|
fit
|
|
}
|
|
|
|
/// Phase of the fit at `f`, from its complex response.
|
|
fn phase(fit: &Fit, f: f64) -> f64 {
|
|
let (re, im) = fit.response(f as f32, RATE);
|
|
(im as f64).atan2(re as f64)
|
|
}
|
|
|
|
#[test]
|
|
fn group_delay_matches_the_phase_slope() {
|
|
let fit = fit(&[(180., 0.9), (1200., 0.6), (3500., 2.)], false);
|
|
for f in [80., 250., 700., 1500., 3000., 8000.] {
|
|
let df = 0.5;
|
|
let mut dphi = phase(&fit, f + df) - phase(&fit, f - df);
|
|
while dphi > std::f64::consts::PI {
|
|
dphi -= std::f64::consts::TAU;
|
|
}
|
|
while dphi < -std::f64::consts::PI {
|
|
dphi += std::f64::consts::TAU;
|
|
}
|
|
let numeric = -dphi / (std::f64::consts::TAU * 2. * df);
|
|
let analytic = fit.group_delay(f as f32, RATE) as f64;
|
|
assert!(
|
|
(numeric - analytic).abs() < 2e-6 + 0.01 * numeric.abs(),
|
|
"{f} Hz: numeric {numeric}, analytic {analytic}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn one_section_peaks_near_the_analog_value() {
|
|
// A second-order analog allpass delays 4·Q/ω₀ at its centre.
|
|
let (fc, q) = (1000., 0.7);
|
|
let delay = fit(&[(fc, q)], false).group_delay(fc, RATE);
|
|
let analog = 4. * q / (std::f32::consts::TAU * fc);
|
|
assert!(
|
|
(delay - analog).abs() < 0.05 * analog,
|
|
"{delay} vs {analog}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_sections_and_polarity_do_not_smear() {
|
|
assert_eq!(Fit::NONE.smear_ms(RATE), 0.);
|
|
assert_eq!(fit(&[], true).smear_ms(RATE), 0.);
|
|
let one = fit(&[(1000., 0.7)], false);
|
|
assert_eq!(
|
|
one.smear_ms(RATE),
|
|
fit(&[(1000., 0.7)], true).smear_ms(RATE)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn smear_separates_gentle_and_sharp_fits() {
|
|
// Above 500 Hz a low section has only its tail left: well under the threshold.
|
|
let low = fit(&[(80., 0.7)], false).smear_ms(RATE);
|
|
assert!(low < 0.25 * SMEAR_AUDIBLE_MS, "{low}");
|
|
// A high-Q section at 1 kHz puts a 2.5 ms peak inside the range.
|
|
let sharp = fit(&[(1000., 4.)], false).smear_ms(RATE);
|
|
assert!(sharp > SMEAR_AUDIBLE_MS, "{sharp}");
|
|
assert!(sharp > 2. && sharp < 2.7, "{sharp}");
|
|
}
|