feat(weld): show the alignment's delay spread
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.
This commit is contained in:
1 parent
340ff13342
commit
d536e3127a
9 files changed
+158
-2
No files matched your search
@@ -311,6 +311,7 @@ impl Analyzer {
|
||||
frames: self.frames,
|
||||
low: self.range().0,
|
||||
high: self.range().1,
|
||||
smear_ms: fit.smear_ms(self.rate),
|
||||
..Report::default()
|
||||
};
|
||||
// Display bands over all bins.
|
||||
|
||||
@@ -7,6 +7,16 @@ pub const MIN_FC: f32 = 30.;
|
||||
pub const MIN_Q: f32 = 0.2;
|
||||
pub const MAX_Q: f32 = 4.;
|
||||
|
||||
/// The range where listeners hear group-delay changes best (Liski, Mäkivirta and
|
||||
/// Välimäki 2021, "Audibility of Group-Delay Equalization").
|
||||
pub const SMEAR_LOW: f32 = 500.;
|
||||
pub const SMEAR_HIGH: f32 = 4000.;
|
||||
/// Smallest mean threshold that study found for a positive group-delay peak in that
|
||||
/// range, with a pink impulse over headphones. An allpass only adds positive delay.
|
||||
/// Sustained bass hides much more; this marks where sharp attacks may soften.
|
||||
pub const SMEAR_AUDIBLE_MS: f32 = 0.64;
|
||||
const SMEAR_POINTS: usize = 64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Section {
|
||||
/// Centre frequency in Hz, where the section's phase passes −180°.
|
||||
@@ -67,6 +77,33 @@ impl Fit {
|
||||
}
|
||||
(re, im)
|
||||
}
|
||||
/// Group delay at `f` Hz in seconds. Polarity adds none.
|
||||
pub fn group_delay(&self, f: f32, rate: f32) -> f32 {
|
||||
let w = std::f32::consts::TAU * f / rate;
|
||||
let (c1, s1, c2, s2) = (w.cos(), w.sin(), (2. * w).cos(), (2. * w).sin());
|
||||
let samples: f32 = self
|
||||
.active()
|
||||
.iter()
|
||||
.map(|s| Coefs::allpass(s.fc, s.q, rate).group_delay(c1, s1, c2, s2))
|
||||
.sum();
|
||||
samples / rate
|
||||
}
|
||||
/// How much the group delay varies between `SMEAR_LOW` and `SMEAR_HIGH`, in ms:
|
||||
/// the largest delay minus the smallest. A constant delay counts as zero.
|
||||
pub fn smear_ms(&self, rate: f32) -> f32 {
|
||||
if self.active().is_empty() {
|
||||
return 0.;
|
||||
}
|
||||
let high = SMEAR_HIGH.min(max_fc(rate));
|
||||
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
|
||||
for i in 0..SMEAR_POINTS {
|
||||
let f = SMEAR_LOW * (high / SMEAR_LOW).powf(i as f32 / (SMEAR_POINTS - 1) as f32);
|
||||
let d = self.group_delay(f, rate);
|
||||
lo = lo.min(d);
|
||||
hi = hi.max(d);
|
||||
}
|
||||
(hi - lo).max(0.) * 1000.
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_fc(rate: f32) -> f32 {
|
||||
@@ -104,6 +141,16 @@ impl Coefs {
|
||||
// times z⁻² = c2 − j s2
|
||||
(qr * c2 + qi * s2, qi * c2 - qr * s2)
|
||||
}
|
||||
/// Group delay in samples, given cos/sin of ω and 2ω. With H = z⁻²·conj(D)/D the
|
||||
/// phase is −2ω − 2·arg D, so the delay is 2 + 2·d(arg D)/dω.
|
||||
#[inline]
|
||||
pub fn group_delay(&self, c1: f32, s1: f32, c2: f32, s2: f32) -> f32 {
|
||||
let x = 1. + self.a1 * c1 + self.a2 * c2;
|
||||
let y = -(self.a1 * s1 + self.a2 * s2);
|
||||
let dx = -(self.a1 * s1 + 2. * self.a2 * s2);
|
||||
let dy = -(self.a1 * c1 + 2. * self.a2 * c2);
|
||||
2. + 2. * (x * dy - y * dx) / (x * x + y * y)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transposed direct form II state, in f64 so sections near 30 Hz stay quiet.
|
||||
|
||||
@@ -9,7 +9,7 @@ mod report;
|
||||
mod settings;
|
||||
mod worker;
|
||||
pub use analyzer::{Analyzer, Update};
|
||||
pub use chain::{Chain, Fit, Section, MAX_SECTIONS};
|
||||
pub use chain::{Chain, Fit, Section, MAX_SECTIONS, SMEAR_AUDIBLE_MS, SMEAR_HIGH, SMEAR_LOW};
|
||||
pub use report::{
|
||||
band_edge, curve_frequency, Band, CurvePoint, Report, Status, BANDS, CURVE, DISPLAY_HIGH,
|
||||
DISPLAY_LOW,
|
||||
|
||||
@@ -83,6 +83,9 @@ pub struct Report {
|
||||
pub lag_ms: f32,
|
||||
/// How clear the time offset is, 0..1.
|
||||
pub lag_strength: f32,
|
||||
/// How much the alignment's group delay varies between `SMEAR_LOW` and `SMEAR_HIGH`,
|
||||
/// ms. Above `SMEAR_AUDIBLE_MS`, sharp attacks in this layer may soften.
|
||||
pub smear_ms: f32,
|
||||
/// Measured frames in the current average.
|
||||
pub frames: u32,
|
||||
pub low: f32,
|
||||
@@ -100,6 +103,7 @@ impl Default for Report {
|
||||
recovered_db: 0.,
|
||||
lag_ms: 0.,
|
||||
lag_strength: 0.,
|
||||
smear_ms: 0.,
|
||||
frames: 0,
|
||||
low: 60.,
|
||||
high: 6000.,
|
||||
|
||||
Reference in new issue
Block a user