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