Skip to main content

truepeak/
resample.rs

1use std::f64::consts::PI;
2
3use crate::{Error, validate_rate};
4
5fn bessel_i0(x: f64) -> f64 {
6    let mut sum = 1.0;
7    let mut term = 1.0;
8    for k in 1..100 {
9        term *= (x / (2.0 * k as f64)).powi(2);
10        sum += term;
11        if term < sum * f64::EPSILON {
12            break;
13        }
14    }
15    sum
16}
17
18fn prototype(factor: u32, base_rate_hz: u32) -> Result<Vec<f64>, Error> {
19    validate_rate(base_rate_hz)?;
20    if !matches!(factor, 4 | 8 | 16) {
21        return Err(Error::InvalidConfig("factor must be 4, 8, or 16".into()));
22    }
23    let factor = factor as usize;
24    let attenuation = 100.0;
25    let beta = 0.1102 * (attenuation - 8.7);
26    let transition = PI * 0.186 / factor as f64;
27    let order = ((attenuation - 8.0) / (2.285 * transition)).ceil() as usize;
28    // The Kaiser estimate is approximate. A 10% length margin meets 100 dB
29    // at the transition edge; multiples of 2L give integer delay both ways.
30    let order = ((order as f64 * 1.1).ceil() as usize).div_ceil(2 * factor) * 2 * factor;
31    let mid = order / 2;
32    let window_scale = bessel_i0(beta);
33    let mut taps = vec![0.0; order + 1];
34    for n in 0..=mid {
35        let distance = n as f64 - mid as f64;
36        let sinc = if distance == 0.0 {
37            1.0 / factor as f64
38        } else {
39            (PI * distance / factor as f64).sin() / (PI * distance)
40        };
41        let r = distance / mid as f64;
42        let window = bessel_i0(beta * (1.0 - r * r).max(0.0).sqrt()) / window_scale;
43        taps[n] = sinc * window;
44        taps[order - n] = taps[n];
45    }
46    let sum: f64 = taps.iter().sum();
47    for tap in &mut taps {
48        *tap /= sum;
49    }
50    Ok(taps)
51}
52
53#[derive(Clone, Debug)]
54struct History {
55    samples: Vec<f64>,
56    next: usize,
57    len: usize,
58}
59
60impl History {
61    fn new(len: usize) -> Self {
62        Self {
63            samples: vec![0.0; 2 * len],
64            next: 0,
65            len,
66        }
67    }
68
69    fn push(&mut self, sample: f32) {
70        self.next = if self.next == 0 {
71            self.len - 1
72        } else {
73            self.next - 1
74        };
75        self.samples[self.next] = sample as f64;
76        self.samples[self.next + self.len] = sample as f64;
77    }
78
79    fn dot(&self, taps: &[f64]) -> f32 {
80        let samples = &self.samples[self.next..self.next + taps.len()];
81        // Fixed pairwise groups shorten the reduction dependency chain while
82        // preserving f64 precision and identical ordering across stream blocks.
83        let mut sum = 0.0;
84        let mut coefficients = taps.chunks_exact(8);
85        let mut history = samples.chunks_exact(8);
86        for (t, x) in coefficients.by_ref().zip(history.by_ref()) {
87            let a = t[0] * x[0] + t[1] * x[1];
88            let b = t[2] * x[2] + t[3] * x[3];
89            let c = t[4] * x[4] + t[5] * x[5];
90            let d = t[6] * x[6] + t[7] * x[7];
91            sum += (a + b) + (c + d);
92        }
93        for (&tap, &sample) in coefficients.remainder().iter().zip(history.remainder()) {
94            sum += tap * sample;
95        }
96        sum as f32
97    }
98
99    fn reset(&mut self) {
100        self.samples.fill(0.0);
101        self.next = 0;
102    }
103}
104
105fn validate_block(input: &[f32], finished: bool) -> Result<(), Error> {
106    if finished {
107        return Err(Error::InvalidInput(
108            "process after flush requires reset".into(),
109        ));
110    }
111    if input.iter().any(|sample| !sample.is_finite()) {
112        return Err(Error::InvalidInput("non-finite sample".into()));
113    }
114    Ok(())
115}
116
117/// Causal, zero-padded interpolation. State size depends only on the filter.
118#[derive(Clone, Debug)]
119pub struct Upsampler {
120    phases: Vec<Vec<f64>>,
121    history: History,
122    delay: usize,
123    active: bool,
124    finished: bool,
125}
126
127impl Upsampler {
128    /// Accepts factors 4, 8, or 16 and any base rate supported by the crate.
129    /// Invalid factors return [`Error::InvalidConfig`]; invalid rates return [`Error::InvalidInput`].
130    pub fn new(factor: u32, base_rate_hz: u32) -> Result<Self, Error> {
131        let taps = prototype(factor, base_rate_hz)?;
132        let factor = factor as usize;
133        let phases: Vec<Vec<f64>> = (0..factor)
134            .map(|phase| {
135                taps.iter()
136                    .skip(phase)
137                    .step_by(factor)
138                    .map(|tap| tap * factor as f64)
139                    .collect()
140            })
141            .collect();
142        let history = History::new(phases[0].len());
143        Ok(Self {
144            phases,
145            history,
146            delay: (taps.len() - 1) / 2,
147            active: false,
148            finished: false,
149        })
150    }
151
152    /// Appends exactly `input.len() * factor` samples, retaining the causal delay.
153    /// An invalid block leaves both state and output unchanged. After flush, reset first.
154    pub fn process(&mut self, input: &[f32], output: &mut Vec<f32>) -> Result<(), Error> {
155        validate_block(input, self.finished)?;
156        for &sample in input {
157            self.push(sample, output);
158        }
159        self.active |= !input.is_empty();
160        Ok(())
161    }
162
163    fn push(&mut self, sample: f32, output: &mut Vec<f32>) {
164        self.history.push(sample);
165        output.extend(self.phases.iter().map(|phase| self.history.dot(phase)));
166    }
167
168    /// Appends `2 * delay()` tail samples once; an empty stream has no tail.
169    /// Samples outside the supplied stream are zero, including before its start.
170    pub fn flush(&mut self, output: &mut Vec<f32>) {
171        if self.active && !self.finished {
172            for _ in 0..self.history.len - 1 {
173                self.push(0.0, output);
174            }
175        }
176        self.finished = true;
177    }
178
179    /// Exact group delay in oversampled output samples.
180    pub fn delay(&self) -> usize {
181        self.delay
182    }
183
184    /// Clears history and permits processing a new stream after flush.
185    pub fn reset(&mut self) {
186        self.history.reset();
187        self.active = false;
188        self.finished = false;
189    }
190}
191
192/// Causal FIR decimation, retaining every Lth filtered sample starting at index 0.
193#[derive(Clone, Debug)]
194pub struct Downsampler {
195    taps: Vec<f64>,
196    factor: usize,
197    history: History,
198    phase: usize,
199    active: bool,
200    finished: bool,
201}
202
203impl Downsampler {
204    /// Accepts factors 4, 8, or 16; `base_rate_hz` is the output sample rate.
205    /// Invalid factors return [`Error::InvalidConfig`]; invalid rates return [`Error::InvalidInput`].
206    pub fn new(factor: u32, base_rate_hz: u32) -> Result<Self, Error> {
207        let taps = prototype(factor, base_rate_hz)?;
208        let history = History::new(taps.len());
209        Ok(Self {
210            taps,
211            factor: factor as usize,
212            history,
213            phase: 0,
214            active: false,
215            finished: false,
216        })
217    }
218
219    /// Appends filtered base-rate samples. Partial L-sample groups carry across calls.
220    /// An invalid block leaves both state and output unchanged. After flush, reset first.
221    pub fn process(&mut self, input: &[f32], output: &mut Vec<f32>) -> Result<(), Error> {
222        validate_block(input, self.finished)?;
223        for &sample in input {
224            self.push(sample, output);
225        }
226        self.active |= !input.is_empty();
227        Ok(())
228    }
229
230    fn push(&mut self, sample: f32, output: &mut Vec<f32>) {
231        self.history.push(sample);
232        if self.phase == 0 {
233            output.push(self.history.dot(&self.taps));
234        }
235        self.phase = (self.phase + 1) & (self.factor - 1);
236    }
237
238    /// Drains the FIR using zero padding, preserving the decimation phase.
239    /// Repeated flushes and flushing an empty stream append nothing.
240    pub fn flush(&mut self, output: &mut Vec<f32>) {
241        if self.active && !self.finished {
242            for _ in 0..self.taps.len() - 1 {
243                self.push(0.0, output);
244            }
245        }
246        self.finished = true;
247    }
248
249    /// Exact group delay in base-rate output samples.
250    pub fn delay(&self) -> usize {
251        (self.taps.len() - 1) / (2 * self.factor)
252    }
253
254    /// Clears history and decimation phase, permitting a new stream after flush.
255    pub fn reset(&mut self) {
256        self.history.reset();
257        self.phase = 0;
258        self.active = false;
259        self.finished = false;
260    }
261}