Skip to main content

audio_codec/
resampler.rs

1//! Default polyphase FIR resampler (f32 core, exact integer timing).
2//!
3//! The filter is the classic Kaiser-windowed polyphase FIR (256 phases × 24
4//! taps). The read position is tracked on an integer grid of
5//! `input_rate * output_rate` ticks per second, so repeated 20 ms frames keep
6//! a **constant, exact** output count (8 kHz → 48 kHz is always 960 samples,
7//! never 959/961). The Q15 fixed-point core with the same timing lives in
8//! [`crate::resampler_fixed`] for targets without an FPU.
9
10use core::f64::consts::PI as PI_F64;
11
12use fearless_simd::{Level, Simd, SimdBase, SimdFloat, dispatch, f32x8};
13
14use super::{CodecError, Sample};
15
16#[cfg(feature = "std")]
17use super::PcmBuf;
18
19/// Number of polyphase filter phases.
20pub const NUM_PHASES: usize = 256;
21/// Number of filter taps per phase.
22pub const TAPS_PER_PHASE: usize = 24;
23/// Required length of the caller-provided coefficient buffer (`NUM_PHASES * TAPS_PER_PHASE`).
24pub const COEFFS_LEN: usize = NUM_PHASES * TAPS_PER_PHASE;
25
26/// `f64::sin` polyfill that works in both std and no_std (via libm).
27#[inline]
28fn fsin(x: f64) -> f64 {
29    #[cfg(feature = "std")]
30    {
31        f64::sin(x)
32    }
33    #[cfg(not(feature = "std"))]
34    {
35        libm::sin(x)
36    }
37}
38
39/// `f64::sqrt` polyfill.
40#[inline]
41fn fsqrt(x: f64) -> f64 {
42    #[cfg(feature = "std")]
43    {
44        f64::sqrt(x)
45    }
46    #[cfg(not(feature = "std"))]
47    {
48        libm::sqrt(x)
49    }
50}
51
52pub struct Resampler<'a> {
53    input_rate: usize,
54    output_rate: usize,
55    coeffs: &'a mut [f32],
56    /// Last [`TAPS_PER_PHASE`] input samples, oldest first.
57    history: [f32; TAPS_PER_PHASE],
58    /// Ticks from the newest input sample to the next output sample, on the
59    /// `input_rate * output_rate` grid (see [`Resampler::resample_into`]).
60    pos: usize,
61    /// SIMD level the filter loop is compiled for.
62    level: Level,
63}
64
65fn bessel_i0(x: f64) -> f64 {
66    let mut sum = 1.0_f64;
67    let mut term = 1.0_f64;
68    let x_sq = x * x * 0.25;
69
70    for m in 1..=30 {
71        term *= x_sq / (m * m) as f64;
72        sum += term;
73        if term < 1e-15 * sum {
74            break;
75        }
76    }
77    sum
78}
79
80fn kaiser_window(n: usize, n_total: usize, beta: f64) -> f64 {
81    if n_total <= 1 {
82        return 1.0;
83    }
84    let alpha = (n_total - 1) as f64 / 2.0;
85    let x = (n as f64 - alpha) / alpha;
86    let arg = beta * fsqrt(1.0 - x * x);
87    bessel_i0(arg) / bessel_i0(beta)
88}
89
90impl<'a> Resampler<'a> {
91    /// Create a new `Resampler`, writing polyphase filter coefficients into the
92    /// caller-provided buffer.
93    ///
94    /// `coeffs.len()` must be at least [`COEFFS_LEN`] (= 6144 floats, ~24 KB).
95    /// The buffer is held by the resampler for its entire lifetime; it is
96    /// written once here and read on every subsequent `resample_into` call.
97    pub fn new(
98        input_rate: usize,
99        output_rate: usize,
100        coeffs: &'a mut [f32],
101    ) -> Result<Self, CodecError> {
102        if coeffs.len() < COEFFS_LEN {
103            return Err(CodecError::BufferTooSmall);
104        }
105        if input_rate == 0 || output_rate == 0 {
106            return Err(CodecError::InvalidInput);
107        }
108        // Guard the integer tick grid's `pos * NUM_PHASES` against overflow
109        // (`pos < output_rate` in the hot loop, so this is the only product).
110        if input_rate.checked_add(output_rate).is_none()
111            || output_rate.checked_mul(NUM_PHASES).is_none()
112        {
113            return Err(CodecError::InvalidInput);
114        }
115
116        const KAISER_BETA: f64 = 7.0;
117
118        let ratio = output_rate as f64 / input_rate as f64;
119        let num_phases = NUM_PHASES;
120        let taps_per_phase = TAPS_PER_PHASE;
121        let filter_len = num_phases * taps_per_phase;
122
123        let coeffs = &mut coeffs[..filter_len];
124
125        let cutoff = if ratio < 1.0 {
126            ratio * 0.5 * 0.95
127        } else {
128            0.5 * 0.95
129        };
130
131        let center = (taps_per_phase as f64 - 1.0) / 2.0;
132
133        // Design the polyphase filter directly into the borrowed buffer.
134        // We use a fixed-size scratch array on the stack (24 f64 = 192 bytes).
135        let mut phase_coeffs: [f64; TAPS_PER_PHASE] = [0.0; TAPS_PER_PHASE];
136
137        for p in 0..num_phases {
138            let mut sum = 0.0_f64;
139
140            for t in 0..taps_per_phase {
141                let x = t as f64 - center - (p as f64 / num_phases as f64);
142
143                let sinc_val = if x.abs() < 1e-10 {
144                    2.0 * cutoff
145                } else {
146                    let x_pi = x * PI_F64;
147                    fsin(x_pi * 2.0 * cutoff) / x_pi
148                };
149
150                let full_filter_idx = t * num_phases + p;
151                let window = kaiser_window(full_filter_idx, filter_len, KAISER_BETA);
152
153                phase_coeffs[t] = sinc_val * window;
154                sum += phase_coeffs[t];
155            }
156
157            for t in 0..taps_per_phase {
158                let normalized = (phase_coeffs[t] / sum) as f32;
159                coeffs[p * taps_per_phase + t] = normalized;
160            }
161        }
162
163        Ok(Self {
164            input_rate,
165            output_rate,
166            coeffs,
167            history: [0.0; TAPS_PER_PHASE],
168            pos: 0,
169            level: default_level(),
170        })
171    }
172
173    pub fn input_rate(&self) -> usize {
174        self.input_rate
175    }
176
177    pub fn output_rate(&self) -> usize {
178        self.output_rate
179    }
180
181    /// 24-tap dot product as three 8-lane FMAs plus one horizontal sum.
182    #[inline(always)]
183    fn dot_product<S: Simd>(simd: S, a: &[f32; TAPS_PER_PHASE], b: &[f32; TAPS_PER_PHASE]) -> f32 {
184        let a0 = f32x8::from_slice(simd, &a[0..8]);
185        let a1 = f32x8::from_slice(simd, &a[8..16]);
186        let a2 = f32x8::from_slice(simd, &a[16..24]);
187        let b0 = f32x8::from_slice(simd, &b[0..8]);
188        let b1 = f32x8::from_slice(simd, &b[8..16]);
189        let b2 = f32x8::from_slice(simd, &b[16..24]);
190        a0.mul_add(b0, a1.mul_add(b1, a2 * b2)).reduce_sum()
191    }
192
193    /// Resample `input` into the caller-provided `out` buffer.
194    ///
195    /// Returns the number of samples written. Returns
196    /// [`CodecError::BufferTooSmall`] if `out` cannot hold the result; use
197    /// [`Self::max_output_samples`] to size it.
198    pub fn resample_into(
199        &mut self,
200        input: &[Sample],
201        out: &mut [Sample],
202    ) -> Result<usize, CodecError> {
203        if self.input_rate == self.output_rate {
204            if out.len() < input.len() {
205                return Err(CodecError::BufferTooSmall);
206            }
207            out[..input.len()].copy_from_slice(input);
208            return Ok(input.len());
209        }
210
211        // Exact number of outputs this call must produce (see the Q15 core for
212        // the same grid): outputs sit `input_rate` ticks apart starting at
213        // `pos`, and the call consumes `input.len() * output_rate` ticks.
214        // Anything else means frame sizes drift across chunk boundaries (the
215        // 0.4.x 959/961 bug); pinned by `debug_assert_eq!` below.
216        let expected: u128 = {
217            let ticks = (input.len() as u128) * (self.output_rate as u128);
218            if ticks > self.pos as u128 {
219                (ticks - self.pos as u128).div_ceil(self.input_rate as u128)
220            } else {
221                0
222            }
223        };
224
225        // Dispatch once per call so the whole loop is compiled with the
226        // selected target features and `dot_product` inlines into it.
227        let level = self.level;
228        let written = dispatch!(level, simd => self.resample_simd(simd, input, out))?;
229
230        debug_assert_eq!(
231            written as u128,
232            expected,
233            "resampler produced {written} samples, exact timing requires {expected} \
234             for {} inputs ({} -> {}): output sizes would drift across chunk \
235             boundaries",
236            input.len(),
237            self.input_rate,
238            self.output_rate,
239        );
240
241        Ok(written)
242    }
243
244    #[inline(always)]
245    fn resample_simd<S: Simd>(
246        &mut self,
247        simd: S,
248        input: &[Sample],
249        out: &mut [Sample],
250    ) -> Result<usize, CodecError> {
251        /// Input samples converted per block. Converting a whole block before
252        /// filtering (instead of pushing one sample at a time into the
253        /// history) avoids store-forwarding stalls: the wide window loads
254        /// would otherwise hit a just-written narrow store on every output.
255        const BLOCK: usize = 256;
256
257        // Integer tick grid: one input sample spans `interval` ticks and one
258        // output sample advances `step` ticks, so timing is exact and never
259        // rounded — repeated 20 ms frames keep a constant output count.
260        let (step, interval) = (self.input_rate, self.output_rate);
261        // One output step in whole input samples plus a fraction of one, and
262        // that fraction in phase units: `step_frac * 256 = dq * interval + dr`.
263        // `new` checked that `interval * NUM_PHASES` fits.
264        let (step_whole, step_frac) = (step / interval, step % interval);
265        let (dq, dr) = (
266            step_frac * NUM_PHASES / interval,
267            step_frac * NUM_PHASES % interval,
268        );
269
270        let coeffs: &[[f32; TAPS_PER_PHASE]; NUM_PHASES] =
271            self.coeffs.as_chunks().0[..NUM_PHASES].try_into().unwrap();
272        // `[history | block]`: the window for an output at block index `i`
273        // is `buf[i + 1..i + 1 + TAPS_PER_PHASE]`, i.e. the last
274        // `TAPS_PER_PHASE` samples up to and including sample `i`.
275        let mut buf = [0.0f32; TAPS_PER_PHASE + BLOCK];
276        let mut written = 0usize;
277
278        for chunk in input.chunks(BLOCK) {
279            let n = chunk.len();
280            buf[..TAPS_PER_PHASE].copy_from_slice(&self.history);
281            for (dst, &src) in buf[TAPS_PER_PHASE..TAPS_PER_PHASE + n]
282                .iter_mut()
283                .zip(chunk)
284            {
285                *dst = src as f32;
286            }
287
288            // `pos` counts ticks from the block's first sample; an output is
289            // due every `step` ticks before the block end.
290            let pos = self.pos as u64;
291            let end = n as u64 * interval as u64;
292            let count = if pos < end {
293                (end - pos).div_ceil(step as u64) as usize
294            } else {
295                0
296            };
297            let out_block = out
298                .get_mut(written..written + count)
299                .ok_or(CodecError::BufferTooSmall)?;
300
301            // The output position, kept as the input sample it follows plus
302            // the filter phase and the remainder below it:
303            // `pos = idx * interval + frac`, `frac * 256 = phase * interval + rem`.
304            // The phase is `frac * 256 / interval`; tracking it with carries
305            // gives the same phase without a division per output.
306            let mut idx = self.pos / interval;
307            let frac = self.pos % interval;
308            let mut phase = frac * NUM_PHASES / interval;
309            let mut rem = frac * NUM_PHASES % interval;
310
311            for slot in out_block {
312                // `idx < n <= BLOCK` and `phase < NUM_PHASES`, so the masks never
313                // change them; they let the compiler drop the bounds checks.
314                let i = idx & (BLOCK - 1);
315                let window = buf[i + 1..i + 1 + TAPS_PER_PHASE].try_into().unwrap();
316                // `as` saturates to the i16 range.
317                *slot = Self::dot_product(simd, &coeffs[phase & (NUM_PHASES - 1)], window) as i16;
318
319                // Advance one output (`step` ticks).
320                rem += dr;
321                if rem >= interval {
322                    rem -= interval;
323                    phase += 1;
324                }
325                phase += dq;
326                idx += step_whole + phase / NUM_PHASES;
327                phase %= NUM_PHASES;
328            }
329
330            written += count;
331            self.pos = (pos + count as u64 * step as u64 - end) as usize;
332            self.history.copy_from_slice(&buf[n..n + TAPS_PER_PHASE]);
333        }
334
335        Ok(written)
336    }
337
338    /// Upper bound on the number of samples `resample_into` will produce for
339    /// an input of `n_input` samples.
340    pub fn max_output_samples(&self, n_input: usize) -> usize {
341        if self.input_rate == self.output_rate {
342            return n_input;
343        }
344        // Exact bound: outputs (`input_rate` ticks apart) that fit in `n_input`
345        // input periods (`output_rate` ticks each); the most fit when `pos == 0`.
346        let count = (n_input as u128 * self.output_rate as u128).div_ceil(self.input_rate as u128);
347        count.min(usize::MAX as u128) as usize
348    }
349
350    /// Convenience wrapper that allocates a `Vec` and calls `resample_into`.
351    #[cfg(feature = "std")]
352    pub fn resample(&mut self, input: &[Sample]) -> PcmBuf {
353        let max = self.max_output_samples(input.len());
354        let mut out = vec![0i16; max];
355        match self.resample_into(input, &mut out) {
356            Ok(n) => {
357                out.truncate(n);
358                out
359            }
360            Err(_) => Vec::new(),
361        }
362    }
363
364    pub fn reset(&mut self) {
365        self.history.fill(0.0);
366        self.pos = 0;
367    }
368}
369
370/// Best SIMD level available: runtime-detected when possible (`std`, wasm32),
371/// otherwise the level implied by the compile-time target features.
372///
373/// Only AVX-512 is downgraded, to AVX2: the kernel is 8 lanes wide either
374/// way, but at the AVX-512 level LLVM also autovectorizes the buffer setup
375/// and sample conversion with 512-bit instructions. On Ice Lake that was
376/// 5-15% slower than AVX2 in 3 of 4 cases. Other levels are used as detected.
377fn default_level() -> Level {
378    let level = Level::try_detect().unwrap_or_else(Level::baseline);
379    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
380    if level.as_avx512().is_some()
381        && let Some(avx2) = level.as_avx2()
382    {
383        return avx2.level();
384    }
385    level
386}
387
388/// One-shot resampling convenience helper (allocates).
389///
390/// Only available with the `std` feature.
391#[cfg(feature = "std")]
392pub fn resample(input: &[Sample], input_sample_rate: u32, output_sample_rate: u32) -> PcmBuf {
393    if input_sample_rate == output_sample_rate {
394        return input.to_vec();
395    }
396    let mut coeffs = vec![0.0f32; COEFFS_LEN];
397    let mut r = match Resampler::new(
398        input_sample_rate as usize,
399        output_sample_rate as usize,
400        &mut coeffs,
401    ) {
402        Ok(r) => r,
403        Err(_) => return Vec::new(),
404    };
405    r.resample(input)
406}
407
408/// Self-contained [`Resampler`] that owns its coefficient buffer (std only).
409///
410/// [`Resampler`] borrows a ~24 KB caller-provided coefficient slice, which
411/// makes it awkward to store as a long-lived struct field (the buffer must
412/// outlive the resampler). `BoxedResampler` heap-allocates the coefficients
413/// once and keeps them alive for the resampler's whole lifetime, restoring
414/// the pre-0.4 ergonomic `new(input_rate, output_rate)` + `resample(..)`
415/// call shape for std users that hold resamplers in struct fields.
416#[cfg(feature = "std")]
417pub struct BoxedResampler {
418    /// Heap allocation: the buffer address is stable for the box's lifetime
419    /// and never reallocated, which is what the `Resampler<'static>` borrow
420    /// below relies on. Field order matters for drop: `inner` (borrower)
421    /// must drop before `coeffs` (borrowed).
422    inner: Resampler<'static>,
423    /// Kept alive only to back `inner`'s `'static` borrow; never read.
424    #[allow(dead_code)]
425    coeffs: Box<[f32]>,
426}
427
428#[cfg(feature = "std")]
429impl BoxedResampler {
430    /// Create a resampler that owns its polyphase filter coefficients.
431    ///
432    /// Fails only for zero rates (the coefficient buffer is always sized
433    /// correctly internally).
434    pub fn new(input_rate: usize, output_rate: usize) -> Result<Self, CodecError> {
435        let mut coeffs = vec![0.0f32; COEFFS_LEN].into_boxed_slice();
436        // SAFETY: `coeffs` is a heap box whose address cannot change while
437        // the allocation lives. We extend the borrow to 'static solely to
438        // store both the buffer and its borrower in the same struct; the
439        // struct's field order drops `inner` before `coeffs`, and nothing
440        // else can reach the buffer (it is moved into `Self` right after).
441        let inner = unsafe {
442            let ptr = coeffs.as_mut_ptr();
443            let loan: &'static mut [f32] = core::slice::from_raw_parts_mut(ptr, coeffs.len());
444            Resampler::new(input_rate, output_rate, loan)?
445        };
446        Ok(Self { inner, coeffs })
447    }
448
449    /// Convenience wrapper that allocates the output buffer.
450    pub fn resample(&mut self, input: &[Sample]) -> PcmBuf {
451        self.inner.resample(input)
452    }
453
454    /// Resample into a caller-provided buffer (no allocation).
455    pub fn resample_into(
456        &mut self,
457        input: &[Sample],
458        out: &mut [Sample],
459    ) -> Result<usize, CodecError> {
460        self.inner.resample_into(input, out)
461    }
462
463    /// Upper bound on the output sample count for `n_input` input samples.
464    pub fn max_output_samples(&self, n_input: usize) -> usize {
465        self.inner.max_output_samples(n_input)
466    }
467
468    /// Reset the resampling history (e.g. after a source discontinuity).
469    pub fn reset(&mut self) {
470        self.inner.reset();
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use alloc::boxed::Box;
478    use alloc::vec;
479    use alloc::vec::Vec;
480    use core::f32::consts::PI as PI_F32;
481    #[cfg(feature = "std")]
482    use std::time::Instant;
483
484    /// no_std stand-in for the std-only convenience `resample()`: same
485    /// contract (buffer sized by `max_output_samples`, truncated to the
486    /// produced count) so the whole suite also runs without `std`.
487    fn resample(r: &mut Resampler<'_>, input: &[Sample]) -> Vec<Sample> {
488        let mut out = vec![0i16; r.max_output_samples(input.len())];
489        let n = r
490            .resample_into(input, &mut out)
491            .expect("out sized by max_output_samples");
492        out.truncate(n);
493        out
494    }
495
496    fn new_resampler(input_rate: usize, output_rate: usize) -> Resampler<'static> {
497        // Leak intentionally: tests are short-lived and we need a 'static reference.
498        let coeffs: &'static mut [f32] = Box::leak(vec![0.0f32; COEFFS_LEN].into_boxed_slice());
499        Resampler::new(input_rate, output_rate, coeffs).expect("resampler init")
500    }
501
502    /// The exact integer tick grid must keep the output count constant across
503    /// repeated 20 ms frames (no 959/961 drift) — the whole point of the f32
504    /// core having the same timing as the Q15 core.
505    #[test]
506    fn test_exact_sample_counts_for_repeated_20ms_blocks() {
507        for input_rate in [8000, 16000, 44100, 48000] {
508            for output_rate in [8000, 16000, 44100, 48000] {
509                let mut r = new_resampler(input_rate, output_rate);
510                let input = vec![1000; input_rate / 50];
511                let mut output = vec![0; output_rate / 50];
512                for block in 0..10 {
513                    let n = r.resample_into(&input, &mut output).unwrap_or_else(|e| {
514                        panic!("{input_rate} -> {output_rate}, block {block}: {e:?}")
515                    });
516                    assert_eq!(
517                        n,
518                        output.len(),
519                        "{input_rate} -> {output_rate}, block {block}"
520                    );
521                }
522            }
523        }
524    }
525
526    /// 8 kHz -> 48 kHz (the phone-side to Opus path): every 20 ms frame must be
527    /// exactly 960 samples, first block included.
528    #[test]
529    fn test_8k_to_48k_is_always_960() {
530        let mut r = new_resampler(8000, 48000);
531        for block in 0..10 {
532            let out = resample(&mut r, &vec![1000i16; 160]);
533            assert_eq!(out.len(), 960, "block {block}");
534        }
535    }
536
537    #[test]
538    fn test_resample_8k_to_16k() {
539        let mut resampler = new_resampler(8000, 16000);
540        let input = vec![1000i16; 80];
541        let output = resample(&mut resampler, &input);
542        assert!(output.len() >= 150 && output.len() <= 170);
543        for &s in &output[48..output.len().saturating_sub(48)] {
544            assert!((s - 1000).abs() < 100, "Value {} is too far from 1000", s);
545        }
546    }
547
548    #[test]
549    fn test_resample_16k_to_8k() {
550        let mut resampler = new_resampler(16000, 8000);
551        let input = vec![1000i16; 160];
552        let output = resample(&mut resampler, &input);
553        assert!(output.len() >= 75 && output.len() <= 85);
554        let skip = output.len() / 4;
555        for &s in &output[skip..output.len() - skip] {
556            assert!((s - 1000).abs() < 100, "Value {} is too far from 1000", s);
557        }
558    }
559
560    #[test]
561    fn test_frequency_response_downsample() {
562        let mut resampler = new_resampler(16000, 8000);
563        let freq = 2000.0_f32; // Well below 4kHz Nyquist
564        let samples: Vec<i16> = (0..160)
565            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / 16000.0).sin() * 10000.0) as i16)
566            .collect();
567
568        let output = resample(&mut resampler, &samples);
569
570        // Output should have similar amplitude (allowing for some attenuation)
571        let input_rms: f32 = samples
572            .iter()
573            .map(|&s| (s as f32).powi(2))
574            .sum::<f32>()
575            .sqrt()
576            / samples.len() as f32;
577        let output_rms: f32 = output
578            .iter()
579            .map(|&s| (s as f32).powi(2))
580            .sum::<f32>()
581            .sqrt()
582            / output.len() as f32;
583
584        assert!(
585            output_rms > input_rms * 0.7,
586            "Too much attenuation: input_rms={}, output_rms={}",
587            input_rms,
588            output_rms
589        );
590    }
591
592    #[test]
593    fn test_aliasing_suppression() {
594        let mut resampler = new_resampler(16000, 8000);
595        let freq = 7000.0_f32; // Above 4kHz Nyquist of output
596        let samples: Vec<i16> = (0..1600)
597            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / 16000.0).sin() * 10000.0) as i16)
598            .collect();
599
600        let output = resample(&mut resampler, &samples);
601
602        let output_rms: f32 =
603            (output.iter().map(|&s| (s as f32).powi(2)).sum::<f32>() / output.len() as f32).sqrt();
604        let input_rms: f32 = 10000.0 / 1.414; // Expected RMS of sine wave with amplitude 10000
605
606        assert!(
607            output_rms < input_rms / 50.0,
608            "Aliasing not sufficiently suppressed: output_rms={}",
609            output_rms
610        );
611    }
612
613    // Uses `std::time::Instant`; the SIMD kernels themselves are covered by
614    // the portable tests above.
615    #[cfg(feature = "std")]
616    #[test]
617    fn test_performance_48k_to_8k() {
618        let mut resampler = new_resampler(48000, 8000);
619        let input = vec![0i16; 48000];
620
621        let start = Instant::now();
622        let iterations = 100;
623        for _ in 0..iterations {
624            let _ = resample(&mut resampler, &input);
625            resampler.reset();
626        }
627        let duration = start.elapsed();
628        let per_second = duration.as_secs_f64() / iterations as f64;
629        println!(
630            "Resampling 1s of 48kHz to 8kHz (24 taps) took: {:.4}ms",
631            per_second * 1000.0
632        );
633        assert!(
634            per_second < 0.1,
635            "Performance regression: {}ms",
636            per_second * 1000.0
637        );
638    }
639
640    #[test]
641    fn test_continuity_between_chunks() {
642        let input_rate = 16000;
643        let output_rate = 8000;
644
645        let freq = 1000.0_f32;
646        let total_samples = 3200;
647        let input: Vec<i16> = (0..total_samples)
648            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / input_rate as f32).sin() * 5000.0) as i16)
649            .collect();
650
651        let mut resampler1 = new_resampler(input_rate, output_rate);
652        let output1 = resample(&mut resampler1, &input);
653
654        let mut resampler2 = new_resampler(input_rate, output_rate);
655        let mid = input.len() / 2;
656        let mut output2 = resample(&mut resampler2, &input[..mid]);
657        output2.extend_from_slice(&resample(&mut resampler2, &input[mid..]));
658
659        assert_eq!(output1.len(), output2.len(), "Output lengths differ");
660
661        let max_diff: i16 = output1
662            .iter()
663            .zip(output2.iter())
664            .map(|(a, b)| (a - b).abs())
665            .max()
666            .unwrap_or(0);
667
668        assert!(
669            max_diff < 100,
670            "Large discontinuity between chunks: max_diff={}",
671            max_diff
672        );
673    }
674
675    /// `BoxedResampler` must produce byte-identical output to the borrowed
676    /// `Resampler` fed the same coefficients, and survive being moved +
677    /// reused across many calls (stable self-referential borrow).
678    #[cfg(feature = "std")]
679    #[test]
680    fn test_boxed_resampler_matches_borrowed() {
681        let input_rate = 48000usize;
682        let output_rate = 8000usize;
683        let input: Vec<i16> = (0..4800)
684            .map(|i| ((i as f32 * 0.05).sin() * 8000.0) as i16)
685            .collect();
686
687        let expected = {
688            let mut coeffs = vec![0.0f32; COEFFS_LEN];
689            let mut borrowed = Resampler::new(input_rate, output_rate, &mut coeffs).unwrap();
690            resample(&mut borrowed, &input)
691        };
692
693        let mut boxed = BoxedResampler::new(input_rate, output_rate).unwrap();
694        // Exact bound now (no `+1`): the integer tick grid never over-produces.
695        let expected_max = (input.len() * output_rate).div_ceil(input_rate);
696        assert_eq!(boxed.max_output_samples(input.len()), expected_max);
697        let got = boxed.resample(&input);
698        assert_eq!(
699            expected, got,
700            "BoxedResampler output must match borrowed Resampler"
701        );
702
703        // Reuse after move: the coefficient borrow must stay valid across
704        // moves. The resampler is a streaming state machine — a second
705        // `resample` of the same input keeps the tail of the previous run in
706        // its history, so only the output LENGTH is stable across the move.
707        let mut moved = boxed;
708        let again = moved.resample(&input);
709        assert_eq!(expected.len(), again.len());
710
711        // reset() clears the history and phase, so a fresh `resample` of the
712        // same input must reproduce the very first output exactly.
713        moved.reset();
714        let after_reset = moved.resample(&input);
715        assert_eq!(expected, after_reset);
716
717        assert!(
718            BoxedResampler::new(0, 8000).is_err(),
719            "zero input rate must error"
720        );
721        assert!(
722            BoxedResampler::new(48000, 0).is_err(),
723            "zero output rate must error"
724        );
725    }
726
727    /// Streaming in chunks of any size (around the history length and the
728    /// internal block size) keeps the running output count at exactly
729    /// `ceil(consumed * out / in)` and the samples identical to one call.
730    #[test]
731    fn test_exact_timing_survives_unaligned_chunks_and_reset() {
732        for (input_rate, output_rate) in [
733            (16000, 48000),
734            (48000, 16000),
735            (44100, 48000),
736            (48000, 44100),
737        ] {
738            let input: Vec<i16> = (0..2000).map(|i| ((i * 37 % 200) - 100) as i16).collect();
739            let mut r = new_resampler(input_rate, output_rate);
740            let expected = resample(&mut r, &input);
741            assert_eq!(
742                expected.len(),
743                (input.len() * output_rate).div_ceil(input_rate)
744            );
745            for chunk_size in [
746                1, 2, 3, 5, 7, 11, 23, 24, 25, 47, 88, 160, 255, 256, 257, 319,
747            ] {
748                r.reset();
749                let mut got = Vec::new();
750                let mut consumed = 0;
751                for chunk in input.chunks(chunk_size) {
752                    got.extend(resample(&mut r, chunk));
753                    consumed += chunk.len();
754                    assert_eq!(
755                        got.len(),
756                        (consumed * output_rate).div_ceil(input_rate),
757                        "{input_rate} -> {output_rate}, chunk {chunk_size}, consumed {consumed}"
758                    );
759                    assert!(resample(&mut r, &[]).is_empty());
760                }
761                assert_eq!(
762                    got, expected,
763                    "{input_rate} -> {output_rate}, chunk {chunk_size}"
764                );
765            }
766        }
767    }
768
769    const RATIOS: [(usize, usize); 8] = [
770        (8000, 16000),
771        (16000, 8000),
772        (48000, 8000),
773        (8000, 48000),
774        (44100, 48000),
775        (48000, 44100),
776        (8000, 22050),
777        (22050, 16000),
778    ];
779
780    /// Deterministic test signals: sine + LCG noise, and a full-scale square
781    /// wave whose filter overshoot exercises output saturation.
782    fn test_signals(len: usize) -> [Vec<i16>; 2] {
783        let mut seed = 0x1234_5678u32;
784        let noisy: Vec<i16> = (0..len)
785            .map(|i| {
786                seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
787                let noise = ((seed >> 16) as i16 as f32) * 0.2;
788                ((i as f32 * 0.07).sin() * 15000.0 + noise) as i16
789            })
790            .collect();
791        let square: Vec<i16> = (0..len)
792            .map(|i| {
793                if (i / 10) % 2 == 0 {
794                    i16::MAX
795                } else {
796                    i16::MIN
797                }
798            })
799            .collect();
800        [noisy, square]
801    }
802
803    /// Straightforward scalar implementation: per-sample shifting history,
804    /// f64 accumulation, explicit clamp, and each output's position derived
805    /// from its absolute index rather than a streaming accumulator.
806    fn reference_resample(coeffs: &[f32], fin: usize, fout: usize, input: &[i16]) -> Vec<i16> {
807        let mut history = [0.0f32; TAPS_PER_PHASE];
808        let mut out = Vec::new();
809        for (input_index, &sample) in input.iter().enumerate() {
810            history.copy_within(1.., 0);
811            history[TAPS_PER_PHASE - 1] = sample as f32;
812            while out.len() * fin < (input_index + 1) * fout {
813                let phase = (out.len() * fin % fout) * NUM_PHASES / fout;
814                let c = &coeffs[phase * TAPS_PER_PHASE..(phase + 1) * TAPS_PER_PHASE];
815                let acc: f64 = c
816                    .iter()
817                    .zip(&history)
818                    .map(|(&c, &h)| c as f64 * h as f64)
819                    .sum();
820                out.push(acc.clamp(i16::MIN as f64, i16::MAX as f64) as i16);
821            }
822        }
823        out
824    }
825
826    /// Every SIMD level reachable on this machine must match the scalar
827    /// reference to within 1 LSB (FMA/summation order may flip truncation).
828    #[test]
829    fn test_simd_matches_scalar_reference() {
830        // `Level::new()` (runtime detection) needs fearless_simd's `std` feature.
831        #[cfg(feature = "std")]
832        let levels = [Level::baseline(), default_level(), Level::new()];
833        #[cfg(not(feature = "std"))]
834        let levels = [Level::baseline(), default_level()];
835        for (from, to) in RATIOS {
836            for input in test_signals(2000) {
837                for level in levels {
838                    let mut r = new_resampler(from, to);
839                    r.level = level;
840                    let expected = reference_resample(r.coeffs, from, to, &input);
841                    let got = resample(&mut r, &input);
842                    assert_eq!(expected.len(), got.len(), "{from}->{to} {level:?}");
843                    for (i, (&e, &g)) in expected.iter().zip(&got).enumerate() {
844                        assert!(
845                            (e as i32 - g as i32).abs() <= 1,
846                            "{from}->{to} {level:?}: sample {i}: expected {e}, got {g}"
847                        );
848                    }
849                }
850            }
851        }
852    }
853
854    /// Streaming in arbitrary chunk sizes (smaller than, around and larger
855    /// than the history length and the internal block size) must be
856    /// bit-identical to a single call.
857    #[test]
858    fn test_chunked_is_bit_identical() {
859        let [input, _] = test_signals(3000);
860        for (from, to) in RATIOS {
861            let whole = resample(&mut new_resampler(from, to), &input);
862            for chunk in [1, 2, 7, 23, 24, 25, 47, 160, 255, 256, 257, 1000] {
863                let mut r = new_resampler(from, to);
864                let mut chunked = Vec::new();
865                for part in input.chunks(chunk) {
866                    chunked.extend_from_slice(&resample(&mut r, part));
867                }
868                assert_eq!(whole, chunked, "{from}->{to} chunk={chunk}");
869            }
870        }
871    }
872
873    /// Constant input must come out at the same level for every ratio
874    /// (each polyphase branch is normalized to unity DC gain).
875    #[test]
876    fn test_dc_gain_all_ratios() {
877        for (from, to) in RATIOS {
878            for level in [1000i16, -20000, i16::MAX] {
879                let output = resample(&mut new_resampler(from, to), &vec![level; 2000]);
880                // Skip the filter warm-up (history starts zeroed).
881                let warmup = (TAPS_PER_PHASE * to).div_ceil(from) + 1;
882                for (i, &s) in output.iter().enumerate().skip(warmup) {
883                    assert!(
884                        (s as i32 - level as i32).abs() <= 2,
885                        "{from}->{to}: sample {i} = {s}, expected ~{level}"
886                    );
887                }
888            }
889        }
890    }
891
892    /// Output never exceeds `max_output_samples`, for any chunk length and
893    /// any carried-over position.
894    #[test]
895    fn test_max_output_samples_is_upper_bound() {
896        for (from, to) in RATIOS {
897            let mut r = new_resampler(from, to);
898            let input = vec![100i16; 200];
899            for n in 1..=200 {
900                let mut out = vec![0i16; r.max_output_samples(n)];
901                r.resample_into(&input[..n], &mut out)
902                    .unwrap_or_else(|e| panic!("{from}->{to} n={n}: {e:?}"));
903            }
904        }
905    }
906
907    #[test]
908    fn test_output_buffer_too_small() {
909        let mut r = new_resampler(8000, 16000);
910        let mut out = [0i16; 10];
911        assert!(matches!(
912            r.resample_into(&[0i16; 160], &mut out),
913            Err(CodecError::BufferTooSmall)
914        ));
915
916        // Same-rate passthrough checks the size too.
917        let mut r = new_resampler(8000, 8000);
918        assert!(matches!(
919            r.resample_into(&[0i16; 160], &mut out),
920            Err(CodecError::BufferTooSmall)
921        ));
922    }
923
924    #[test]
925    fn test_same_rate_passthrough() {
926        let [input, _] = test_signals(500);
927        let mut r = new_resampler(16000, 16000);
928        assert_eq!(r.max_output_samples(input.len()), input.len());
929        assert_eq!(resample(&mut r, &input), input);
930    }
931
932    #[test]
933    fn test_new_rejects_bad_args() {
934        let mut small = vec![0.0f32; COEFFS_LEN - 1];
935        assert!(matches!(
936            Resampler::new(8000, 16000, &mut small),
937            Err(CodecError::BufferTooSmall)
938        ));
939        let mut coeffs = vec![0.0f32; COEFFS_LEN];
940        assert!(matches!(
941            Resampler::new(0, 16000, &mut coeffs),
942            Err(CodecError::InvalidInput)
943        ));
944        assert!(matches!(
945            Resampler::new(8000, 0, &mut coeffs),
946            Err(CodecError::InvalidInput)
947        ));
948    }
949
950    /// `reset` must restore the exact initial state, even mid-stream.
951    #[test]
952    fn test_reset_mid_stream() {
953        let [input, _] = test_signals(1000);
954        let mut r = new_resampler(44100, 48000);
955        let fresh = resample(&mut r, &input);
956        // 13 samples leaves partial history and a non-zero `pos`.
957        resample(&mut r, &input[..13]);
958        r.reset();
959        assert_eq!(resample(&mut r, &input), fresh);
960    }
961}