subx_cli/services/vad/
resample.rs

1//!
2//! Audio resampling utilities using the rubato crate.
3//! Supports i16 <-> f32 conversion and multi-channel processing.
4
5use log::{debug, trace};
6use rubato::{FftFixedIn, Resampler};
7use std::error::Error;
8use std::time::Instant;
9
10use once_cell::sync::Lazy;
11use std::sync::Mutex;
12
13type FftResamplerCache = Option<(u32, u32, FftFixedIn<f32>)>;
14
15static FFT_RESAMPLER_CACHE: Lazy<Mutex<FftResamplerCache>> = Lazy::new(|| Mutex::new(None));
16
17/// Resample i16 mono audio to the target sample rate (returns `Vec<i16>`).
18pub fn resample_to_target_rate(
19    input_samples: &[i16],
20    input_sample_rate: u32,
21    output_sample_rate: u32,
22) -> Result<Vec<i16>, Box<dyn Error>> {
23    let total_start = Instant::now();
24    debug!(
25        "[resample] input_samples: {} input_sample_rate: {} output_sample_rate: {}",
26        input_samples.len(),
27        input_sample_rate,
28        output_sample_rate
29    );
30    if input_sample_rate == output_sample_rate {
31        debug!("[resample] sample rate unchanged, fast path");
32        return Ok(input_samples.to_vec());
33    }
34    let t_convert = Instant::now();
35    // Convert to f32 and minimize allocation and copying
36    let input: Vec<f32> = input_samples.iter().map(|&s| s as f32 / 32768.0).collect();
37    debug!(
38        "[resample] i16->f32 conversion done in {:.3?}",
39        t_convert.elapsed()
40    );
41    let input_len = input.len();
42    let input_channels = 1;
43    let resample_ratio = output_sample_rate as f64 / input_sample_rate as f64;
44    let chunk_size = 8192; // Increase chunk size to reduce the number of function calls
45    let t_resampler_init = Instant::now();
46    let mut resampler = {
47        let mut cache = FFT_RESAMPLER_CACHE.lock().unwrap();
48        if let Some((in_sr, out_sr, ref mut cached)) = *cache {
49            if in_sr == input_sample_rate && out_sr == output_sample_rate {
50                debug!("[resample] using cached FftFixedIn");
51                cached.reset();
52                let mut new_resampler = FftFixedIn::<f32>::new(
53                    input_sample_rate as usize,
54                    output_sample_rate as usize,
55                    chunk_size,
56                    1, // sub_chunks
57                    input_channels,
58                )?;
59                std::mem::swap(&mut new_resampler, cached);
60                new_resampler
61            } else {
62                debug!("[resample] creating new FftFixedIn (cache miss)");
63                let new_resampler = FftFixedIn::<f32>::new(
64                    input_sample_rate as usize,
65                    output_sample_rate as usize,
66                    chunk_size,
67                    1, // sub_chunks
68                    input_channels,
69                )?;
70                *cache = Some((input_sample_rate, output_sample_rate, new_resampler));
71                let mut new_resampler = FftFixedIn::<f32>::new(
72                    input_sample_rate as usize,
73                    output_sample_rate as usize,
74                    chunk_size,
75                    1, // sub_chunks
76                    input_channels,
77                )?;
78                std::mem::swap(&mut new_resampler, &mut cache.as_mut().unwrap().2);
79                new_resampler
80            }
81        } else {
82            debug!("[resample] creating new FftFixedIn (cache empty)");
83            let new_resampler = FftFixedIn::<f32>::new(
84                input_sample_rate as usize,
85                output_sample_rate as usize,
86                chunk_size,
87                1, // sub_chunks
88                input_channels,
89            )?;
90            *cache = Some((input_sample_rate, output_sample_rate, new_resampler));
91            let mut new_resampler = FftFixedIn::<f32>::new(
92                input_sample_rate as usize,
93                output_sample_rate as usize,
94                chunk_size,
95                1, // sub_chunks
96                input_channels,
97            )?;
98            std::mem::swap(&mut new_resampler, &mut cache.as_mut().unwrap().2);
99            new_resampler
100        }
101    };
102    debug!(
103        "[resample] FftFixedIn ready in {:.3?}",
104        t_resampler_init.elapsed()
105    );
106    let mut output: Vec<f32> =
107        Vec::with_capacity((input_len as f64 * resample_ratio) as usize + 128);
108    let mut pos = 0;
109    let t_resample = Instant::now();
110    let mut chunk_count = 0;
111    // Correction: each chunk must be input frame of chunk_size, pad with 0 if not enough at the end
112    while pos < input_len {
113        let frames_needed = resampler.input_frames_next();
114        let end = (pos + frames_needed).min(input_len);
115        let mut chunk: Vec<f32> = Vec::with_capacity(frames_needed);
116        chunk.extend_from_slice(&input[pos..end]);
117        if end - pos < frames_needed {
118            // Pad with 0 until frames_needed is reached
119            chunk.resize(frames_needed, 0.0);
120        }
121        let chunk_ref = [&chunk[..]];
122        trace!(
123            "[resample] chunk {}: pos={} end={} frames_needed={}",
124            chunk_count, pos, end, frames_needed
125        );
126        let out_chunk = resampler.process(&chunk_ref, None)?;
127        output.extend_from_slice(&out_chunk[0]);
128        pos += frames_needed;
129        chunk_count += 1;
130    }
131    debug!(
132        "[resample] main resample loop done in {:.3?} ({} chunks)",
133        t_resample.elapsed(),
134        chunk_count
135    );
136    // flush
137    let t_flush = Instant::now();
138    let mut flush_count = 0;
139    loop {
140        let out_chunk = resampler.process_partial::<Vec<f32>>(None, None)?;
141        if out_chunk[0].is_empty() {
142            break;
143        }
144        output.extend_from_slice(&out_chunk[0]);
145        flush_count += 1;
146    }
147    debug!(
148        "[resample] flush done in {:.3?} ({} flushes)",
149        t_flush.elapsed(),
150        flush_count
151    );
152    // f32 -> i16
153    let t_i16 = Instant::now();
154    // Correction: only keep samples with correct length
155    let expected_len = ((input_samples.len() as f64) * resample_ratio).round() as usize;
156    let mut output_i16: Vec<i16> = output
157        .iter()
158        .map(|&s| (s.clamp(-1.0, 1.0) * 32767.0) as i16)
159        .collect();
160    if output_i16.len() > expected_len {
161        output_i16.truncate(expected_len);
162    }
163    debug!(
164        "[resample] f32->i16 conversion done in {:.3?}",
165        t_i16.elapsed()
166    );
167    debug!(
168        "[resample] total elapsed: {:.3?} (input {} -> output {} samples)",
169        total_start.elapsed(),
170        input_samples.len(),
171        output_i16.len()
172    );
173    Ok(output_i16)
174}