Weld's allpass sections add group delay between 500 Hz and 4 kHz, where listeners hear it best. Liski et al. (2021) measured a threshold of about 0.64 ms for a positive group-delay peak on clicks. Weld now reports how much its delay varies over that range and, above the threshold, suggests comparing attacks in SUM and SUM DRY. Common fits from Weld's own tests spread 0.82-1.03 ms, so the line is a neutral note, not a warning: sustained bass hides much more than a click. - Fit::group_delay (analytic, per section) and Fit::smear_ms in weld-core. - Report::smear_ms, filled by the analyzer from the applied fit. - Tests against the numeric phase slope and the analog 4Q/w0 peak. - Display only; the audio path does not change.
71 lines
2.3 KiB
Rust
71 lines
2.3 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}");
|
|
}
|