gemini_adk_fluent_rs/voice/
resampler.rs1const TAPS_PER_PHASE: usize = 16;
21const CUTOFF: f64 = 0.9;
23
24#[derive(Debug, Clone)]
26pub struct StreamResampler {
27 up: usize,
29 down: usize,
31 bank: Vec<[f32; TAPS_PER_PHASE]>,
33 history: Vec<f32>,
35 cursor: u64,
38}
39
40fn gcd(a: u32, b: u32) -> u32 {
41 if b == 0 { a } else { gcd(b, a % b) }
42}
43
44impl StreamResampler {
45 pub fn new(from_hz: u32, to_hz: u32) -> Self {
48 let from_hz = from_hz.max(1);
49 let to_hz = to_hz.max(1);
50 let g = gcd(from_hz, to_hz);
51 let up = (to_hz / g) as usize;
52 let down = (from_hz / g) as usize;
53 if up == down {
54 return Self {
55 up: 1,
56 down: 1,
57 bank: Vec::new(),
58 history: Vec::new(),
59 cursor: 0,
60 };
61 }
62 let length = TAPS_PER_PHASE * up;
65 let fc = CUTOFF * 0.5 / up.max(down) as f64;
66 let centre = (length - 1) as f64 / 2.0;
67 let mut prototype = vec![0f64; length];
68 for (n, tap) in prototype.iter_mut().enumerate() {
69 let x = n as f64 - centre;
70 let sinc = if x == 0.0 {
71 2.0 * fc
72 } else {
73 (2.0 * std::f64::consts::PI * fc * x).sin() / (std::f64::consts::PI * x)
74 };
75 let w = n as f64 / (length - 1) as f64;
76 let blackman = 0.42 - 0.5 * (2.0 * std::f64::consts::PI * w).cos()
77 + 0.08 * (4.0 * std::f64::consts::PI * w).cos();
78 *tap = sinc * blackman * up as f64;
80 }
81 let bank = (0..up)
82 .map(|phase| {
83 let mut taps = [0f32; TAPS_PER_PHASE];
84 for (j, tap) in taps.iter_mut().enumerate() {
85 *tap = prototype[phase + j * up] as f32;
86 }
87 taps
88 })
89 .collect();
90 Self {
91 up,
92 down,
93 bank,
94 history: vec![0.0; TAPS_PER_PHASE - 1],
95 cursor: ((TAPS_PER_PHASE - 1) * up) as u64,
96 }
97 }
98
99 pub fn process(&mut self, input: &[i16]) -> Vec<i16> {
101 if self.up == self.down {
102 return input.to_vec();
103 }
104 self.history.extend(input.iter().map(|&s| f32::from(s)));
105 let available = self.history.len() as u64;
106 let mut out = Vec::with_capacity(input.len() * self.up / self.down + 1);
107 loop {
108 let newest = self.cursor / self.up as u64;
110 if newest >= available {
111 break;
112 }
113 let phase = (self.cursor % self.up as u64) as usize;
114 let taps = &self.bank[phase];
115 let newest = newest as usize;
116 let mut acc = 0f32;
117 for (j, &tap) in taps.iter().enumerate() {
118 acc += tap * self.history[newest - j];
119 }
120 out.push(acc.round().clamp(f32::from(i16::MIN), f32::from(i16::MAX)) as i16);
121 self.cursor += self.down as u64;
122 }
123 let keep_from = self.history.len() - (TAPS_PER_PHASE - 1);
125 self.history.drain(..keep_from);
126 self.cursor -= (keep_from * self.up) as u64;
127 out
128 }
129
130 pub fn reset(&mut self) {
132 if self.up != self.down {
133 self.history.clear();
134 self.history.resize(TAPS_PER_PHASE - 1, 0.0);
135 self.cursor = ((TAPS_PER_PHASE - 1) * self.up) as u64;
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn tone(hz: f64, rate: u32, samples: usize, amplitude: f64) -> Vec<i16> {
145 (0..samples)
146 .map(|n| {
147 (amplitude * (2.0 * std::f64::consts::PI * hz * n as f64 / f64::from(rate)).sin())
148 as i16
149 })
150 .collect()
151 }
152
153 fn rms(samples: &[i16]) -> f64 {
154 let s = &samples[samples.len() / 4..];
156 (s.iter().map(|&x| f64::from(x).powi(2)).sum::<f64>() / s.len() as f64).sqrt()
157 }
158
159 #[test]
160 fn durations_are_preserved() {
161 for (from, to) in [
162 (24_000, 8_000),
163 (8_000, 16_000),
164 (16_000, 24_000),
165 (48_000, 16_000),
166 ] {
167 let mut r = StreamResampler::new(from, to);
168 let out = r.process(&vec![0i16; from as usize]); let expected = to as usize;
170 assert!(
171 out.len().abs_diff(expected) <= 16,
172 "{from}->{to}: {}",
173 out.len()
174 );
175 }
176 }
177
178 #[test]
179 fn speech_band_passes_and_aliases_are_removed() {
180 let mut r = StreamResampler::new(24_000, 8_000);
182 let pass = r.process(&tone(1_000.0, 24_000, 24_000, 10_000.0));
183 let gain = rms(&pass) / (10_000.0 / 2f64.sqrt());
184 assert!((gain - 1.0).abs() < 0.05, "pass-band gain {gain}");
185
186 let mut r = StreamResampler::new(24_000, 8_000);
189 let alias = r.process(&tone(7_000.0, 24_000, 24_000, 10_000.0));
190 assert!(
191 rms(&alias) < 10_000.0 / 2f64.sqrt() * 0.01,
192 "alias rms {}",
193 rms(&alias)
194 );
195
196 let linear =
198 super::super::resample(&tone(7_000.0, 24_000, 24_000, 10_000.0), 24_000, 8_000);
199 assert!(rms(&linear) > 1_000.0);
200 }
201
202 #[test]
203 fn chunked_equals_whole() {
204 let input = tone(440.0, 8_000, 8_000, 8_000.0);
205 let mut whole = StreamResampler::new(8_000, 16_000);
206 let expected = whole.process(&input);
207 let mut chunked = StreamResampler::new(8_000, 16_000);
208 let mut got = Vec::new();
209 for chunk in input.chunks(160) {
210 got.extend(chunked.process(chunk));
211 }
212 assert_eq!(got, expected);
213 }
214
215 #[test]
216 fn equal_rates_pass_through() {
217 let mut r = StreamResampler::new(16_000, 16_000);
218 assert_eq!(r.process(&[1, 2, 3]), [1, 2, 3]);
219 }
220}