voice_bird_cli/audio/
resample.rs1use 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, )?;
25 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; 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 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 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 assert!(out.iter().all(|&s| s.abs() < 0.01));
121 }
122}