1use rubato::Resampler as _;
4use rubato::audioadapter_buffers::direct::{SequentialSliceOfSlices, SequentialSliceOfVecs};
5
6use crate::Error;
7use crate::SAMPLE_RATE;
8
9fn channel_mean(frame: &[f32]) -> f32 {
13 let scale = f32::from(u16::try_from(frame.len()).unwrap_or(u16::MAX));
15 frame.iter().sum::<f32>() / scale
16}
17
18pub(crate) fn raw_peak(block: &[f32]) -> f32 {
21 block.iter().fold(0.0f32, |loudest, sample| loudest.max(sample.abs()))
22}
23
24const RESAMPLE_CHUNK: usize = 480;
27
28fn sinc_parameters() -> rubato::SincInterpolationParameters {
32 rubato::SincInterpolationParameters {
34 sinc_len: 256,
35 f_cutoff: Some(0.95),
36 oversampling_factor: 256,
37 interpolation: rubato::SincInterpolationType::Cubic,
38 window: rubato::WindowFunction::BlackmanHarris2,
39 }
40}
41
42pub fn resample_mono(pcm: &[f32], from: u32, to: u32) -> Result<Vec<f32>, Error> {
45 const CHUNK: usize = 1024;
46 if from == to {
47 return Ok(pcm.to_vec());
48 }
49 if from == 0 || to == 0 {
50 return Err(Error::Capture {
51 message: "a sample rate of zero cannot be converted".to_owned(),
52 });
53 }
54 if pcm.is_empty() {
55 return Ok(Vec::new());
56 }
57
58 let mut resampler = rubato::Async::<f32>::new_sinc(
59 f64::from(to) / f64::from(from),
60 1.0,
61 &sinc_parameters(),
62 CHUNK,
63 1,
64 rubato::FixedAsync::Input,
65 )
66 .map_err(|error| Error::Capture { message: error.to_string() })?;
67 let from_frames = usize::try_from(from).unwrap_or(usize::MAX);
68 let to_frames = usize::try_from(to).unwrap_or(usize::MAX);
69 let mut converted = Vec::with_capacity(
70 pcm.len().saturating_mul(to_frames).saturating_div(from_frames).saturating_add(CHUNK),
71 );
72 let mut buffer = vec![vec![0.0f32; resampler.output_frames_max()]];
73 let frames_out = buffer[0].len();
74
75 let mut position = 0;
76 while position < pcm.len() {
77 let input = &pcm[position..position + CHUNK.min(pcm.len() - position)];
78 let input_buffer =
79 SequentialSliceOfSlices::new(std::slice::from_ref(&input), 1, input.len())
80 .map_err(|error| Error::Capture { message: error.to_string() })?;
81 let mut output_buffer = SequentialSliceOfVecs::new_mut(&mut buffer, 1, frames_out)
82 .map_err(|error| Error::Capture { message: error.to_string() })?;
83 let indexing = rubato::Indexing::new().partial_len(input.len());
84 let produced = resampler
85 .process_into_buffer(&input_buffer, &mut output_buffer, Some(&indexing))
86 .map_err(|error| Error::Capture { message: error.to_string() })?
87 .1;
88 converted.extend_from_slice(&buffer[0][..produced]);
89 position += input.len();
90 }
91 let empty: [&[f32]; 1] = [&[]];
95 let input_buffer = SequentialSliceOfSlices::new(&empty, 1, 0)
96 .map_err(|error| Error::Capture { message: error.to_string() })?;
97 let mut output_buffer = SequentialSliceOfVecs::new_mut(&mut buffer, 1, frames_out)
98 .map_err(|error| Error::Capture { message: error.to_string() })?;
99 let indexing = rubato::Indexing::new().partial_len(0);
100 let flushed = resampler
101 .process_into_buffer(&input_buffer, &mut output_buffer, Some(&indexing))
102 .map_err(|error| Error::Capture { message: error.to_string() })?
103 .1;
104 converted.extend_from_slice(&buffer[0][..flushed]);
105 converted.truncate(pcm.len().saturating_mul(to_frames) / from_frames);
106 Ok(converted)
107}
108
109pub(crate) struct InputConverter {
120 channels: usize,
121 resampling: Option<Resampling>,
122}
123
124impl InputConverter {
125 pub(crate) fn new(channels: usize, device_rate: Option<u32>) -> Result<Self, Error> {
128 let resampling = device_rate.map(Resampling::new).transpose()?;
129 Ok(Self { channels, resampling })
130 }
131
132 pub(crate) fn push(&mut self, block: &[f32], out: &mut impl FnMut(&[f32], f32)) {
135 let peak = raw_peak(block);
136 match &mut self.resampling {
137 None => {
138 if self.channels <= 1 {
139 out(block, peak);
140 } else {
141 let mono: Vec<f32> =
142 block.chunks_exact(self.channels).map(channel_mean).collect();
143 out(&mono, peak);
144 }
145 }
146 Some(resampling) => resampling.push(block, self.channels, peak, out),
147 }
148 }
149}
150
151struct Resampling {
152 resampler: rubato::Async<f32>,
153 staging: Vec<f32>,
154 input: Vec<Vec<f32>>,
155 output: Vec<Vec<f32>>,
156 pending_peak: f32,
161}
162
163impl Resampling {
164 fn new(device_rate: u32) -> Result<Self, Error> {
165 let resampler = rubato::Async::<f32>::new_sinc(
166 f64::from(SAMPLE_RATE) / f64::from(device_rate),
167 1.0,
168 &sinc_parameters(),
169 RESAMPLE_CHUNK,
170 1,
171 rubato::FixedAsync::Input,
172 )
173 .map_err(|error| Error::Capture { message: error.to_string() })?;
174 let output = vec![vec![0.0f32; resampler.output_frames_max()]];
175 let staging = Vec::with_capacity(RESAMPLE_CHUNK * 4);
180 let input = vec![vec![0.0; RESAMPLE_CHUNK]];
181 Ok(Self { resampler, staging, input, output, pending_peak: 0.0 })
182 }
183
184 fn push(
185 &mut self,
186 block: &[f32],
187 channels: usize,
188 peak: f32,
189 out: &mut impl FnMut(&[f32], f32),
190 ) {
191 self.pending_peak = self.pending_peak.max(peak);
192 if channels <= 1 {
193 self.staging.extend_from_slice(block);
194 } else {
195 self.staging.extend(block.chunks_exact(channels).map(channel_mean));
196 }
197 while self.staging.len() >= RESAMPLE_CHUNK {
198 self.input[0].copy_from_slice(&self.staging[..RESAMPLE_CHUNK]);
199 let frames_out = self.output[0].len();
203 let input = SequentialSliceOfVecs::new(&self.input, 1, RESAMPLE_CHUNK);
204 let output = SequentialSliceOfVecs::new_mut(&mut self.output, 1, frames_out);
205 let processed = match (input, output) {
206 (Ok(input), Ok(mut output)) => self
207 .resampler
208 .process_into_buffer(&input, &mut output, None)
209 .map(|(_, produced)| produced)
210 .map_err(|error| error.to_string()),
211 (Err(error), _) | (_, Err(error)) => Err(error.to_string()),
212 };
213 let produced = match processed {
214 Ok(produced) => produced,
215 Err(error) => {
216 self.staging.clear();
219 tracing::warn!(
220 event_name = "resampler_block_rejected",
221 %error,
222 "resampler rejected a block; dropping it"
223 );
224 return;
225 }
226 };
227 self.staging.drain(..RESAMPLE_CHUNK);
228 out(&self.output[0][..produced], std::mem::take(&mut self.pending_peak));
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 const RATE: f32 = 16_000.0;
240
241 const SECOND: usize = 16_000;
243
244 fn feed_stereo(converter: &mut InputConverter, left: &[f32], right: &[f32]) -> Vec<f32> {
247 let mut mono = Vec::new();
248 for (l, r) in left.chunks(512).zip(right.chunks(512)) {
249 let block: Vec<f32> = l.iter().zip(r.iter()).flat_map(|(l, r)| [*l, *r]).collect();
250 converter.push(&block, &mut |samples, _| mono.extend_from_slice(samples));
251 }
252 mono
253 }
254
255 fn magnitude_at(pcm: &[f32], freq: f32) -> f32 {
259 let omega = std::f32::consts::TAU * freq / RATE;
260 let coeff = 2.0 * omega.cos();
261 let (mut s1, mut s2) = (0.0f32, 0.0f32);
262 for &sample in pcm {
263 let s = sample + coeff * s1 - s2;
264 s2 = s1;
265 s1 = s;
266 }
267 (s1 * s1 + s2 * s2 - coeff * s1 * s2).sqrt() / (RATE / 2.0)
268 }
269
270 fn interior_second(pcm: &[f32]) -> Vec<f32> {
273 pcm[pcm.len() - SECOND..].to_vec()
274 }
275
276 fn sine(freq: f32, gain: f32, rate: u32, seconds: u32) -> Vec<f32> {
278 let rate_f = f32::from(u16::try_from(rate).unwrap_or(u16::MAX));
279 let step = std::f32::consts::TAU * freq / rate_f;
280 let mut phase = 0.0f32;
281 (0..seconds * rate)
282 .map(|_| {
283 let sample = gain * phase.sin();
284 phase += step;
285 sample
286 })
287 .collect()
288 }
289
290 #[test]
291 fn a_48k_stereo_take_converts_to_mono_16k_keeping_its_tone_and_dropping_what_would_alias() {
292 let mut converter = InputConverter::new(2, Some(48_000)).expect("the resampler must build");
296 let pcm =
297 feed_stereo(&mut converter, &sine(440.0, 0.3, 48_000, 2), &sine(440.0, 0.1, 48_000, 2));
298 assert!(
299 (31_600..=32_400).contains(&pcm.len()),
300 "two seconds at 48 kHz must land as two seconds at 16 kHz, got {} samples",
301 pcm.len()
302 );
303 let pcm = interior_second(&pcm);
304 let kept = magnitude_at(&pcm, 440.0);
305 assert!(
306 (kept - 0.2).abs() < 0.02,
307 "the tone must come through at the downmixed level 0.2, got {kept}"
308 );
309 assert!(
310 magnitude_at(&pcm, 540.0) < 0.02,
311 "the energy must stay at the tone's own frequency: a wrong rate would move it"
312 );
313
314 let mut converter = InputConverter::new(2, Some(48_000)).expect("the resampler must build");
319 let pcm = feed_stereo(
320 &mut converter,
321 &sine(12_000.0, 0.25, 48_000, 2),
322 &sine(12_000.0, 0.25, 48_000, 2),
323 );
324 let aliased = magnitude_at(&interior_second(&pcm), 4_000.0);
325 assert!(
326 aliased < 0.005,
327 "a 12 kHz tone must be filtered away, not aliased into the speech band at 4 kHz, got {aliased}"
328 );
329 }
330
331 #[test]
332 fn a_native_16k_path_forwards_its_samples_through_todays_downmix() {
333 let mut converter =
334 InputConverter::new(2, None).expect("pass-through has nothing to build");
335 let mut out = Vec::new();
336 converter.push(&[1.0, 0.0, 0.5, -0.5], &mut |samples, _| out.extend_from_slice(samples));
337 assert_eq!(
338 out,
339 vec![0.5, 0.0],
340 "a stereo block on the native path must flow through today's exact downmix"
341 );
342 }
343
344 #[test]
345 fn one_callback_larger_than_a_resampler_chunk_is_consumed_in_full() {
346 let mut converter = InputConverter::new(2, Some(48_000)).expect("the resampler must build");
347 let mut produced = 0;
350 converter.push(&vec![0.0; 2 * 480 * 2], &mut |samples, _| produced += samples.len());
351 assert!(
352 (200..=320).contains(&produced),
353 "both chunks of a 960-frame callback must be resampled (116 then 160 frames), got {produced}"
354 );
355 }
356
357 #[test]
361 fn a_one_shot_resample_keeps_a_tone_and_filters_what_would_alias() {
362 let pcm = sine(440.0, 0.3, 24_000, 2);
363 let out = resample_mono(&pcm, 24_000, 16_000).expect("the resampler must build");
364 assert_eq!(out.len(), 32_000, "two seconds at 24 kHz is two seconds at 16 kHz");
365 let kept = magnitude_at(&interior_second(&out), 440.0);
366 assert!((kept - 0.3).abs() < 0.02, "the tone must come through at its level, got {kept}");
367
368 let pcm = sine(10_000.0, 0.25, 24_000, 2);
369 let out = resample_mono(&pcm, 24_000, 16_000).expect("the resampler must build");
370 let aliased = magnitude_at(&interior_second(&out), 6_000.0);
371 assert!(
372 aliased < 0.005,
373 "a 10 kHz tone must be filtered away, not aliased into the speech band at 6 kHz, got {aliased}"
374 );
375 }
376
377 #[test]
380 fn a_one_shot_resample_at_the_same_rate_returns_the_buffer() {
381 let pcm = vec![0.1, -0.2, 0.3];
382 assert_eq!(resample_mono(&pcm, 16_000, 16_000).unwrap(), pcm);
383 assert!(resample_mono(&[], 24_000, 16_000).unwrap().is_empty());
384 }
385
386 #[test]
387 fn the_raw_peak_reaches_the_caller_from_a_clipping_channel() {
388 let mut converter = InputConverter::new(2, Some(48_000)).expect("the resampler must build");
389 let mut block = vec![0.0f32; 2 * RESAMPLE_CHUNK];
393 for frame in block.as_chunks_mut::<2>().0 {
394 frame[0] = 1.0;
395 }
396 let mut peak = 0.0f32;
397 converter.push(&block, &mut |_, block_peak| peak = block_peak);
398 assert!(
399 (peak - 1.0).abs() < f32::EPSILON,
400 "the peak must be the raw channel's, not the mix's, got {peak}"
401 );
402 }
403}