Skip to main content

audio_codec/
resampler.rs

1use core::f64::consts::PI as PI_F64;
2
3use super::{CodecError, Sample};
4
5#[cfg(feature = "std")]
6use super::PcmBuf;
7
8/// Number of polyphase filter phases.
9pub const NUM_PHASES: usize = 256;
10/// Number of filter taps per phase.
11pub const TAPS_PER_PHASE: usize = 24;
12/// Required length of the caller-provided coefficient buffer (`NUM_PHASES * TAPS_PER_PHASE`).
13pub const COEFFS_LEN: usize = NUM_PHASES * TAPS_PER_PHASE;
14
15/// `f64::sin` polyfill that works in both std and no_std (via libm).
16#[inline]
17fn fsin(x: f64) -> f64 {
18    #[cfg(feature = "std")]
19    {
20        f64::sin(x)
21    }
22    #[cfg(not(feature = "std"))]
23    {
24        libm::sin(x)
25    }
26}
27
28/// `f64::sqrt` polyfill.
29#[inline]
30fn fsqrt(x: f64) -> f64 {
31    #[cfg(feature = "std")]
32    {
33        f64::sqrt(x)
34    }
35    #[cfg(not(feature = "std"))]
36    {
37        libm::sqrt(x)
38    }
39}
40
41pub struct Resampler<'a> {
42    input_rate: usize,
43    output_rate: usize,
44    ratio: f64,
45    coeffs: &'a mut [f32],
46    num_phases: usize,
47    taps_per_phase: usize,
48    history: [f32; TAPS_PER_PHASE],
49    current_pos: f64,
50}
51
52fn bessel_i0(x: f64) -> f64 {
53    let mut sum = 1.0_f64;
54    let mut term = 1.0_f64;
55    let x_sq = x * x * 0.25;
56
57    for m in 1..=30 {
58        term *= x_sq / (m * m) as f64;
59        sum += term;
60        if term < 1e-15 * sum {
61            break;
62        }
63    }
64    sum
65}
66
67fn kaiser_window(n: usize, n_total: usize, beta: f64) -> f64 {
68    if n_total <= 1 {
69        return 1.0;
70    }
71    let alpha = (n_total - 1) as f64 / 2.0;
72    let x = (n as f64 - alpha) / alpha;
73    let arg = beta * fsqrt(1.0 - x * x);
74    bessel_i0(arg) / bessel_i0(beta)
75}
76
77impl<'a> Resampler<'a> {
78    /// Create a new `Resampler`, writing polyphase filter coefficients into the
79    /// caller-provided buffer.
80    ///
81    /// `coeffs.len()` must be at least [`COEFFS_LEN`] (= 6144 floats, ~24 KB).
82    /// The buffer is held by the resampler for its entire lifetime; it is
83    /// written once here and read on every subsequent `resample_into` call.
84    pub fn new(
85        input_rate: usize,
86        output_rate: usize,
87        coeffs: &'a mut [f32],
88    ) -> Result<Self, CodecError> {
89        if coeffs.len() < COEFFS_LEN {
90            return Err(CodecError::BufferTooSmall);
91        }
92        if input_rate == 0 || output_rate == 0 {
93            return Err(CodecError::InvalidInput);
94        }
95
96        const KAISER_BETA: f64 = 7.0;
97
98        let ratio = output_rate as f64 / input_rate as f64;
99        let num_phases = NUM_PHASES;
100        let taps_per_phase = TAPS_PER_PHASE;
101        let filter_len = num_phases * taps_per_phase;
102
103        let coeffs = &mut coeffs[..filter_len];
104
105        let cutoff = if ratio < 1.0 {
106            ratio * 0.5 * 0.95
107        } else {
108            0.5 * 0.95
109        };
110
111        let center = (taps_per_phase as f64 - 1.0) / 2.0;
112
113        // Design the polyphase filter directly into the borrowed buffer.
114        // We use a fixed-size scratch array on the stack (24 f64 = 192 bytes).
115        let mut phase_coeffs: [f64; TAPS_PER_PHASE] = [0.0; TAPS_PER_PHASE];
116
117        for p in 0..num_phases {
118            let mut sum = 0.0_f64;
119
120            for t in 0..taps_per_phase {
121                let x = t as f64 - center - (p as f64 / num_phases as f64);
122
123                let sinc_val = if x.abs() < 1e-10 {
124                    2.0 * cutoff
125                } else {
126                    let x_pi = x * PI_F64;
127                    fsin(x_pi * 2.0 * cutoff) / x_pi
128                };
129
130                let full_filter_idx = t * num_phases + p;
131                let window = kaiser_window(full_filter_idx, filter_len, KAISER_BETA);
132
133                phase_coeffs[t] = sinc_val * window;
134                sum += phase_coeffs[t];
135            }
136
137            for t in 0..taps_per_phase {
138                let normalized = (phase_coeffs[t] / sum) as f32;
139                coeffs[p * taps_per_phase + t] = normalized;
140            }
141        }
142
143        Ok(Self {
144            input_rate,
145            output_rate,
146            ratio,
147            coeffs,
148            num_phases,
149            taps_per_phase,
150            history: [0.0; TAPS_PER_PHASE],
151            current_pos: 0.0,
152        })
153    }
154
155    pub fn input_rate(&self) -> usize {
156        self.input_rate
157    }
158
159    pub fn output_rate(&self) -> usize {
160        self.output_rate
161    }
162
163    #[inline(always)]
164    fn dot_product(a: &[f32], b: &[f32]) -> f32 {
165        debug_assert_eq!(a.len(), TAPS_PER_PHASE);
166        debug_assert_eq!(b.len(), TAPS_PER_PHASE);
167
168        #[cfg(target_arch = "aarch64")]
169        {
170            // ARM NEON: 24 taps = 6 iterations of 4-wide vectors
171            unsafe {
172                use core::arch::aarch64::*;
173                let mut sumv = vdupq_n_f32(0.0);
174                for i in (0..TAPS_PER_PHASE).step_by(4) {
175                    let av = vld1q_f32(a.as_ptr().add(i));
176                    let bv = vld1q_f32(b.as_ptr().add(i));
177                    sumv = vfmaq_f32(sumv, av, bv);
178                }
179                vaddvq_f32(sumv)
180            }
181        }
182        #[cfg(all(target_arch = "x86_64", target_feature = "avx"))]
183        {
184            unsafe {
185                use core::arch::x86_64::*;
186                let mut sumv = _mm256_setzero_ps();
187                for i in (0..TAPS_PER_PHASE).step_by(8) {
188                    let av = _mm256_loadu_ps(a.as_ptr().add(i));
189                    let bv = _mm256_loadu_ps(b.as_ptr().add(i));
190                    sumv = _mm256_add_ps(sumv, _mm256_mul_ps(av, bv));
191                }
192                // Horizontal sum
193                let x128 = _mm_add_ps(_mm256_extractf128_ps(sumv, 1), _mm256_castps256_ps128(sumv));
194                let x64 = _mm_add_ps(x128, _mm_movehl_ps(x128, x128));
195                let x32 = _mm_add_ss(x64, _mm_shuffle_ps(x64, x64, 0x55));
196                _mm_cvtss_f32(x32)
197            }
198        }
199        #[cfg(all(
200            target_arch = "x86_64",
201            target_feature = "sse2",
202            not(target_feature = "avx")
203        ))]
204        {
205            unsafe {
206                use core::arch::x86_64::*;
207                let mut sumv = _mm_setzero_ps();
208                for i in (0..TAPS_PER_PHASE).step_by(4) {
209                    let av = _mm_loadu_ps(a.as_ptr().add(i));
210                    let bv = _mm_loadu_ps(b.as_ptr().add(i));
211                    sumv = _mm_add_ps(sumv, _mm_mul_ps(av, bv));
212                }
213                let x64 = _mm_add_ps(sumv, _mm_shuffle_ps(sumv, sumv, 0x4e));
214                let x32 = _mm_add_ss(x64, _mm_shuffle_ps(x64, x64, 0x11));
215                _mm_cvtss_f32(x32)
216            }
217        }
218        #[cfg(not(any(
219            target_arch = "aarch64",
220            all(target_arch = "x86_64", target_feature = "sse2")
221        )))]
222        {
223            let mut s = 0.0f32;
224            for i in 0..TAPS_PER_PHASE {
225                s += a[i] * b[i];
226            }
227            s
228        }
229    }
230
231    /// Resample `input` into the caller-provided `out` buffer.
232    ///
233    /// Returns the number of samples written. Returns
234    /// [`CodecError::BufferTooSmall`] if `out` cannot hold the result; use
235    /// [`Self::max_output_samples`] to size it.
236    pub fn resample_into(
237        &mut self,
238        input: &[Sample],
239        out: &mut [Sample],
240    ) -> Result<usize, CodecError> {
241        if self.input_rate == self.output_rate {
242            if out.len() < input.len() {
243                return Err(CodecError::BufferTooSmall);
244            }
245            out[..input.len()].copy_from_slice(input);
246            return Ok(input.len());
247        }
248
249        let inv_ratio = 1.0 / self.ratio;
250        let taps = self.taps_per_phase;
251        let num_phases_f = self.num_phases as f64;
252
253        let mut written = 0usize;
254
255        for &sample in input {
256            self.history.copy_within(1..taps, 0);
257            self.history[taps - 1] = sample as f32;
258
259            while self.current_pos < 1.0 {
260                let phase_idx = (self.current_pos * num_phases_f) as usize;
261                let phase_idx = phase_idx.min(self.num_phases - 1); // Safety clamp
262                let offset = phase_idx * taps;
263                let phase_coeffs = &self.coeffs[offset..offset + taps];
264
265                let out_sample = Self::dot_product(phase_coeffs, &self.history);
266
267                if written >= out.len() {
268                    return Err(CodecError::BufferTooSmall);
269                }
270                out[written] = out_sample.clamp(i16::MIN as f32, i16::MAX as f32) as i16;
271                written += 1;
272                self.current_pos += inv_ratio;
273            }
274            self.current_pos -= 1.0;
275        }
276
277        Ok(written)
278    }
279
280    /// Upper bound on the number of samples `resample_into` will produce for
281    /// an input of `n_input` samples.
282    pub fn max_output_samples(&self, n_input: usize) -> usize {
283        if self.input_rate == self.output_rate {
284            return n_input;
285        }
286        ((n_input as f64 * self.ratio) as usize) + 1
287    }
288
289    /// Convenience wrapper that allocates a `Vec` and calls `resample_into`.
290    #[cfg(feature = "std")]
291    pub fn resample(&mut self, input: &[Sample]) -> PcmBuf {
292        let max = self.max_output_samples(input.len());
293        let mut out = vec![0i16; max];
294        match self.resample_into(input, &mut out) {
295            Ok(n) => {
296                out.truncate(n);
297                out
298            }
299            Err(_) => Vec::new(),
300        }
301    }
302
303    pub fn reset(&mut self) {
304        self.history.fill(0.0);
305        self.current_pos = 0.0;
306    }
307}
308
309/// One-shot resampling convenience helper (allocates).
310///
311/// Only available with the `std` feature.
312#[cfg(feature = "std")]
313pub fn resample(input: &[Sample], input_sample_rate: u32, output_sample_rate: u32) -> PcmBuf {
314    if input_sample_rate == output_sample_rate {
315        return input.to_vec();
316    }
317    let mut coeffs = vec![0.0f32; COEFFS_LEN];
318    let mut r = match Resampler::new(
319        input_sample_rate as usize,
320        output_sample_rate as usize,
321        &mut coeffs,
322    ) {
323        Ok(r) => r,
324        Err(_) => return Vec::new(),
325    };
326    r.resample(input)
327}
328
329/// Self-contained [`Resampler`] that owns its coefficient buffer (std only).
330///
331/// [`Resampler`] borrows a ~24 KB caller-provided coefficient slice, which
332/// makes it awkward to store as a long-lived struct field (the buffer must
333/// outlive the resampler). `BoxedResampler` heap-allocates the coefficients
334/// once and keeps them alive for the resampler's whole lifetime, restoring
335/// the pre-0.4 ergonomic `new(input_rate, output_rate)` + `resample(..)`
336/// call shape for std users that hold resamplers in struct fields.
337#[cfg(feature = "std")]
338pub struct BoxedResampler {
339    /// Heap allocation: the buffer address is stable for the box's lifetime
340    /// and never reallocated, which is what the `Resampler<'static>` borrow
341    /// below relies on. Field order matters for drop: `inner` (borrower)
342    /// must drop before `coeffs` (borrowed).
343    inner: Resampler<'static>,
344    coeffs: Box<[f32]>,
345}
346
347#[cfg(feature = "std")]
348impl BoxedResampler {
349    /// Create a resampler that owns its polyphase filter coefficients.
350    ///
351    /// Fails only for zero rates (the coefficient buffer is always sized
352    /// correctly internally).
353    pub fn new(input_rate: usize, output_rate: usize) -> Result<Self, CodecError> {
354        let mut coeffs = vec![0.0f32; COEFFS_LEN].into_boxed_slice();
355        // SAFETY: `coeffs` is a heap box whose address cannot change while
356        // the allocation lives. We extend the borrow to 'static solely to
357        // store both the buffer and its borrower in the same struct; the
358        // struct's field order drops `inner` before `coeffs`, and nothing
359        // else can reach the buffer (it is moved into `Self` right after).
360        let inner = unsafe {
361            let ptr = coeffs.as_mut_ptr();
362            let loan: &'static mut [f32] = core::slice::from_raw_parts_mut(ptr, coeffs.len());
363            Resampler::new(input_rate, output_rate, loan)?
364        };
365        Ok(Self { inner, coeffs })
366    }
367
368    /// Convenience wrapper that allocates the output buffer.
369    pub fn resample(&mut self, input: &[Sample]) -> PcmBuf {
370        self.inner.resample(input)
371    }
372
373    /// Resample into a caller-provided buffer (no allocation).
374    pub fn resample_into(
375        &mut self,
376        input: &[Sample],
377        out: &mut [Sample],
378    ) -> Result<usize, CodecError> {
379        self.inner.resample_into(input, out)
380    }
381
382    /// Upper bound on the output sample count for `n_input` input samples.
383    pub fn max_output_samples(&self, n_input: usize) -> usize {
384        self.inner.max_output_samples(n_input)
385    }
386
387    /// Reset the resampling history (e.g. after a source discontinuity).
388    pub fn reset(&mut self) {
389        self.inner.reset();
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use std::f32::consts::PI as PI_F32;
397    use std::time::Instant;
398
399    fn new_resampler(input_rate: usize, output_rate: usize) -> Resampler<'static> {
400        // Leak intentionally: tests are short-lived and we need a 'static reference.
401        let coeffs: &'static mut [f32] = Box::leak(vec![0.0f32; COEFFS_LEN].into_boxed_slice());
402        Resampler::new(input_rate, output_rate, coeffs).expect("resampler init")
403    }
404
405    #[test]
406    fn test_resample_8k_to_16k() {
407        let mut resampler = new_resampler(8000, 16000);
408        let input = vec![1000i16; 80];
409        let output = resampler.resample(&input);
410        assert!(output.len() >= 150 && output.len() <= 170);
411        for &s in &output[48..output.len().saturating_sub(48)] {
412            assert!((s - 1000).abs() < 100, "Value {} is too far from 1000", s);
413        }
414    }
415
416    #[test]
417    fn test_resample_16k_to_8k() {
418        let mut resampler = new_resampler(16000, 8000);
419        let input = vec![1000i16; 160];
420        let output = resampler.resample(&input);
421        assert!(output.len() >= 75 && output.len() <= 85);
422        let skip = output.len() / 4;
423        for &s in &output[skip..output.len() - skip] {
424            assert!((s - 1000).abs() < 100, "Value {} is too far from 1000", s);
425        }
426    }
427
428    #[test]
429    fn test_frequency_response_downsample() {
430        let mut resampler = new_resampler(16000, 8000);
431        let freq = 2000.0_f32; // Well below 4kHz Nyquist
432        let samples: Vec<i16> = (0..160)
433            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / 16000.0).sin() * 10000.0) as i16)
434            .collect();
435
436        let output = resampler.resample(&samples);
437
438        // Output should have similar amplitude (allowing for some attenuation)
439        let input_rms: f32 = samples
440            .iter()
441            .map(|&s| (s as f32).powi(2))
442            .sum::<f32>()
443            .sqrt()
444            / samples.len() as f32;
445        let output_rms: f32 = output
446            .iter()
447            .map(|&s| (s as f32).powi(2))
448            .sum::<f32>()
449            .sqrt()
450            / output.len() as f32;
451
452        assert!(
453            output_rms > input_rms * 0.7,
454            "Too much attenuation: input_rms={}, output_rms={}",
455            input_rms,
456            output_rms
457        );
458    }
459
460    #[test]
461    fn test_aliasing_suppression() {
462        let mut resampler = new_resampler(16000, 8000);
463        let freq = 7000.0_f32; // Above 4kHz Nyquist of output
464        let samples: Vec<i16> = (0..1600)
465            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / 16000.0).sin() * 10000.0) as i16)
466            .collect();
467
468        let output = resampler.resample(&samples);
469
470        let output_rms: f32 =
471            (output.iter().map(|&s| (s as f32).powi(2)).sum::<f32>() / output.len() as f32).sqrt();
472        let input_rms: f32 = 10000.0 / 1.414; // Expected RMS of sine wave with amplitude 10000
473
474        assert!(
475            output_rms < input_rms / 50.0,
476            "Aliasing not sufficiently suppressed: output_rms={}",
477            output_rms
478        );
479    }
480
481    #[test]
482    fn test_performance_48k_to_8k() {
483        let mut resampler = new_resampler(48000, 8000);
484        let input = vec![0i16; 48000];
485
486        let start = Instant::now();
487        let iterations = 100;
488        for _ in 0..iterations {
489            let _ = resampler.resample(&input);
490            resampler.reset();
491        }
492        let duration = start.elapsed();
493        let per_second = duration.as_secs_f64() / iterations as f64;
494        println!(
495            "Resampling 1s of 48kHz to 8kHz (24 taps) took: {:.4}ms",
496            per_second * 1000.0
497        );
498        assert!(
499            per_second < 0.1,
500            "Performance regression: {}ms",
501            per_second * 1000.0
502        );
503    }
504
505    #[test]
506    fn test_continuity_between_chunks() {
507        let input_rate = 16000;
508        let output_rate = 8000;
509
510        let freq = 1000.0_f32;
511        let total_samples = 3200;
512        let input: Vec<i16> = (0..total_samples)
513            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / input_rate as f32).sin() * 5000.0) as i16)
514            .collect();
515
516        let mut resampler1 = new_resampler(input_rate, output_rate);
517        let output1 = resampler1.resample(&input);
518
519        let mut resampler2 = new_resampler(input_rate, output_rate);
520        let mid = input.len() / 2;
521        let mut output2 = resampler2.resample(&input[..mid]);
522        output2.extend_from_slice(&resampler2.resample(&input[mid..]));
523
524        assert_eq!(output1.len(), output2.len(), "Output lengths differ");
525
526        let max_diff: i16 = output1
527            .iter()
528            .zip(output2.iter())
529            .map(|(a, b)| (a - b).abs())
530            .max()
531            .unwrap_or(0);
532
533        assert!(
534            max_diff < 100,
535            "Large discontinuity between chunks: max_diff={}",
536            max_diff
537        );
538    }
539
540    /// `BoxedResampler` must produce byte-identical output to the borrowed
541    /// `Resampler` fed the same coefficients, and survive being moved +
542    /// reused across many calls (stable self-referential borrow).
543    #[cfg(feature = "std")]
544    #[test]
545    fn test_boxed_resampler_matches_borrowed() {
546        let input_rate = 48000usize;
547        let output_rate = 8000usize;
548        let input: Vec<i16> = (0..4800)
549            .map(|i| ((i as f32 * 0.05).sin() * 8000.0) as i16)
550            .collect();
551
552        let expected = {
553            let mut coeffs = vec![0.0f32; COEFFS_LEN];
554            let mut borrowed = Resampler::new(input_rate, output_rate, &mut coeffs).unwrap();
555            borrowed.resample(&input)
556        };
557
558        let mut boxed = BoxedResampler::new(input_rate, output_rate).unwrap();
559        let expected_max = ((input.len() * output_rate + input_rate - 1) / input_rate) + 1;
560        assert_eq!(boxed.max_output_samples(input.len()), expected_max);
561        let got = boxed.resample(&input);
562        assert_eq!(
563            expected, got,
564            "BoxedResampler output must match borrowed Resampler"
565        );
566
567        // Reuse after move: the coefficient borrow must stay valid across
568        // moves. The resampler is a streaming state machine — a second
569        // `resample` of the same input keeps the tail of the previous run in
570        // its history, so only the output LENGTH is stable across the move.
571        let mut moved = boxed;
572        let again = moved.resample(&input);
573        assert_eq!(expected.len(), again.len());
574
575        // reset() clears the history and phase, so a fresh `resample` of the
576        // same input must reproduce the very first output exactly.
577        moved.reset();
578        let after_reset = moved.resample(&input);
579        assert_eq!(expected, after_reset);
580
581        assert!(
582            BoxedResampler::new(0, 8000).is_err(),
583            "zero input rate must error"
584        );
585        assert!(
586            BoxedResampler::new(48000, 0).is_err(),
587            "zero output rate must error"
588        );
589    }
590}
591#[cfg(test)]
592mod debug_probe2 {
593    use super::*;
594
595    #[cfg(feature = "std")]
596    #[test]
597    fn probe_order() {
598        let input: Vec<i16> = (0..4800)
599            .map(|i| ((i as f32 * 0.05).sin() * 8000.0) as i16)
600            .collect();
601        // borrowed FIRST, then boxed — same order as the failing test
602        let expected = {
603            let mut coeffs = vec![0.0f32; COEFFS_LEN];
604            let mut r = Resampler::new(48000, 8000, &mut coeffs).unwrap();
605            r.resample(&input)
606        };
607        let mut boxed = BoxedResampler::new(48000, 8000).unwrap();
608        let got = boxed.resample(&input);
609        let diffs: Vec<usize> = expected
610            .iter()
611            .zip(got.iter())
612            .enumerate()
613            .filter(|(_, (a, b))| a != b)
614            .map(|(i, _)| i)
615            .collect();
616        println!(
617            "diff_count={} first_diffs={:?} expected_len={} got_len={}",
618            diffs.len(),
619            &diffs[..diffs.len().min(8)],
620            expected.len(),
621            got.len()
622        );
623    }
624}