gemini_adk_fluent_rs/voice/dsp/
resample.rs1use std::collections::VecDeque;
17
18use rubato::{
19 Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
20};
21
22use super::{AudioBus, DspStage};
23
24pub struct SincResampler {
26 inner: SincFixedIn<f32>,
27 from_hz: u32,
28 to_hz: u32,
29 chunk: usize,
30 input_fifo: VecDeque<f32>,
31 in_scratch: Vec<Vec<f32>>,
32 out_scratch: Vec<Vec<f32>>,
33 output_fifo: VecDeque<f32>,
34 latency_out: usize,
35 in_total: u64,
37 out_total: u64,
39}
40
41impl SincResampler {
42 pub fn new(from_hz: u32, to_hz: u32) -> Self {
45 let chunk = (from_hz / 100).max(1) as usize; let params = SincInterpolationParameters {
47 sinc_len: 128,
48 f_cutoff: 0.95,
49 interpolation: SincInterpolationType::Linear,
50 oversampling_factor: 128,
51 window: WindowFunction::BlackmanHarris2,
52 };
53 let inner =
54 SincFixedIn::<f32>::new(f64::from(to_hz) / f64::from(from_hz), 1.0, params, chunk, 1)
55 .expect("valid resampler parameters");
56 let latency_out = inner.output_delay();
57 let in_scratch = vec![vec![0.0f32; chunk]];
58 let out_scratch = inner.output_buffer_allocate(true);
59 Self {
60 inner,
61 from_hz,
62 to_hz,
63 chunk,
64 input_fifo: VecDeque::with_capacity(chunk * 4),
65 in_scratch,
66 out_scratch,
67 output_fifo: VecDeque::with_capacity(chunk * 4),
68 latency_out,
69 in_total: 0,
70 out_total: 0,
71 }
72 }
73}
74
75impl DspStage for SincResampler {
76 fn name(&self) -> &'static str {
77 "resample"
78 }
79
80 fn process(&mut self, bus: &mut AudioBus) {
81 debug_assert_eq!(
82 bus.sample_rate, self.from_hz,
83 "SincResampler built for {} Hz fed {} Hz",
84 self.from_hz, bus.sample_rate
85 );
86 let in_len = bus.samples.len();
87 self.input_fifo.extend(bus.samples.iter().copied());
88
89 while self.input_fifo.len() >= self.chunk {
90 for slot in self.in_scratch[0].iter_mut() {
91 *slot = self.input_fifo.pop_front().unwrap_or(0.0);
92 }
93 let (_, out_len) = self
94 .inner
95 .process_into_buffer(&self.in_scratch, &mut self.out_scratch, None)
96 .expect("fixed-size chunk");
97 self.output_fifo
98 .extend(self.out_scratch[0][..out_len].iter().copied());
99 }
100
101 self.in_total += in_len as u64;
108 let due = self.in_total * u64::from(self.to_hz) / u64::from(self.from_hz);
109 let want = due.saturating_sub(self.out_total) as usize;
110 bus.samples.clear();
111 let take = want.min(self.output_fifo.len());
112 for _ in 0..take {
113 bus.samples
114 .push(self.output_fifo.pop_front().unwrap_or(0.0));
115 }
116 self.out_total += take as u64;
117 bus.sample_rate = self.to_hz;
118 }
119
120 fn latency_samples(&self) -> usize {
121 self.latency_out
124 + (self.chunk as u64 * u64::from(self.to_hz) / u64::from(self.from_hz)) as usize
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn sine(hz: f32, rate: u32, seconds: f32, amp: f32) -> Vec<f32> {
133 let n = (rate as f32 * seconds) as usize;
134 (0..n)
135 .map(|i| amp * (2.0 * std::f32::consts::PI * hz * i as f32 / rate as f32).sin())
136 .collect()
137 }
138
139 fn run_in_chunks(stage: &mut SincResampler, input: &[f32], from_hz: u32) -> Vec<f32> {
140 let mut out = Vec::new();
141 let mut buf = Vec::new();
142 for chunk in input.chunks(173) {
143 buf.clear();
144 buf.extend_from_slice(chunk);
145 let mut bus = AudioBus {
146 samples: &mut buf,
147 sample_rate: from_hz,
148 };
149 stage.process(&mut bus);
150 out.extend_from_slice(&buf);
151 }
152 out
153 }
154
155 #[test]
156 fn preserves_tone_across_48k_to_16k() {
157 let mut stage = SincResampler::new(48_000, 16_000);
158 let input = sine(1_000.0, 48_000, 1.0, 0.5);
159 let out = run_in_chunks(&mut stage, &input, 48_000);
160 let skip = stage.latency_samples() * 2;
162 let tail = &out[skip..];
163 let rms = (tail.iter().map(|s| s * s).sum::<f32>() / tail.len() as f32).sqrt();
164 let expected = 0.5 / std::f32::consts::SQRT_2;
165 assert!(
166 (rms - expected).abs() / expected < 0.05,
167 "rms {rms} vs {expected}"
168 );
169 }
170
171 #[test]
172 fn rejects_content_above_target_nyquist() {
173 let mut stage = SincResampler::new(48_000, 16_000);
177 let input = sine(12_000.0, 48_000, 1.0, 0.5);
178 let out = run_in_chunks(&mut stage, &input, 48_000);
179 let skip = stage.latency_samples() * 2;
180 let tail = &out[skip..];
181 let rms = (tail.iter().map(|s| s * s).sum::<f32>() / tail.len() as f32).sqrt();
182 assert!(rms < 0.01, "alias energy leaked: rms {rms}");
183 }
184
185 #[test]
186 fn tiny_blocks_still_emit_the_full_rate_share() {
187 let mut stage = SincResampler::new(48_000, 16_000);
191 let mut emitted = 0usize;
192 let mut buf = Vec::with_capacity(1);
193 for _ in 0..48_000 {
194 buf.clear();
195 buf.push(0.25f32);
196 let mut bus = AudioBus {
197 samples: &mut buf,
198 sample_rate: 48_000,
199 };
200 stage.process(&mut bus);
201 emitted += buf.len();
202 }
203 let expected = 16_000usize;
204 assert!(
205 (emitted as i64 - expected as i64).unsigned_abs() as usize
206 <= stage.latency_samples() + 320,
207 "emitted {emitted} of ~{expected}"
208 );
209 }
210
211 #[test]
212 fn output_rate_and_length_track_ratio() {
213 let mut stage = SincResampler::new(48_000, 16_000);
214 let input = vec![0.0f32; 48_000];
215 let out = run_in_chunks(&mut stage, &input, 48_000);
216 let expected = 16_000usize;
217 assert!(
218 (out.len() as i64 - expected as i64).unsigned_abs() as usize
219 <= stage.latency_samples() + 320,
220 "len {} vs ~{expected}",
221 out.len()
222 );
223 }
224}