atagen

headroom

public
Star0
Watch0
c454b10854660e0f55220e43b21a60913b505716
1
ssh://brewery@pls.lobotomise.me:33/atagen/headroom.git

Blame: crates/headroom-dsp/src/oversample.rs

614b4e43f9f3atagen2026-05-27 15:49 UTC1
//! polyphase fir up/downsamplers for the limiter's oversampled isp detection (itu-r bs.1770-4).
614b4e43f9f3atagen2026-05-27 15:49 UTC2
614b4e43f9f3atagen2026-05-27 15:49 UTC3
use std::f32::consts::PI;
614b4e43f9f3atagen2026-05-27 15:49 UTC4
614b4e43f9f3atagen2026-05-27 15:49 UTC5
/// blackman-windowed sinc lowpass fir, normalized for unity dc gain.
614b4e43f9f3atagen2026-05-27 15:49 UTC6
/// odd `taps` → linear phase, group delay `(taps - 1) / 2`. `fc` normalized in `0.0..0.5`.
614b4e43f9f3atagen2026-05-27 15:49 UTC7
/// use `fc = 0.5 / M` as the prototype for M-times oversampling.
614b4e43f9f3atagen2026-05-27 15:49 UTC8
#[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC9
pub fn design_lowpass_blackman(taps: usize, fc: f32) -> Vec<f32> {
614b4e43f9f3atagen2026-05-27 15:49 UTC10
    let taps = taps.max(1);
614b4e43f9f3atagen2026-05-27 15:49 UTC11
    let m = (taps as f32 - 1.0).max(1.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC12
    let mut h = vec![0.0_f32; taps];
614b4e43f9f3atagen2026-05-27 15:49 UTC13
    let mut sum = 0.0_f32;
614b4e43f9f3atagen2026-05-27 15:49 UTC14
    for (n, h_n) in h.iter_mut().enumerate() {
614b4e43f9f3atagen2026-05-27 15:49 UTC15
        let x = n as f32 - m / 2.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC16
        let sinc = if x.abs() < 1e-9 {
614b4e43f9f3atagen2026-05-27 15:49 UTC17
            2.0 * fc
614b4e43f9f3atagen2026-05-27 15:49 UTC18
        } else {
614b4e43f9f3atagen2026-05-27 15:49 UTC19
            (2.0 * PI * fc * x).sin() / (PI * x)
614b4e43f9f3atagen2026-05-27 15:49 UTC20
        };
614b4e43f9f3atagen2026-05-27 15:49 UTC21
        let w = 0.42 - 0.5 * (2.0 * PI * n as f32 / m).cos() + 0.08 * (4.0 * PI * n as f32 / m).cos();
614b4e43f9f3atagen2026-05-27 15:49 UTC22
        *h_n = sinc * w;
614b4e43f9f3atagen2026-05-27 15:49 UTC23
        sum += *h_n;
614b4e43f9f3atagen2026-05-27 15:49 UTC24
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC25
    if sum.abs() > 1e-12 {
614b4e43f9f3atagen2026-05-27 15:49 UTC26
        for v in &mut h {
614b4e43f9f3atagen2026-05-27 15:49 UTC27
            *v /= sum;
614b4e43f9f3atagen2026-05-27 15:49 UTC28
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC29
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC30
    h
614b4e43f9f3atagen2026-05-27 15:49 UTC31
}
614b4e43f9f3atagen2026-05-27 15:49 UTC32
614b4e43f9f3atagen2026-05-27 15:49 UTC33
/// one input sample → `factor` output samples. coeffs pre-scaled by `factor` for unity dc gain.
614b4e43f9f3atagen2026-05-27 15:49 UTC34
pub struct PolyphaseUpsampler {
614b4e43f9f3atagen2026-05-27 15:49 UTC35
    factor: usize,
614b4e43f9f3atagen2026-05-27 15:49 UTC36
    taps_per_phase: usize,
614b4e43f9f3atagen2026-05-27 15:49 UTC37
    /// `phases[j * taps_per_phase + p] = h[p * factor + j] * factor`.
614b4e43f9f3atagen2026-05-27 15:49 UTC38
    phases: Vec<f32>,
614b4e43f9f3atagen2026-05-27 15:49 UTC39
    history: Vec<f32>,
614b4e43f9f3atagen2026-05-27 15:49 UTC40
    write_idx: usize,
614b4e43f9f3atagen2026-05-27 15:49 UTC41
}
614b4e43f9f3atagen2026-05-27 15:49 UTC42
614b4e43f9f3atagen2026-05-27 15:49 UTC43
impl PolyphaseUpsampler {
614b4e43f9f3atagen2026-05-27 15:49 UTC44
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC45
    pub fn new(factor: usize, fir_taps: &[f32]) -> Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC46
        let factor = factor.max(1);
614b4e43f9f3atagen2026-05-27 15:49 UTC47
        let taps_per_phase = fir_taps.len().div_ceil(factor);
614b4e43f9f3atagen2026-05-27 15:49 UTC48
        let mut phases = vec![0.0_f32; factor * taps_per_phase];
614b4e43f9f3atagen2026-05-27 15:49 UTC49
        for (n, &h) in fir_taps.iter().enumerate() {
614b4e43f9f3atagen2026-05-27 15:49 UTC50
            let j = n % factor;
614b4e43f9f3atagen2026-05-27 15:49 UTC51
            let p = n / factor;
614b4e43f9f3atagen2026-05-27 15:49 UTC52
            phases[j * taps_per_phase + p] = h * factor as f32;
614b4e43f9f3atagen2026-05-27 15:49 UTC53
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC54
        Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC55
            factor,
614b4e43f9f3atagen2026-05-27 15:49 UTC56
            taps_per_phase,
614b4e43f9f3atagen2026-05-27 15:49 UTC57
            phases,
614b4e43f9f3atagen2026-05-27 15:49 UTC58
            history: vec![0.0_f32; taps_per_phase.max(1)],
614b4e43f9f3atagen2026-05-27 15:49 UTC59
            write_idx: 0,
614b4e43f9f3atagen2026-05-27 15:49 UTC60
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC61
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC62
614b4e43f9f3atagen2026-05-27 15:49 UTC63
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC64
    pub fn factor(&self) -> usize {
614b4e43f9f3atagen2026-05-27 15:49 UTC65
        self.factor
614b4e43f9f3atagen2026-05-27 15:49 UTC66
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC67
614b4e43f9f3atagen2026-05-27 15:49 UTC68
    /// emit `factor` samples into `out[..factor]`; `out.len() >= factor`.
614b4e43f9f3atagen2026-05-27 15:49 UTC69
    pub fn process(&mut self, x: f32, out: &mut [f32]) {
614b4e43f9f3atagen2026-05-27 15:49 UTC70
        debug_assert!(out.len() >= self.factor);
614b4e43f9f3atagen2026-05-27 15:49 UTC71
        let len = self.history.len();
614b4e43f9f3atagen2026-05-27 15:49 UTC72
        self.history[self.write_idx] = x;
614b4e43f9f3atagen2026-05-27 15:49 UTC73
        let just_written = self.write_idx;
614b4e43f9f3atagen2026-05-27 15:49 UTC74
        self.write_idx = (self.write_idx + 1) % len;
614b4e43f9f3atagen2026-05-27 15:49 UTC75
614b4e43f9f3atagen2026-05-27 15:49 UTC76
        for (j, slot) in out.iter_mut().take(self.factor).enumerate() {
614b4e43f9f3atagen2026-05-27 15:49 UTC77
            let phase = &self.phases[j * self.taps_per_phase..(j + 1) * self.taps_per_phase];
614b4e43f9f3atagen2026-05-27 15:49 UTC78
            let mut acc = 0.0_f32;
614b4e43f9f3atagen2026-05-27 15:49 UTC79
            for (p, &h) in phase.iter().enumerate() {
614b4e43f9f3atagen2026-05-27 15:49 UTC80
                let idx = (just_written + len - p) % len;
614b4e43f9f3atagen2026-05-27 15:49 UTC81
                acc += h * self.history[idx];
614b4e43f9f3atagen2026-05-27 15:49 UTC82
            }
614b4e43f9f3atagen2026-05-27 15:49 UTC83
            *slot = acc;
614b4e43f9f3atagen2026-05-27 15:49 UTC84
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC85
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC86
614b4e43f9f3atagen2026-05-27 15:49 UTC87
    pub fn reset(&mut self) {
614b4e43f9f3atagen2026-05-27 15:49 UTC88
        for v in &mut self.history {
614b4e43f9f3atagen2026-05-27 15:49 UTC89
            *v = 0.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC90
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC91
        self.write_idx = 0;
614b4e43f9f3atagen2026-05-27 15:49 UTC92
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC93
}
614b4e43f9f3atagen2026-05-27 15:49 UTC94
614b4e43f9f3atagen2026-05-27 15:49 UTC95
/// `factor` input samples → one output. same prototype lowpass as the upsampler.
614b4e43f9f3atagen2026-05-27 15:49 UTC96
/// no polyphase split — savings are modest at our tap count, clarity wins.
614b4e43f9f3atagen2026-05-27 15:49 UTC97
pub struct PolyphaseDownsampler {
614b4e43f9f3atagen2026-05-27 15:49 UTC98
    factor: usize,
614b4e43f9f3atagen2026-05-27 15:49 UTC99
    taps: Vec<f32>,
614b4e43f9f3atagen2026-05-27 15:49 UTC100
    history: Vec<f32>,
614b4e43f9f3atagen2026-05-27 15:49 UTC101
    write_idx: usize,
614b4e43f9f3atagen2026-05-27 15:49 UTC102
}
614b4e43f9f3atagen2026-05-27 15:49 UTC103
614b4e43f9f3atagen2026-05-27 15:49 UTC104
impl PolyphaseDownsampler {
614b4e43f9f3atagen2026-05-27 15:49 UTC105
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC106
    pub fn new(factor: usize, fir_taps: &[f32]) -> Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC107
        let factor = factor.max(1);
614b4e43f9f3atagen2026-05-27 15:49 UTC108
        Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC109
            factor,
614b4e43f9f3atagen2026-05-27 15:49 UTC110
            taps: fir_taps.to_vec(),
614b4e43f9f3atagen2026-05-27 15:49 UTC111
            history: vec![0.0_f32; fir_taps.len().max(1)],
614b4e43f9f3atagen2026-05-27 15:49 UTC112
            write_idx: 0,
614b4e43f9f3atagen2026-05-27 15:49 UTC113
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC114
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC115
614b4e43f9f3atagen2026-05-27 15:49 UTC116
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC117
    pub fn factor(&self) -> usize {
614b4e43f9f3atagen2026-05-27 15:49 UTC118
        self.factor
614b4e43f9f3atagen2026-05-27 15:49 UTC119
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC120
614b4e43f9f3atagen2026-05-27 15:49 UTC121
    /// push `factor` samples, return one filtered output.
614b4e43f9f3atagen2026-05-27 15:49 UTC122
    pub fn process(&mut self, ins: &[f32]) -> f32 {
614b4e43f9f3atagen2026-05-27 15:49 UTC123
        debug_assert_eq!(ins.len(), self.factor);
614b4e43f9f3atagen2026-05-27 15:49 UTC124
        let len = self.history.len();
614b4e43f9f3atagen2026-05-27 15:49 UTC125
        for &x in ins {
614b4e43f9f3atagen2026-05-27 15:49 UTC126
            self.history[self.write_idx] = x;
614b4e43f9f3atagen2026-05-27 15:49 UTC127
            self.write_idx = (self.write_idx + 1) % len;
614b4e43f9f3atagen2026-05-27 15:49 UTC128
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC129
        let mut acc = 0.0_f32;
614b4e43f9f3atagen2026-05-27 15:49 UTC130
        for (k, &h) in self.taps.iter().enumerate() {
614b4e43f9f3atagen2026-05-27 15:49 UTC131
            let idx = (self.write_idx + len - 1 - k) % len;
614b4e43f9f3atagen2026-05-27 15:49 UTC132
            acc += h * self.history[idx];
614b4e43f9f3atagen2026-05-27 15:49 UTC133
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC134
        acc
614b4e43f9f3atagen2026-05-27 15:49 UTC135
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC136
614b4e43f9f3atagen2026-05-27 15:49 UTC137
    pub fn reset(&mut self) {
614b4e43f9f3atagen2026-05-27 15:49 UTC138
        for v in &mut self.history {
614b4e43f9f3atagen2026-05-27 15:49 UTC139
            *v = 0.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC140
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC141
        self.write_idx = 0;
614b4e43f9f3atagen2026-05-27 15:49 UTC142
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC143
}
614b4e43f9f3atagen2026-05-27 15:49 UTC144
614b4e43f9f3atagen2026-05-27 15:49 UTC145
#[cfg(test)]
614b4e43f9f3atagen2026-05-27 15:49 UTC146
mod tests {
614b4e43f9f3atagen2026-05-27 15:49 UTC147
    use super::*;
614b4e43f9f3atagen2026-05-27 15:49 UTC148
614b4e43f9f3atagen2026-05-27 15:49 UTC149
    fn fir(taps: usize, factor: usize) -> Vec<f32> {
614b4e43f9f3atagen2026-05-27 15:49 UTC150
        design_lowpass_blackman(taps, 0.45 / factor as f32)
614b4e43f9f3atagen2026-05-27 15:49 UTC151
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC152
614b4e43f9f3atagen2026-05-27 15:49 UTC153
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC154
    fn upsampler_dc_gain_preserved() {
614b4e43f9f3atagen2026-05-27 15:49 UTC155
        let h = fir(31, 4);
614b4e43f9f3atagen2026-05-27 15:49 UTC156
        let mut up = PolyphaseUpsampler::new(4, &h);
614b4e43f9f3atagen2026-05-27 15:49 UTC157
        // Drive DC and let the filter settle, then check unity gain.
614b4e43f9f3atagen2026-05-27 15:49 UTC158
        let mut buf = [0.0_f32; 8];
614b4e43f9f3atagen2026-05-27 15:49 UTC159
        let mut last_avg = 0.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC160
        for _ in 0..200 {
614b4e43f9f3atagen2026-05-27 15:49 UTC161
            up.process(1.0, &mut buf);
614b4e43f9f3atagen2026-05-27 15:49 UTC162
            last_avg = buf[..4].iter().sum::<f32>() / 4.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC163
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC164
        assert!((last_avg - 1.0).abs() < 1e-3, "got {last_avg}");
614b4e43f9f3atagen2026-05-27 15:49 UTC165
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC166
614b4e43f9f3atagen2026-05-27 15:49 UTC167
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC168
    fn down_then_up_roundtrip_is_bounded() {
614b4e43f9f3atagen2026-05-27 15:49 UTC169
        // Stuff zero-padded input through up then down; output amplitude
614b4e43f9f3atagen2026-05-27 15:49 UTC170
        // should approximately equal input on smooth signals.
614b4e43f9f3atagen2026-05-27 15:49 UTC171
        let h = fir(31, 4);
614b4e43f9f3atagen2026-05-27 15:49 UTC172
        let mut up = PolyphaseUpsampler::new(4, &h);
614b4e43f9f3atagen2026-05-27 15:49 UTC173
        let mut down = PolyphaseDownsampler::new(4, &h);
614b4e43f9f3atagen2026-05-27 15:49 UTC174
        let mut max_err = 0.0_f32;
614b4e43f9f3atagen2026-05-27 15:49 UTC175
        let mut up_buf = [0.0_f32; 8];
614b4e43f9f3atagen2026-05-27 15:49 UTC176
        // Drive a slow sine well below Nyquist.
614b4e43f9f3atagen2026-05-27 15:49 UTC177
        for n in 0..2_000 {
614b4e43f9f3atagen2026-05-27 15:49 UTC178
            let t = n as f32 / 48_000.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC179
            let x = (2.0 * std::f32::consts::PI * 1_000.0 * t).sin() * 0.5;
614b4e43f9f3atagen2026-05-27 15:49 UTC180
            up.process(x, &mut up_buf);
614b4e43f9f3atagen2026-05-27 15:49 UTC181
            let y = down.process(&up_buf[..4]);
614b4e43f9f3atagen2026-05-27 15:49 UTC182
            // After group-delay warm-up, the error should be small.
614b4e43f9f3atagen2026-05-27 15:49 UTC183
            if n > 80 {
614b4e43f9f3atagen2026-05-27 15:49 UTC184
                max_err = max_err.max((x - y).abs());
614b4e43f9f3atagen2026-05-27 15:49 UTC185
            }
614b4e43f9f3atagen2026-05-27 15:49 UTC186
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC187
        // The filter is symmetric, so up/down with the same kernel
614b4e43f9f3atagen2026-05-27 15:49 UTC188
        // introduces ~6 dB attenuation by design (each pass contributes
614b4e43f9f3atagen2026-05-27 15:49 UTC189
        // half the gain). What we care about here is finite, bounded
614b4e43f9f3atagen2026-05-27 15:49 UTC190
        // output and no runaway.
614b4e43f9f3atagen2026-05-27 15:49 UTC191
        assert!(max_err < 1.0, "max_err {max_err}");
614b4e43f9f3atagen2026-05-27 15:49 UTC192
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC193
614b4e43f9f3atagen2026-05-27 15:49 UTC194
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC195
    fn upsampler_handles_impulse() {
614b4e43f9f3atagen2026-05-27 15:49 UTC196
        let h = fir(15, 4);
614b4e43f9f3atagen2026-05-27 15:49 UTC197
        let mut up = PolyphaseUpsampler::new(4, &h);
614b4e43f9f3atagen2026-05-27 15:49 UTC198
        let mut buf = [0.0_f32; 8];
614b4e43f9f3atagen2026-05-27 15:49 UTC199
        up.process(1.0, &mut buf);
614b4e43f9f3atagen2026-05-27 15:49 UTC200
        // Some non-zero output expected on first impulse already.
614b4e43f9f3atagen2026-05-27 15:49 UTC201
        assert!(buf[..4].iter().any(|&v| v.abs() > 1e-6));
614b4e43f9f3atagen2026-05-27 15:49 UTC202
        // Drive zeros; output decays to zero.
614b4e43f9f3atagen2026-05-27 15:49 UTC203
        for _ in 0..200 {
614b4e43f9f3atagen2026-05-27 15:49 UTC204
            up.process(0.0, &mut buf);
614b4e43f9f3atagen2026-05-27 15:49 UTC205
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC206
        assert!(buf[..4].iter().all(|&v| v.abs() < 1e-6));
614b4e43f9f3atagen2026-05-27 15:49 UTC207
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC208
}