atagen

headroom

public
Star0
Watch0
b4f9722db37afb8ec0881327edf1a085c821d6a9
1
ssh://brewery@pls.lobotomise.me:33/atagen/headroom.git
crates/headroom-dsp/src/oversample.rsRust208 lines6.5 KB
1
//! polyphase fir up/downsamplers for the limiter's oversampled isp detection (itu-r bs.1770-4).
2
3
use std::f32::consts::PI;
4
5
/// blackman-windowed sinc lowpass fir, normalized for unity dc gain.
6
/// odd `taps` → linear phase, group delay `(taps - 1) / 2`. `fc` normalized in `0.0..0.5`.
7
/// use `fc = 0.5 / M` as the prototype for M-times oversampling.
8
#[must_use]
9
pub fn design_lowpass_blackman(taps: usize, fc: f32) -> Vec<f32> {
10
    let taps = taps.max(1);
11
    let m = (taps as f32 - 1.0).max(1.0);
12
    let mut h = vec![0.0_f32; taps];
13
    let mut sum = 0.0_f32;
14
    for (n, h_n) in h.iter_mut().enumerate() {
15
        let x = n as f32 - m / 2.0;
16
        let sinc = if x.abs() < 1e-9 {
17
            2.0 * fc
18
        } else {
19
            (2.0 * PI * fc * x).sin() / (PI * x)
20
        };
21
        let w = 0.42 - 0.5 * (2.0 * PI * n as f32 / m).cos() + 0.08 * (4.0 * PI * n as f32 / m).cos();
22
        *h_n = sinc * w;
23
        sum += *h_n;
24
    }
25
    if sum.abs() > 1e-12 {
26
        for v in &mut h {
27
            *v /= sum;
28
        }
29
    }
30
    h
31
}
32
33
/// one input sample → `factor` output samples. coeffs pre-scaled by `factor` for unity dc gain.
34
pub struct PolyphaseUpsampler {
35
    factor: usize,
36
    taps_per_phase: usize,
37
    /// `phases[j * taps_per_phase + p] = h[p * factor + j] * factor`.
38
    phases: Vec<f32>,
39
    history: Vec<f32>,
40
    write_idx: usize,
41
}
42
43
impl PolyphaseUpsampler {
44
    #[must_use]
45
    pub fn new(factor: usize, fir_taps: &[f32]) -> Self {
46
        let factor = factor.max(1);
47
        let taps_per_phase = fir_taps.len().div_ceil(factor);
48
        let mut phases = vec![0.0_f32; factor * taps_per_phase];
49
        for (n, &h) in fir_taps.iter().enumerate() {
50
            let j = n % factor;
51
            let p = n / factor;
52
            phases[j * taps_per_phase + p] = h * factor as f32;
53
        }
54
        Self {
55
            factor,
56
            taps_per_phase,
57
            phases,
58
            history: vec![0.0_f32; taps_per_phase.max(1)],
59
            write_idx: 0,
60
        }
61
    }
62
63
    #[must_use]
64
    pub fn factor(&self) -> usize {
65
        self.factor
66
    }
67
68
    /// emit `factor` samples into `out[..factor]`; `out.len() >= factor`.
69
    pub fn process(&mut self, x: f32, out: &mut [f32]) {
70
        debug_assert!(out.len() >= self.factor);
71
        let len = self.history.len();
72
        self.history[self.write_idx] = x;
73
        let just_written = self.write_idx;
74
        self.write_idx = (self.write_idx + 1) % len;
75
76
        for (j, slot) in out.iter_mut().take(self.factor).enumerate() {
77
            let phase = &self.phases[j * self.taps_per_phase..(j + 1) * self.taps_per_phase];
78
            let mut acc = 0.0_f32;
79
            for (p, &h) in phase.iter().enumerate() {
80
                let idx = (just_written + len - p) % len;
81
                acc += h * self.history[idx];
82
            }
83
            *slot = acc;
84
        }
85
    }
86
87
    pub fn reset(&mut self) {
88
        for v in &mut self.history {
89
            *v = 0.0;
90
        }
91
        self.write_idx = 0;
92
    }
93
}
94
95
/// `factor` input samples → one output. same prototype lowpass as the upsampler.
96
/// no polyphase split — savings are modest at our tap count, clarity wins.
97
pub struct PolyphaseDownsampler {
98
    factor: usize,
99
    taps: Vec<f32>,
100
    history: Vec<f32>,
101
    write_idx: usize,
102
}
103
104
impl PolyphaseDownsampler {
105
    #[must_use]
106
    pub fn new(factor: usize, fir_taps: &[f32]) -> Self {
107
        let factor = factor.max(1);
108
        Self {
109
            factor,
110
            taps: fir_taps.to_vec(),
111
            history: vec![0.0_f32; fir_taps.len().max(1)],
112
            write_idx: 0,
113
        }
114
    }
115
116
    #[must_use]
117
    pub fn factor(&self) -> usize {
118
        self.factor
119
    }
120
121
    /// push `factor` samples, return one filtered output.
122
    pub fn process(&mut self, ins: &[f32]) -> f32 {
123
        debug_assert_eq!(ins.len(), self.factor);
124
        let len = self.history.len();
125
        for &x in ins {
126
            self.history[self.write_idx] = x;
127
            self.write_idx = (self.write_idx + 1) % len;
128
        }
129
        let mut acc = 0.0_f32;
130
        for (k, &h) in self.taps.iter().enumerate() {
131
            let idx = (self.write_idx + len - 1 - k) % len;
132
            acc += h * self.history[idx];
133
        }
134
        acc
135
    }
136
137
    pub fn reset(&mut self) {
138
        for v in &mut self.history {
139
            *v = 0.0;
140
        }
141
        self.write_idx = 0;
142
    }
143
}
144
145
#[cfg(test)]
146
mod tests {
147
    use super::*;
148
149
    fn fir(taps: usize, factor: usize) -> Vec<f32> {
150
        design_lowpass_blackman(taps, 0.45 / factor as f32)
151
    }
152
153
    #[test]
154
    fn upsampler_dc_gain_preserved() {
155
        let h = fir(31, 4);
156
        let mut up = PolyphaseUpsampler::new(4, &h);
157
        // Drive DC and let the filter settle, then check unity gain.
158
        let mut buf = [0.0_f32; 8];
159
        let mut last_avg = 0.0;
160
        for _ in 0..200 {
161
            up.process(1.0, &mut buf);
162
            last_avg = buf[..4].iter().sum::<f32>() / 4.0;
163
        }
164
        assert!((last_avg - 1.0).abs() < 1e-3, "got {last_avg}");
165
    }
166
167
    #[test]
168
    fn down_then_up_roundtrip_is_bounded() {
169
        // Stuff zero-padded input through up then down; output amplitude
170
        // should approximately equal input on smooth signals.
171
        let h = fir(31, 4);
172
        let mut up = PolyphaseUpsampler::new(4, &h);
173
        let mut down = PolyphaseDownsampler::new(4, &h);
174
        let mut max_err = 0.0_f32;
175
        let mut up_buf = [0.0_f32; 8];
176
        // Drive a slow sine well below Nyquist.
177
        for n in 0..2_000 {
178
            let t = n as f32 / 48_000.0;
179
            let x = (2.0 * std::f32::consts::PI * 1_000.0 * t).sin() * 0.5;
180
            up.process(x, &mut up_buf);
181
            let y = down.process(&up_buf[..4]);
182
            // After group-delay warm-up, the error should be small.
183
            if n > 80 {
184
                max_err = max_err.max((x - y).abs());
185
            }
186
        }
187
        // The filter is symmetric, so up/down with the same kernel
188
        // introduces ~6 dB attenuation by design (each pass contributes
189
        // half the gain). What we care about here is finite, bounded
190
        // output and no runaway.
191
        assert!(max_err < 1.0, "max_err {max_err}");
192
    }
193
194
    #[test]
195
    fn upsampler_handles_impulse() {
196
        let h = fir(15, 4);
197
        let mut up = PolyphaseUpsampler::new(4, &h);
198
        let mut buf = [0.0_f32; 8];
199
        up.process(1.0, &mut buf);
200
        // Some non-zero output expected on first impulse already.
201
        assert!(buf[..4].iter().any(|&v| v.abs() > 1e-6));
202
        // Drive zeros; output decays to zero.
203
        for _ in 0..200 {
204
            up.process(0.0, &mut buf);
205
        }
206
        assert!(buf[..4].iter().all(|&v| v.abs() < 1e-6));
207
    }
208
}