Skip to main content

aria_kit/audio/
resample.rs

1//! Conversion of mono audio to the canonical sample rate.
2
3use rubato::Resampler as _;
4use rubato::audioadapter_buffers::direct::InterleavedSlice;
5use rubato::{Fft, FixedSync};
6
7use super::CANONICAL_SAMPLE_RATE;
8
9const INPUT_CHUNK: usize = 1_024;
10
11/// Converts mono f32 audio at any sample rate to canonical 16 kHz.
12pub struct Resampler {
13    mode: Mode,
14}
15
16enum Mode {
17    Passthrough,
18    Fft(Box<FftState>),
19}
20
21impl Resampler {
22    pub fn new(sample_rate: u32) -> Result<Self, ResampleError> {
23        let mode = if sample_rate == CANONICAL_SAMPLE_RATE {
24            Mode::Passthrough
25        } else {
26            Mode::Fft(Box::new(FftState::new(sample_rate)?))
27        };
28        Ok(Self { mode })
29    }
30
31    /// Converts a whole clip at `sample_rate` in one call.
32    pub fn convert(sample_rate: u32, samples: &[f32]) -> Result<Vec<f32>, ResampleError> {
33        let mut resampler = Self::new(sample_rate)?;
34        let mut converted = Vec::new();
35        resampler.push(samples, |block| converted.extend_from_slice(block))?;
36        resampler.finish(|block| converted.extend_from_slice(block))?;
37        Ok(converted)
38    }
39
40    /// Hands each resampled block to `emit`.
41    pub fn push(
42        &mut self,
43        samples: &[f32],
44        mut emit: impl FnMut(&[f32]),
45    ) -> Result<(), ResampleError> {
46        match &mut self.mode {
47            Mode::Passthrough => {
48                emit(samples);
49                Ok(())
50            }
51            Mode::Fft(state) => state.push(samples, &mut emit),
52        }
53    }
54
55    /// Flushes the filter's tail, so the total output matches the input's
56    /// duration.
57    pub fn finish(mut self, mut emit: impl FnMut(&[f32])) -> Result<(), ResampleError> {
58        match &mut self.mode {
59            Mode::Passthrough => Ok(()),
60            Mode::Fft(state) => state.flush(&mut emit),
61        }
62    }
63}
64
65struct FftState {
66    fft: Fft<f32>,
67    sample_rate: u32,
68    chunk: Vec<f32>,
69    filled: usize,
70    output: Vec<f32>,
71    delay: usize,
72    samples_in: u64,
73    samples_out: u64,
74}
75
76impl FftState {
77    fn new(sample_rate: u32) -> Result<Self, ResampleError> {
78        let fft = Fft::<f32>::new(
79            sample_rate as usize,
80            CANONICAL_SAMPLE_RATE as usize,
81            INPUT_CHUNK,
82            1,
83            FixedSync::Input,
84        )
85        .map_err(ResampleError::initialize)?;
86        let chunk = vec![0.0; fft.input_frames_next()];
87        let output = vec![0.0; fft.output_frames_max()];
88        let delay = fft.output_delay();
89        Ok(Self {
90            fft,
91            sample_rate,
92            chunk,
93            filled: 0,
94            output,
95            delay,
96            samples_in: 0,
97            samples_out: 0,
98        })
99    }
100
101    fn push(
102        &mut self,
103        mut samples: &[f32],
104        emit: &mut impl FnMut(&[f32]),
105    ) -> Result<(), ResampleError> {
106        while !samples.is_empty() {
107            let count = samples.len().min(self.chunk.len() - self.filled);
108            self.chunk[self.filled..self.filled + count].copy_from_slice(&samples[..count]);
109            self.filled += count;
110            self.samples_in = self
111                .samples_in
112                .checked_add(count as u64)
113                .ok_or(ResampleError::SampleCountOverflow)?;
114            samples = &samples[count..];
115            if self.filled == self.chunk.len() {
116                self.process(emit)?;
117            }
118        }
119        Ok(())
120    }
121
122    /// Pads with silence until the output has caught up with the input.
123    fn flush(&mut self, emit: &mut impl FnMut(&[f32])) -> Result<(), ResampleError> {
124        while self.samples_out < self.target_samples() {
125            self.chunk[self.filled..].fill(0.0);
126            self.process(emit)?;
127        }
128        Ok(())
129    }
130
131    fn target_samples(&self) -> u64 {
132        (u128::from(self.samples_in) * u128::from(CANONICAL_SAMPLE_RATE)
133            / u128::from(self.sample_rate)) as u64
134    }
135
136    fn process(&mut self, emit: &mut impl FnMut(&[f32])) -> Result<(), ResampleError> {
137        let input = InterleavedSlice::new(&self.chunk, 1, self.chunk.len())
138            .map_err(ResampleError::process)?;
139        let output_capacity = self.output.len();
140        let mut output = InterleavedSlice::new_mut(&mut self.output, 1, output_capacity)
141            .map_err(ResampleError::process)?;
142        let (_, produced) = self
143            .fft
144            .process_into_buffer(&input, &mut output, None)
145            .map_err(ResampleError::process)?;
146        self.filled = 0;
147        let skipped = self.delay.min(produced);
148        self.delay -= skipped;
149        // Padding flushes the filter's tail but must not lengthen the output.
150        let count = (produced - skipped).min((self.target_samples() - self.samples_out) as usize);
151        emit(&self.output[skipped..skipped + count]);
152        self.samples_out += count as u64;
153        Ok(())
154    }
155}
156
157#[derive(Debug, thiserror::Error)]
158pub enum ResampleError {
159    #[error("failed to set up resampling to {CANONICAL_SAMPLE_RATE} Hz")]
160    Initialize(#[source] Box<dyn std::error::Error + Send + Sync>),
161
162    #[error("failed to resample audio to {CANONICAL_SAMPLE_RATE} Hz")]
163    Process(#[source] Box<dyn std::error::Error + Send + Sync>),
164
165    #[error("resampler input sample count overflowed")]
166    SampleCountOverflow,
167}
168
169impl ResampleError {
170    fn initialize(error: impl std::error::Error + Send + Sync + 'static) -> Self {
171        Self::Initialize(Box::new(error))
172    }
173
174    fn process(error: impl std::error::Error + Send + Sync + 'static) -> Self {
175        Self::Process(Box::new(error))
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    fn convert(rate: u32, input: &[f32], chunk: usize) -> Vec<f32> {
184        let mut resampler = Resampler::new(rate).unwrap();
185        let mut output = Vec::new();
186        for samples in input.chunks(chunk) {
187            resampler
188                .push(samples, |block| output.extend_from_slice(block))
189                .unwrap();
190        }
191        resampler
192            .finish(|block| output.extend_from_slice(block))
193            .unwrap();
194        output
195    }
196
197    #[test]
198    fn converting_a_whole_clip_matches_pushing_it_in_chunks() {
199        let input: Vec<f32> = (0..4_801)
200            .map(|index| (index as f32 * 0.01).sin())
201            .collect();
202
203        let converted = Resampler::convert(48_000, &input).unwrap();
204
205        assert_eq!(converted, convert(48_000, &input, 137));
206    }
207
208    #[test]
209    fn canonical_samples_pass_through_unchanged() {
210        let samples = [0.0, 0.25, -0.5, 1.0];
211        assert_eq!(convert(CANONICAL_SAMPLE_RATE, &samples, 3), samples);
212    }
213
214    #[test]
215    fn resampling_preserves_duration_across_chunk_boundaries() {
216        for count in [0, 1, 2, 3, 1_023, 1_024, 1_025, 48_017] {
217            let input = vec![0.5; count];
218            let output = convert(48_000, &input, 137);
219            assert_eq!(output.len(), count / 3, "input length {count}");
220            if output.len() > 1_000 {
221                assert!(
222                    output[100..output.len() - 100]
223                        .iter()
224                        .all(|x| (x - 0.5).abs() < 0.001)
225                );
226            }
227        }
228    }
229
230    #[test]
231    fn flushing_retains_an_impulse_near_the_end() {
232        let mut input = vec![0.0; 4_101];
233        input[4_080] = 1.0;
234        let output = convert(48_000, &input, 257);
235        assert_eq!(output.len(), 1_367);
236        assert!(output[1_350..].iter().any(|sample| sample.abs() > 0.1));
237        assert_eq!(output, convert(48_000, &input, input.len()));
238    }
239}