subx_cli/services/vad/
resample.rs1use audioadapter_buffers::direct::{SequentialSlice, SequentialSliceOfVecs};
12use log::{debug, trace};
13use rubato::{Fft, FixedSync, Resampler};
14use std::error::Error;
15use std::time::Instant;
16
17const CHUNK_SIZE: usize = 8192;
24
25pub fn resample_to_target_rate(
27 input_samples: &[i16],
28 input_sample_rate: u32,
29 output_sample_rate: u32,
30) -> Result<Vec<i16>, Box<dyn Error>> {
31 let total_start = Instant::now();
32 debug!(
33 "[resample] input_samples: {} input_sample_rate: {} output_sample_rate: {}",
34 input_samples.len(),
35 input_sample_rate,
36 output_sample_rate
37 );
38
39 if input_sample_rate == output_sample_rate {
40 debug!("[resample] sample rate unchanged, fast path");
41 return Ok(input_samples.to_vec());
42 }
43
44 if input_samples.is_empty() {
45 debug!("[resample] empty input, returning empty output");
46 return Ok(Vec::new());
47 }
48
49 let t_convert = Instant::now();
50 let input_f32: Vec<f32> = input_samples.iter().map(|&s| s as f32 / 32768.0).collect();
51 debug!(
52 "[resample] i16->f32 conversion done in {:.3?}",
53 t_convert.elapsed()
54 );
55
56 let input_len = input_f32.len();
57 let channels = 1;
58
59 let t_init = Instant::now();
63 let mut resampler = Fft::<f32>::new(
64 input_sample_rate as usize,
65 output_sample_rate as usize,
66 CHUNK_SIZE,
67 1,
68 channels,
69 FixedSync::Input,
70 )?;
71 debug!("[resample] Fft ready in {:.3?}", t_init.elapsed());
72
73 let needed_out = resampler.process_all_needed_output_len(input_len);
76 let mut output_f32: Vec<Vec<f32>> = vec![vec![0.0f32; needed_out]];
77
78 let in_adapter = SequentialSlice::new(&input_f32, channels, input_len)
79 .map_err(|e| format!("input adapter construction failed: {e}"))?;
80 let mut out_adapter = SequentialSliceOfVecs::new_mut(&mut output_f32, channels, needed_out)
81 .map_err(|e| format!("output adapter construction failed: {e}"))?;
82
83 let t_resample = Instant::now();
84 let (in_used, out_produced) =
85 resampler.process_all_into_buffer(&in_adapter, &mut out_adapter, input_len, None)?;
86 trace!(
87 "[resample] process_all_into_buffer consumed {} frames, produced {} frames",
88 in_used, out_produced
89 );
90 debug!("[resample] resampling done in {:.3?}", t_resample.elapsed());
91
92 let t_i16 = Instant::now();
93 let resample_ratio = output_sample_rate as f64 / input_sample_rate as f64;
94 let expected_len = ((input_samples.len() as f64) * resample_ratio).round() as usize;
95 let mut output_i16: Vec<i16> = output_f32[0]
96 .iter()
97 .take(out_produced)
98 .map(|&s| (s.clamp(-1.0, 1.0) * 32767.0) as i16)
99 .collect();
100 if output_i16.len() > expected_len {
101 output_i16.truncate(expected_len);
102 }
103 debug!(
104 "[resample] f32->i16 conversion done in {:.3?}",
105 t_i16.elapsed()
106 );
107 debug!(
108 "[resample] total elapsed: {:.3?} (input {} -> output {} samples)",
109 total_start.elapsed(),
110 input_samples.len(),
111 output_i16.len()
112 );
113 Ok(output_i16)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
122 fn identical_sample_rates_returns_input_unchanged() {
123 let input: Vec<i16> = (0..16_000).map(|i| (i % 1024) as i16).collect();
124 let out = resample_to_target_rate(&input, 16_000, 16_000).expect("resample succeeds");
125 assert_eq!(out, input);
126 }
127
128 #[test]
130 fn empty_input_returns_empty() {
131 let out = resample_to_target_rate(&[], 44_100, 16_000).expect("resample succeeds");
132 assert!(out.is_empty());
133 }
134
135 #[test]
139 fn downsample_44100_to_16000_length_matches_ratio() {
140 let input: Vec<i16> = (0..44_100).map(|i| (i % 1024) as i16).collect();
141 let out = resample_to_target_rate(&input, 44_100, 16_000).expect("resample succeeds");
142 let ratio = 16_000.0 / 44_100.0;
143 let expected = (input.len() as f64 * ratio).round() as usize;
144 assert!(
145 out.len().abs_diff(expected) <= 8,
146 "expected ~{expected} frames, got {}",
147 out.len()
148 );
149 }
150
151 #[test]
153 fn upsample_16000_to_48000_length_matches_ratio() {
154 let input: Vec<i16> = (0..16_000).map(|i| (i % 1024) as i16).collect();
155 let out = resample_to_target_rate(&input, 16_000, 48_000).expect("resample succeeds");
156 let ratio = 48_000.0 / 16_000.0;
157 let expected = (input.len() as f64 * ratio).round() as usize;
158 assert!(
159 out.len().abs_diff(expected) <= 8,
160 "expected ~{expected} frames, got {}",
161 out.len()
162 );
163 }
164}