Skip to main content

voice_bird_cli/audio/
resample.rs

1use rubato::{FftFixedInOut, Resampler as RubatoResampler};
2
3const TARGET_SR: u32 = 16_000;
4
5pub struct Resampler {
6    input_sr: u32,
7    channels: u16,
8    inner: Option<FftFixedInOut<f32>>,
9    chunk_size_in: usize,
10    leftover: Vec<f32>,
11}
12
13impl Resampler {
14    pub fn new(input_sr: u32, channels: u16) -> anyhow::Result<Self> {
15        let requested = 1024.max((input_sr as usize) / 50);
16        let (inner, chunk_size_in) = if input_sr == TARGET_SR {
17            (None, requested)
18        } else {
19            let r = FftFixedInOut::new(
20                input_sr as usize,
21                TARGET_SR as usize,
22                requested,
23                1, // output channels — we downmix to mono upstream
24            )?;
25            // rubato 0.14 derives the actual required input chunk size from the
26            // sample-rate ratio; the value passed to `new` is only a hint.
27            let actual = r.input_frames_next();
28            (Some(r), actual)
29        };
30        Ok(Self {
31            input_sr,
32            channels,
33            inner,
34            chunk_size_in,
35            leftover: Vec::new(),
36        })
37    }
38
39    pub fn process(&mut self, interleaved: &[f32]) -> anyhow::Result<Vec<f32>> {
40        let mono = downmix(interleaved, self.channels);
41
42        if self.inner.is_none() {
43            return Ok(mono);
44        }
45
46        let mut buf = std::mem::take(&mut self.leftover);
47        buf.extend_from_slice(&mono);
48
49        let mut out = Vec::new();
50        while buf.len() >= self.chunk_size_in {
51            let chunk = &buf[..self.chunk_size_in];
52            let input_channels = vec![chunk.to_vec()];
53            let resampled = self
54                .inner
55                .as_mut()
56                .unwrap()
57                .process(&input_channels, None)?;
58            out.extend_from_slice(&resampled[0]);
59            buf.drain(..self.chunk_size_in);
60        }
61        self.leftover = buf;
62        Ok(out)
63    }
64}
65
66fn downmix(interleaved: &[f32], channels: u16) -> Vec<f32> {
67    if channels <= 1 {
68        return interleaved.to_vec();
69    }
70    let ch = channels as usize;
71    let mut out = Vec::with_capacity(interleaved.len() / ch);
72    for frame in interleaved.chunks_exact(ch) {
73        let sum: f32 = frame.iter().sum();
74        out.push(sum / ch as f32);
75    }
76    out
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn identity_when_already_16k_mono() {
85        let input: Vec<f32> = (0..16_000).map(|i| (i as f32 / 16_000.0).sin()).collect();
86        let mut r = Resampler::new(16_000, 1).unwrap();
87        let out = r.process(&input).unwrap();
88        assert!((out.len() as i64 - input.len() as i64).abs() < 32);
89    }
90
91    #[test]
92    fn downsample_48k_to_16k_preserves_duration() {
93        let sr_in = 48_000;
94        let len = 48_000; // 1 second
95        let input: Vec<f32> = (0..len)
96            .map(|i| (i as f32 / 48_000.0 * 440.0 * std::f32::consts::TAU).sin())
97            .collect();
98        let mut r = Resampler::new(sr_in, 1).unwrap();
99        let out = r.process(&input).unwrap();
100        // Expect ~16_000 samples (±5%)
101        let expected = 16_000;
102        let diff = (out.len() as i64 - expected).abs();
103        assert!(
104            diff < (expected as f32 * 0.05) as i64,
105            "out.len = {}, expected ~{}",
106            out.len(),
107            expected
108        );
109    }
110
111    #[test]
112    fn stereo_downmix_to_mono() {
113        // interleaved [L,R,L,R,...]
114        let input: Vec<f32> = (0..16_000 * 2)
115            .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
116            .collect();
117        let mut r = Resampler::new(16_000, 2).unwrap();
118        let out = r.process(&input).unwrap();
119        // Downmix should produce ~0 for all samples (1 + -1)/2 = 0
120        assert!(out.iter().all(|&s| s.abs() < 0.01));
121    }
122}