Skip to main content

audio_codec/
resampler_fixed.rs

1use core::f64::consts::PI as PI_F64;
2
3use fearless_simd::{Level, Simd, dispatch};
4
5use super::{CodecError, Sample};
6
7#[cfg(feature = "std")]
8use super::PcmBuf;
9
10/// Number of polyphase filter phases.
11pub const NUM_PHASES: usize = 256;
12/// Number of filter taps per phase.
13pub const TAPS_PER_PHASE: usize = 24;
14/// Required length of the caller-provided coefficient buffer (`NUM_PHASES * TAPS_PER_PHASE`).
15pub const COEFFS_LEN: usize = NUM_PHASES * TAPS_PER_PHASE;
16
17/// Fixed-point scale for the polyphase coefficients and the FIR accumulator
18/// (Q15: one unit = 1/32768).
19const Q15_SHIFT: u32 = 15;
20// The phase index is kept in 8 bits (see `resample_simd`).
21const _: () = assert!(NUM_PHASES == 256);
22/// The 24-tap window is accumulated as two `i32` parts: the 8 centre taps
23/// (`MID_TAPS`, where nearly all of the filter's weight sits) and the 16
24/// outer taps. Each part is a whole number of 128-bit multiply-adds.
25const MID_TAPS: core::ops::Range<usize> = 8..16;
26/// Largest `sum(|coefficient|)` within either part for which an `i32`
27/// accumulator provably cannot overflow: `|acc| <= 32768 * sum(|coeff|) < 2^31`.
28const MAX_PART_ABS_COEFF_SUM: i32 = 65535;
29
30/// `f64::sin` polyfill that works in both std and no_std (via libm).
31#[inline]
32fn fsin(x: f64) -> f64 {
33    #[cfg(feature = "std")]
34    {
35        f64::sin(x)
36    }
37    #[cfg(not(feature = "std"))]
38    {
39        libm::sin(x)
40    }
41}
42
43/// `f64::sqrt` polyfill.
44#[inline]
45fn fsqrt(x: f64) -> f64 {
46    #[cfg(feature = "std")]
47    {
48        f64::sqrt(x)
49    }
50    #[cfg(not(feature = "std"))]
51    {
52        libm::sqrt(x)
53    }
54}
55
56/// `f64::round` polyfill (round half away from zero).
57#[inline]
58fn fround(x: f64) -> f64 {
59    #[cfg(feature = "std")]
60    {
61        x.round()
62    }
63    #[cfg(not(feature = "std"))]
64    {
65        libm::round(x)
66    }
67}
68
69/// Polyphase resampler.
70///
71/// The filter runs entirely in **fixed point** (Q15 i16 coefficients, i16
72/// history, integer accumulator, integer phase accumulator). This matters on
73/// targets without an FPU (e.g. ESP32-S3 / Xtensa LX7): a soft-float `f32`
74/// FIR is ~20× slower than the equivalent integer one there. Coefficients are
75/// still *designed* in `f64` at construction time and quantised once.
76///
77/// The coefficient buffer is **Q15 `i16`** (6144 entries, 12 KB) supplied by
78/// the caller so this type stays `no_std`/allocation-free.
79pub struct Resampler<'a> {
80    input_rate: usize,
81    output_rate: usize,
82    coeffs: &'a mut [i16],
83    /// The last `TAPS_PER_PHASE` input samples, oldest first.
84    history: [i16; TAPS_PER_PHASE],
85    /// Ticks from the newest input sample to the next output sample. It can
86    /// exceed one input sample when downsampling.
87    ///
88    /// Timing runs on an integer grid of `input_rate * output_rate` ticks per
89    /// second: one input sample is `output_rate` ticks and one output sample
90    /// is `input_rate` ticks, so the timing is exact and never rounded.
91    pos: usize,
92    /// SIMD level the filter loop is compiled for.
93    level: Level,
94}
95
96fn bessel_i0(x: f64) -> f64 {
97    let mut sum = 1.0_f64;
98    let mut term = 1.0_f64;
99    let x_sq = x * x * 0.25;
100
101    for m in 1..=30 {
102        term *= x_sq / (m * m) as f64;
103        sum += term;
104        if term < 1e-15 * sum {
105            break;
106        }
107    }
108    sum
109}
110
111fn kaiser_window(n: usize, n_total: usize, beta: f64) -> f64 {
112    if n_total <= 1 {
113        return 1.0;
114    }
115    let alpha = (n_total - 1) as f64 / 2.0;
116    let x = (n as f64 - alpha) / alpha;
117    let arg = beta * fsqrt(1.0 - x * x);
118    bessel_i0(arg) / bessel_i0(beta)
119}
120
121impl<'a> Resampler<'a> {
122    /// Create a new `Resampler`, writing Q15 polyphase filter coefficients into
123    /// the caller-provided buffer.
124    ///
125    /// `coeffs.len()` must be at least [`COEFFS_LEN`] (= 6144 `i16`, 12 KB).
126    /// The buffer is held by the resampler for its entire lifetime; it is
127    /// written once here and read on every subsequent `resample_into` call.
128    pub fn new(
129        input_rate: usize,
130        output_rate: usize,
131        coeffs: &'a mut [i16],
132    ) -> Result<Self, CodecError> {
133        if coeffs.len() < COEFFS_LEN {
134            return Err(CodecError::BufferTooSmall);
135        }
136        if input_rate == 0 || output_rate == 0 {
137            return Err(CodecError::InvalidInput);
138        }
139
140        const KAISER_BETA: f64 = 7.0;
141
142        let ratio = output_rate as f64 / input_rate as f64;
143        let num_phases = NUM_PHASES;
144        let taps_per_phase = TAPS_PER_PHASE;
145        let filter_len = num_phases * taps_per_phase;
146
147        // Integer tick periods keep the timing exact: the old Q24 step rounded
148        // 1/3 and produced an extra sample on the first 16 -> 48 kHz block.
149        // In the hot loop `pos < output_rate` whenever `pos + input_rate` or
150        // `pos * NUM_PHASES` is computed, so neither can overflow.
151        if input_rate.checked_add(output_rate).is_none()
152            || output_rate.checked_mul(NUM_PHASES).is_none()
153        {
154            return Err(CodecError::InvalidInput);
155        }
156
157        let coeffs = &mut coeffs[..filter_len];
158
159        let cutoff = if ratio < 1.0 {
160            ratio * 0.5 * 0.95
161        } else {
162            0.5 * 0.95
163        };
164
165        let center = (taps_per_phase as f64 - 1.0) / 2.0;
166
167        // Design the polyphase filter directly into the borrowed buffer.
168        // We use a fixed-size scratch array on the stack (24 f64 = 192 bytes).
169        let mut phase_coeffs: [f64; TAPS_PER_PHASE] = [0.0; TAPS_PER_PHASE];
170
171        for p in 0..num_phases {
172            let mut sum = 0.0_f64;
173
174            for t in 0..taps_per_phase {
175                let x = t as f64 - center - (p as f64 / num_phases as f64);
176
177                let sinc_val = if x.abs() < 1e-10 {
178                    2.0 * cutoff
179                } else {
180                    let x_pi = x * PI_F64;
181                    fsin(x_pi * 2.0 * cutoff) / x_pi
182                };
183
184                let full_filter_idx = t * num_phases + p;
185                let window = kaiser_window(full_filter_idx, filter_len, KAISER_BETA);
186
187                phase_coeffs[t] = sinc_val * window;
188                sum += phase_coeffs[t];
189            }
190
191            let mut part_abs_sums = [0i32; 2];
192            for t in 0..taps_per_phase {
193                // Quantise the normalised tap to Q15.
194                let q = fround((phase_coeffs[t] / sum) * 32768.0);
195                let q = q.clamp(-32768.0, 32767.0) as i16;
196                coeffs[p * taps_per_phase + t] = q;
197                part_abs_sums[MID_TAPS.contains(&t) as usize] += (q as i32).abs();
198            }
199            // The filter loop accumulates each part in `i32`; refuse the
200            // (degenerate) filters for which that could overflow.
201            if part_abs_sums.iter().any(|&s| s > MAX_PART_ABS_COEFF_SUM) {
202                return Err(CodecError::InvalidInput);
203            }
204        }
205
206        Ok(Self {
207            input_rate,
208            output_rate,
209            coeffs,
210            history: [0; TAPS_PER_PHASE],
211            pos: 0,
212            level: default_level(),
213        })
214    }
215
216    pub fn input_rate(&self) -> usize {
217        self.input_rate
218    }
219
220    pub fn output_rate(&self) -> usize {
221        self.output_rate
222    }
223
224    /// Q15 dot product of the 24-tap window against one phase.
225    ///
226    /// The two parts of the window ([`MID_TAPS`] and the rest) are each
227    /// accumulated in `i32` (each `i16`×`i16` product fits, and
228    /// [`MAX_PART_ABS_COEFF_SUM`], enforced in [`Self::new`], bounds the part
229    /// sums) and combined in `i64`. The full 24-tap sum can exceed `i32` for
230    /// full-scale input (`sum(|coeff|)` is ~2.1), so this is the cheapest way
231    /// to stay *exactly* equal to a plain `i64` accumulation. Plain widening
232    /// multiply-add loops like these are what the compiler turns into
233    /// `pmaddwd`/`smlal` when compiled for a SIMD target feature level.
234    ///
235    /// Takes fixed-size array refs so the outer loop's slice↔array `try_into`
236    /// is the only length check.
237    #[inline(always)]
238    fn dot_q15(history: &[i16; TAPS_PER_PHASE], coeffs: &[i16; TAPS_PER_PHASE]) -> i32 {
239        let mut mid = 0i32;
240        for i in MID_TAPS {
241            mid = mid.wrapping_add((history[i] as i32) * (coeffs[i] as i32));
242        }
243        let mut outer = 0i32;
244        for i in 0..MID_TAPS.start {
245            outer = outer.wrapping_add((history[i] as i32) * (coeffs[i] as i32));
246        }
247        for i in MID_TAPS.end..TAPS_PER_PHASE {
248            outer = outer.wrapping_add((history[i] as i32) * (coeffs[i] as i32));
249        }
250        ((mid as i64 + outer as i64) >> Q15_SHIFT) as i32
251    }
252
253    /// Resample `input` into the caller-provided `out` buffer.
254    ///
255    /// Returns the number of samples written. Returns
256    /// [`CodecError::BufferTooSmall`] if `out` cannot hold the result; use
257    /// [`Self::max_output_samples`] to size it.
258    pub fn resample_into(
259        &mut self,
260        input: &[Sample],
261        out: &mut [Sample],
262    ) -> Result<usize, CodecError> {
263        if self.input_rate == self.output_rate {
264            if out.len() < input.len() {
265                return Err(CodecError::BufferTooSmall);
266            }
267            out[..input.len()].copy_from_slice(input);
268            return Ok(input.len());
269        }
270
271        // Exact number of outputs this call must produce: outputs sit
272        // `input_rate` ticks apart starting at `pos`, and the call consumes
273        // `input.len() * output_rate` ticks. Anything else means output sizes
274        // drift across chunk boundaries (the 0.4.8 bug: 20 ms frames came out
275        // with 959/961 samples). Pinned by `debug_assert_eq!` below.
276        let expected: u128 = {
277            let ticks = (input.len() as u128) * (self.output_rate as u128);
278            if ticks > self.pos as u128 {
279                (ticks - self.pos as u128).div_ceil(self.input_rate as u128)
280            } else {
281                0
282            }
283        };
284
285        // Dispatch once per call so the whole loop is compiled with the
286        // selected target features and `dot_q15` inlines into it.
287        let level = self.level;
288        let written = dispatch!(level, simd => self.resample_simd(simd, input, out))?;
289
290        // The tick-grid timing is exact by construction; this assert pins it
291        // so a future change to the stepping can never silently reintroduce
292        // wrong frame sizes (debug builds and tests only; free in release).
293        debug_assert_eq!(
294            written as u128,
295            expected,
296            "resampler produced {written} samples, exact timing requires {expected} \
297             for {} inputs ({input_rate} -> {output_rate} at pos {}): output \
298             sizes would drift across chunk boundaries",
299            input.len(),
300            self.pos,
301            input_rate = self.input_rate,
302            output_rate = self.output_rate,
303        );
304
305        Ok(written)
306    }
307
308    #[inline(always)]
309    fn resample_simd<S: Simd>(
310        &mut self,
311        _simd: S,
312        input: &[Sample],
313        out: &mut [Sample],
314    ) -> Result<usize, CodecError> {
315        /// Input samples processed per block. Staging a whole block before
316        /// filtering (instead of pushing one sample at a time into the
317        /// window) avoids store-forwarding stalls: the wide window loads
318        /// would otherwise hit a just-written narrow store on every output.
319        const BLOCK: usize = 256;
320
321        // Tick periods (see `pos`): `step` per output, `interval` per input.
322        let (step, interval) = (self.input_rate, self.output_rate);
323        // One output step in whole input samples plus a fraction of one, and
324        // that fraction in phase units: `step_frac * 256 = dq * interval + dr`.
325        // `new` checked that `interval * NUM_PHASES` fits.
326        let (step_whole, step_frac) = (step / interval, step % interval);
327        let (dq, dr) = (
328            step_frac * NUM_PHASES / interval,
329            step_frac * NUM_PHASES % interval,
330        );
331
332        let coeffs: &[[i16; TAPS_PER_PHASE]; NUM_PHASES] =
333            self.coeffs.as_chunks().0[..NUM_PHASES].try_into().unwrap();
334        // `[history | block]`: the window for an output at block index `i`
335        // is `buf[i + 1..i + 1 + TAPS_PER_PHASE]`, i.e. the last
336        // `TAPS_PER_PHASE` samples up to and including sample `i`.
337        let mut buf = [0i16; TAPS_PER_PHASE + BLOCK];
338        let mut written = 0usize;
339
340        for chunk in input.chunks(BLOCK) {
341            let n = chunk.len();
342            buf[..TAPS_PER_PHASE].copy_from_slice(&self.history);
343            buf[TAPS_PER_PHASE..TAPS_PER_PHASE + n].copy_from_slice(chunk);
344
345            // `pos` counts ticks from the block's first sample; an output is
346            // due every `step` ticks before the block end.
347            let pos = self.pos as u64;
348            let end = n as u64 * interval as u64;
349            let count = if pos < end {
350                (end - pos).div_ceil(step as u64) as usize
351            } else {
352                0
353            };
354            let out_block = out
355                .get_mut(written..written + count)
356                .ok_or(CodecError::BufferTooSmall)?;
357
358            // The output position, kept as the input sample it follows plus
359            // the filter phase and the remainder below it:
360            // `pos = idx * interval + frac`, `frac * 256 = phase * interval + rem`.
361            // Main picks the phase as `frac * 256 / interval`; tracking it with
362            // carries gives the same phase without a division per output.
363            let mut idx = self.pos / interval;
364            let frac = self.pos % interval;
365            let mut phase = frac * NUM_PHASES / interval;
366            let mut rem = frac * NUM_PHASES % interval;
367
368            for slot in out_block {
369                // `idx < n <= BLOCK` and `phase < NUM_PHASES`, so the masks never
370                // change them; they let the compiler drop the bounds checks.
371                let i = idx & (BLOCK - 1);
372                let window = buf[i + 1..i + 1 + TAPS_PER_PHASE].try_into().unwrap();
373                let out_sample = Self::dot_q15(window, &coeffs[phase & (NUM_PHASES - 1)]);
374                *slot = out_sample.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
375
376                // Advance one output (`step` ticks).
377                rem += dr;
378                if rem >= interval {
379                    rem -= interval;
380                    phase += 1;
381                }
382                phase += dq;
383                idx += step_whole + phase / NUM_PHASES;
384                phase %= NUM_PHASES;
385            }
386
387            written += count;
388            self.pos = (pos + count as u64 * step as u64 - end) as usize;
389            self.history.copy_from_slice(&buf[n..n + TAPS_PER_PHASE]);
390        }
391
392        Ok(written)
393    }
394
395    /// Upper bound on the number of samples `resample_into` will produce for
396    /// an input of `n_input` samples.
397    pub fn max_output_samples(&self, n_input: usize) -> usize {
398        if self.input_rate == self.output_rate {
399            return n_input;
400        }
401        // Outputs (`input_rate` ticks apart) that fit in `n_input` input
402        // periods (`output_rate` ticks each); the most fit when `pos == 0`.
403        let count = (n_input as u128 * self.output_rate as u128).div_ceil(self.input_rate as u128);
404        count.min(usize::MAX as u128) as usize
405    }
406
407    /// Convenience wrapper that allocates a `Vec` and calls `resample_into`.
408    #[cfg(feature = "std")]
409    pub fn resample(&mut self, input: &[Sample]) -> PcmBuf {
410        let max = self.max_output_samples(input.len());
411        let mut out = vec![0i16; max];
412        match self.resample_into(input, &mut out) {
413            Ok(n) => {
414                debug_assert!(
415                    n <= max,
416                    "resampler emitted {n} samples but `max_output_samples` \
417                     said {max}; buffer sizing invariant broken"
418                );
419                out.truncate(n);
420                out
421            }
422            Err(e) => {
423                // Unreachable while the buffer is sized by
424                // `max_output_samples` (an exact upper bound). Panic in debug
425                // builds and tests so a sizing/count regression can never
426                // silently turn into empty (e.g. Opus) frames; release keeps
427                // the documented empty-vec-on-error behaviour.
428                if cfg!(debug_assertions) {
429                    panic!("resample_into failed inside `resample`: {e:?}");
430                }
431                Vec::new()
432            }
433        }
434    }
435
436    pub fn reset(&mut self) {
437        self.history.fill(0);
438        self.pos = 0;
439    }
440}
441
442/// Best SIMD level available: runtime-detected when possible (`std`, wasm32),
443/// otherwise the level implied by the compile-time target features.
444///
445/// Only AVX-512 is downgraded, to AVX2: the filter is at most 128-bit
446/// `pmaddwd` wide either way, but at the AVX-512 level LLVM also
447/// autovectorizes the block staging with 512-bit instructions. On Ice Lake
448/// (Xeon Gold 6354) that made every ratio 20-25% slower than AVX2. Other
449/// levels are used as detected.
450fn default_level() -> Level {
451    let level = Level::try_detect().unwrap_or_else(Level::baseline);
452    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
453    if level.as_avx512().is_some()
454        && let Some(avx2) = level.as_avx2()
455    {
456        return avx2.level();
457    }
458    level
459}
460
461/// One-shot resampling convenience helper (allocates).
462///
463/// Only available with the `std` feature.
464#[cfg(feature = "std")]
465pub fn resample(input: &[Sample], input_sample_rate: u32, output_sample_rate: u32) -> PcmBuf {
466    if input_sample_rate == output_sample_rate {
467        return input.to_vec();
468    }
469    let mut coeffs = vec![0i16; COEFFS_LEN];
470    let mut r = match Resampler::new(
471        input_sample_rate as usize,
472        output_sample_rate as usize,
473        &mut coeffs,
474    ) {
475        Ok(r) => r,
476        Err(_) => return Vec::new(),
477    };
478    r.resample(input)
479}
480
481/// Self-contained [`Resampler`] that owns its coefficient buffer (std only).
482///
483/// [`Resampler`] borrows a ~12 KB caller-provided coefficient slice, which
484/// makes it awkward to store as a long-lived struct field (the buffer must
485/// outlive the resampler). `BoxedResampler` heap-allocates the coefficients
486/// once and keeps them alive for the resampler's whole lifetime, restoring
487/// the pre-0.4 ergonomic `new(input_rate, output_rate)` + `resample(..)`
488/// call shape for std users that hold resamplers in struct fields.
489#[cfg(feature = "std")]
490pub struct BoxedResampler {
491    /// Heap allocation: the buffer address is stable for the box's lifetime
492    /// and never reallocated, which is what the `Resampler<'static>` borrow
493    /// below relies on. Field order matters for drop: `inner` (borrower)
494    /// must drop before `coeffs` (borrowed).
495    inner: Resampler<'static>,
496    /// Kept alive only to back `inner`'s `'static` borrow; never read.
497    #[allow(dead_code)]
498    coeffs: Box<[i16]>,
499}
500
501#[cfg(feature = "std")]
502impl BoxedResampler {
503    /// Create a resampler that owns its polyphase filter coefficients.
504    ///
505    /// Fails only for zero rates (the coefficient buffer is always sized
506    /// correctly internally).
507    pub fn new(input_rate: usize, output_rate: usize) -> Result<Self, CodecError> {
508        let mut coeffs = vec![0i16; COEFFS_LEN].into_boxed_slice();
509        // SAFETY: `coeffs` is a heap box whose address cannot change while
510        // the allocation lives. We extend the borrow to 'static solely to
511        // store both the buffer and its borrower in the same struct; the
512        // struct's field order drops `inner` before `coeffs`, and nothing
513        // else can reach the buffer (it is moved into `Self` right after).
514        let inner = unsafe {
515            let ptr = coeffs.as_mut_ptr();
516            let loan: &'static mut [i16] = core::slice::from_raw_parts_mut(ptr, coeffs.len());
517            Resampler::new(input_rate, output_rate, loan)?
518        };
519        Ok(Self { inner, coeffs })
520    }
521
522    /// Convenience wrapper that allocates the output buffer.
523    pub fn resample(&mut self, input: &[Sample]) -> PcmBuf {
524        self.inner.resample(input)
525    }
526
527    /// Resample into a caller-provided buffer (no allocation).
528    pub fn resample_into(
529        &mut self,
530        input: &[Sample],
531        out: &mut [Sample],
532    ) -> Result<usize, CodecError> {
533        self.inner.resample_into(input, out)
534    }
535
536    /// Upper bound on the output sample count for `n_input` input samples.
537    pub fn max_output_samples(&self, n_input: usize) -> usize {
538        self.inner.max_output_samples(n_input)
539    }
540
541    /// Reset the resampling history (e.g. after a source discontinuity).
542    pub fn reset(&mut self) {
543        self.inner.reset();
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use alloc::boxed::Box;
551    use alloc::vec;
552    use alloc::vec::Vec;
553    use core::f32::consts::PI as PI_F32;
554    #[cfg(feature = "std")]
555    use std::time::Instant;
556
557    /// no_std stand-in for the std-only convenience `resample()`: same
558    /// contract (buffer sized by `max_output_samples`, truncated to the
559    /// produced count) so the whole suite also runs without `std`.
560    fn resample(r: &mut Resampler<'_>, input: &[Sample]) -> Vec<Sample> {
561        let mut out = vec![0i16; r.max_output_samples(input.len())];
562        let n = r
563            .resample_into(input, &mut out)
564            .expect("out sized by max_output_samples");
565        out.truncate(n);
566        out
567    }
568
569    fn new_resampler(input_rate: usize, output_rate: usize) -> Resampler<'static> {
570        // Leak intentionally: tests are short-lived and we need a 'static reference.
571        let coeffs: &'static mut [i16] = Box::leak(vec![0i16; COEFFS_LEN].into_boxed_slice());
572        Resampler::new(input_rate, output_rate, coeffs).expect("resampler init")
573    }
574
575    #[test]
576    fn test_exact_sample_counts_for_repeated_20ms_blocks() {
577        for input_rate in [8000, 16000, 44100, 48000] {
578            for output_rate in [8000, 16000, 44100, 48000] {
579                let mut r = new_resampler(input_rate, output_rate);
580                let input = vec![1000; input_rate / 50];
581                let mut output = vec![0; output_rate / 50];
582                for block in 0..10 {
583                    let n = r.resample_into(&input, &mut output).unwrap_or_else(|e| {
584                        panic!("{input_rate} -> {output_rate}, block {block}: {e:?}")
585                    });
586                    assert_eq!(
587                        n,
588                        output.len(),
589                        "{input_rate} -> {output_rate}, block {block}"
590                    );
591                }
592            }
593        }
594    }
595
596    #[test]
597    fn test_exact_timing_survives_unaligned_chunks_and_reset() {
598        for (input_rate, output_rate) in [
599            (16000, 48000),
600            (48000, 16000),
601            (44100, 48000),
602            (48000, 44100),
603        ] {
604            let input: Vec<i16> = (0..2000).map(|i| ((i * 37 % 200) - 100) as i16).collect();
605            let mut r = new_resampler(input_rate, output_rate);
606            let expected = resample(&mut r, &input);
607            assert_eq!(
608                expected.len(),
609                (input.len() * output_rate).div_ceil(input_rate)
610            );
611            // Sweep chunk sizes around the history length (24) so every
612            // phase-accumulator alignment is exercised: the running output
613            // count must always be exactly ceil(consumed * out / in).
614            for chunk_size in [1, 2, 3, 5, 7, 11, 23, 24, 25, 47, 88, 160, 319] {
615                r.reset();
616                let mut got = Vec::new();
617                let mut consumed = 0;
618                for chunk in input.chunks(chunk_size) {
619                    got.extend(resample(&mut r, chunk));
620                    consumed += chunk.len();
621                    assert_eq!(
622                        got.len(),
623                        (consumed * output_rate).div_ceil(input_rate),
624                        "{input_rate} -> {output_rate}, chunk {chunk_size}, consumed {consumed}"
625                    );
626                    assert!(resample(&mut r, &[]).is_empty());
627                }
628                assert_eq!(
629                    got, expected,
630                    "{input_rate} -> {output_rate}, chunk {chunk_size}"
631                );
632            }
633            r.reset();
634            assert_eq!(resample(&mut r, &input), expected);
635        }
636    }
637
638    // Uses the std-only allocating `OpusEncoder::encode`; the resampler's
639    // output counts themselves are covered portably by the exact-count tests.
640    #[cfg(all(feature = "opus", feature = "std"))]
641    #[test]
642    fn test_resampled_20ms_frames_encode_as_opus() {
643        use crate::{Encoder, opus::OpusEncoder};
644
645        for input_rate in [8000, 16000] {
646            let mut r = new_resampler(input_rate, 48000);
647            let mut encoder = OpusEncoder::new_default();
648            for _ in 0..3 {
649                let pcm = resample(&mut r, &vec![0; input_rate / 50]);
650                assert_eq!(pcm.len(), 960);
651                assert!(!encoder.encode(&pcm).is_empty());
652            }
653        }
654    }
655
656    #[test]
657    fn test_resample_8k_to_16k() {
658        let mut resampler = new_resampler(8000, 16000);
659        let input = vec![1000i16; 80];
660        let output = resample(&mut resampler, &input);
661        assert!(output.len() >= 150 && output.len() <= 170);
662        for &s in &output[48..output.len().saturating_sub(48)] {
663            assert!((s - 1000).abs() < 100, "Value {} is too far from 1000", s);
664        }
665    }
666
667    #[test]
668    fn test_resample_16k_to_8k() {
669        let mut resampler = new_resampler(16000, 8000);
670        let input = vec![1000i16; 160];
671        let output = resample(&mut resampler, &input);
672        assert!(output.len() >= 75 && output.len() <= 85);
673        let skip = output.len() / 4;
674        for &s in &output[skip..output.len() - skip] {
675            assert!((s - 1000).abs() < 100, "Value {} is too far from 1000", s);
676        }
677    }
678
679    #[test]
680    fn test_frequency_response_downsample() {
681        let mut resampler = new_resampler(16000, 8000);
682        let freq = 2000.0_f32; // Well below 4kHz Nyquist
683        let samples: Vec<i16> = (0..160)
684            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / 16000.0).sin() * 10000.0) as i16)
685            .collect();
686
687        let output = resample(&mut resampler, &samples);
688
689        // Output should have similar amplitude (allowing for some attenuation)
690        let input_rms: f32 = samples
691            .iter()
692            .map(|&s| (s as f32).powi(2))
693            .sum::<f32>()
694            .sqrt()
695            / samples.len() as f32;
696        let output_rms: f32 = output
697            .iter()
698            .map(|&s| (s as f32).powi(2))
699            .sum::<f32>()
700            .sqrt()
701            / output.len() as f32;
702
703        assert!(
704            output_rms > input_rms * 0.7,
705            "Too much attenuation: input_rms={}, output_rms={}",
706            input_rms,
707            output_rms
708        );
709    }
710
711    #[test]
712    fn test_aliasing_suppression() {
713        let mut resampler = new_resampler(16000, 8000);
714        let freq = 7000.0_f32; // Above 4kHz Nyquist of output
715        let samples: Vec<i16> = (0..1600)
716            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / 16000.0).sin() * 10000.0) as i16)
717            .collect();
718
719        let output = resample(&mut resampler, &samples);
720
721        let output_rms: f32 =
722            (output.iter().map(|&s| (s as f32).powi(2)).sum::<f32>() / output.len() as f32).sqrt();
723        let input_rms: f32 = 10000.0 / 1.414; // Expected RMS of sine wave with amplitude 10000
724
725        assert!(
726            output_rms < input_rms / 50.0,
727            "Aliasing not sufficiently suppressed: output_rms={}",
728            output_rms
729        );
730    }
731
732    // Uses `std::time::Instant`; the integer kernel itself is covered by
733    // the portable tests above.
734    #[cfg(feature = "std")]
735    #[test]
736    fn test_performance_48k_to_8k() {
737        let mut resampler = new_resampler(48000, 8000);
738        let input = vec![0i16; 48000];
739
740        let start = Instant::now();
741        let iterations = 100;
742        for _ in 0..iterations {
743            let _ = resample(&mut resampler, &input);
744            resampler.reset();
745        }
746        let duration = start.elapsed();
747        let per_second = duration.as_secs_f64() / iterations as f64;
748        println!(
749            "Resampling 1s of 48kHz to 8kHz (24 taps) took: {:.4}ms",
750            per_second * 1000.0
751        );
752        assert!(
753            per_second < 0.1,
754            "Performance regression: {}ms",
755            per_second * 1000.0
756        );
757    }
758
759    #[test]
760    fn test_continuity_between_chunks() {
761        let input_rate = 16000;
762        let output_rate = 8000;
763
764        let freq = 1000.0_f32;
765        let total_samples = 3200;
766        let input: Vec<i16> = (0..total_samples)
767            .map(|i| ((i as f32 * freq * 2.0 * PI_F32 / input_rate as f32).sin() * 5000.0) as i16)
768            .collect();
769
770        let mut resampler1 = new_resampler(input_rate, output_rate);
771        let output1 = resample(&mut resampler1, &input);
772
773        let mut resampler2 = new_resampler(input_rate, output_rate);
774        let mid = input.len() / 2;
775        let mut output2 = resample(&mut resampler2, &input[..mid]);
776        output2.extend_from_slice(&resample(&mut resampler2, &input[mid..]));
777
778        assert_eq!(output1.len(), output2.len(), "Output lengths differ");
779
780        let max_diff: i16 = output1
781            .iter()
782            .zip(output2.iter())
783            .map(|(a, b)| (a - b).abs())
784            .max()
785            .unwrap_or(0);
786
787        assert!(
788            max_diff < 100,
789            "Large discontinuity between chunks: max_diff={}",
790            max_diff
791        );
792    }
793
794    /// The Q15 integer core must match an f32 accumulator using the *same*
795    /// quantised coefficients — going integer must not change the result
796    /// beyond rounding. (Coefficient quantisation itself is covered by the
797    /// passband/aliasing tests above.)
798    #[test]
799    fn test_q15_core_matches_float_reference() {
800        let fin = 16000usize;
801        let fout = 48000usize;
802        let mut coeffs = vec![0i16; COEFFS_LEN];
803        let mut r = Resampler::new(fin, fout, &mut coeffs).unwrap();
804
805        let input: Vec<i16> = (0..2000)
806            .map(|i| (((i * 37) % 200 - 100) * 60) as i16)
807            .collect();
808        let got = resample(&mut r, &input);
809
810        // f32 reference: derive positions from the absolute output index,
811        // independently of the streaming accumulator, using the same Q15 taps.
812        let taps = TAPS_PER_PHASE;
813        let mut hist = [0.0f32; TAPS_PER_PHASE];
814        let mut want = Vec::new();
815        for (input_index, &s) in input.iter().enumerate() {
816            hist.copy_within(1..taps, 0);
817            hist[taps - 1] = s as f32;
818            while want.len() * fin < (input_index + 1) * fout {
819                let phase = (want.len() * fin % fout) * NUM_PHASES / fout;
820                let mut acc = 0.0f32;
821                for t in 0..taps {
822                    acc += hist[t] * (coeffs[phase * taps + t] as f32 / 32768.0);
823                }
824                want.push(acc.round().clamp(-32768.0, 32767.0) as i16);
825            }
826        }
827
828        assert_eq!(got.len(), want.len());
829        let se: f64 = got
830            .iter()
831            .zip(&want)
832            .map(|(&a, &b)| {
833                let d = a as f64 - b as f64;
834                d * d
835            })
836            .sum();
837        let sig: f64 = want.iter().map(|&b| (b as f64) * (b as f64)).sum();
838        let snr = 10.0 * (sig / se.max(1.0)).log10();
839        assert!(
840            snr > 60.0,
841            "integer core diverges from float: {:.1} dB",
842            snr
843        );
844    }
845
846    /// `BoxedResampler` must produce byte-identical output to the borrowed
847    /// `Resampler` fed the same coefficients, and survive being moved +
848    /// reused across many calls (stable self-referential borrow).
849    #[cfg(feature = "std")]
850    #[test]
851    fn test_boxed_resampler_matches_borrowed() {
852        let input_rate = 48000usize;
853        let output_rate = 8000usize;
854        let input: Vec<i16> = (0..4800)
855            .map(|i| ((i as f32 * 0.05).sin() * 8000.0) as i16)
856            .collect();
857
858        let expected = {
859            let mut coeffs = vec![0i16; COEFFS_LEN];
860            let mut borrowed = Resampler::new(input_rate, output_rate, &mut coeffs).unwrap();
861            resample(&mut borrowed, &input)
862        };
863
864        let mut boxed = BoxedResampler::new(input_rate, output_rate).unwrap();
865        let expected_max = (input.len() * output_rate).div_ceil(input_rate);
866        assert_eq!(boxed.max_output_samples(input.len()), expected_max);
867        let got = boxed.resample(&input);
868        assert_eq!(
869            expected, got,
870            "BoxedResampler output must match borrowed Resampler"
871        );
872
873        // Reuse after move: the coefficient borrow must stay valid across
874        // moves. The resampler is a streaming state machine — a second
875        // `resample` of the same input keeps the tail of the previous run in
876        // its history, so only the output LENGTH is stable across the move.
877        let mut moved = boxed;
878        let again = moved.resample(&input);
879        assert_eq!(expected.len(), again.len());
880
881        // reset() clears the history and phase, so a fresh `resample` of the
882        // same input must reproduce the very first output exactly.
883        moved.reset();
884        let after_reset = moved.resample(&input);
885        assert_eq!(expected, after_reset);
886
887        assert!(
888            BoxedResampler::new(0, 8000).is_err(),
889            "zero input rate must error"
890        );
891        assert!(
892            BoxedResampler::new(48000, 0).is_err(),
893            "zero output rate must error"
894        );
895    }
896
897    const RATIOS: [(usize, usize); 8] = [
898        (8000, 16000),
899        (16000, 8000),
900        (48000, 8000),
901        (8000, 48000),
902        (44100, 48000),
903        (48000, 44100),
904        (8000, 22050),
905        (22050, 16000),
906    ];
907
908    /// Deterministic test signals: sine + LCG noise, and a full-scale square
909    /// wave whose filter overshoot exercises output saturation.
910    fn test_signals(len: usize) -> [Vec<i16>; 2] {
911        let mut seed = 0x1234_5678u32;
912        let noisy: Vec<i16> = (0..len)
913            .map(|i| {
914                seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
915                let noise = ((seed >> 16) as i16 as f32) * 0.2;
916                ((i as f32 * 0.07).sin() * 15000.0 + noise) as i16
917            })
918            .collect();
919        let square: Vec<i16> = (0..len)
920            .map(|i| {
921                if (i / 10) % 2 == 0 {
922                    i16::MAX
923                } else {
924                    i16::MIN
925                }
926            })
927            .collect();
928        [noisy, square]
929    }
930
931    /// Straightforward scalar implementation: per-sample shifting window,
932    /// `i64` accumulator, and each output's position derived from its
933    /// absolute index rather than a streaming accumulator. The optimised loop
934    /// must match it exactly: there is no float rounding to excuse
935    /// differences.
936    fn reference_resample(coeffs: &[i16], fin: usize, fout: usize, input: &[i16]) -> Vec<i16> {
937        let mut history = [0i16; TAPS_PER_PHASE];
938        let mut out = Vec::new();
939        for (input_index, &sample) in input.iter().enumerate() {
940            history.copy_within(1.., 0);
941            history[TAPS_PER_PHASE - 1] = sample;
942            while out.len() * fin < (input_index + 1) * fout {
943                let phase = (out.len() * fin % fout) * NUM_PHASES / fout;
944                let c = &coeffs[phase * TAPS_PER_PHASE..(phase + 1) * TAPS_PER_PHASE];
945                let acc: i64 = c
946                    .iter()
947                    .zip(&history)
948                    .map(|(&c, &h)| c as i64 * h as i64)
949                    .sum();
950                out.push((acc >> Q15_SHIFT).clamp(i16::MIN as i64, i16::MAX as i64) as i16);
951            }
952        }
953        out
954    }
955
956    /// Every SIMD level reachable on this machine must match the scalar
957    /// reference bit for bit.
958    #[test]
959    fn test_simd_matches_scalar_reference() {
960        // `Level::new()` (runtime detection) needs fearless_simd's `std` feature.
961        #[cfg(feature = "std")]
962        let levels = [Level::baseline(), default_level(), Level::new()];
963        #[cfg(not(feature = "std"))]
964        let levels = [Level::baseline(), default_level()];
965        for (from, to) in RATIOS {
966            for input in test_signals(2000) {
967                for level in levels {
968                    let mut r = new_resampler(from, to);
969                    r.level = level;
970                    let expected = reference_resample(r.coeffs, from, to, &input);
971                    let got = resample(&mut r, &input);
972                    assert_eq!(expected, got, "{from}->{to} {level:?}");
973                }
974            }
975        }
976    }
977
978    /// Each `i32` part-accumulator is only exact while the part's absolute
979    /// coefficient sum stays within [`MAX_PART_ABS_COEFF_SUM`]; `new`
980    /// enforces it. Check that realistic (and extreme) ratios are accepted
981    /// and that the worst part leaves headroom.
982    #[test]
983    fn test_part_coeff_sums_fit_i32_accumulator() {
984        let mut worst = 0i32;
985        let rates = [
986            8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, 96000, 192000,
987        ];
988        // The filter depends only on the ratio (cutoff scales with it when
989        // downsampling), so also sweep it finely, including extremes.
990        let sweep = (1..=100).flat_map(|k| [(48000, 480 * k), (480 * k, 48000)]);
991        let pairs = rates
992            .iter()
993            .flat_map(|&from| rates.iter().map(move |&to| (from, to)))
994            .chain(sweep);
995        for (from, to) in pairs {
996            let mut coeffs = vec![0i16; COEFFS_LEN];
997            Resampler::new(from, to, &mut coeffs).unwrap_or_else(|e| panic!("{from}->{to}: {e:?}"));
998            for phase in coeffs.chunks(TAPS_PER_PHASE) {
999                let abs = |c: &i16| (*c as i32).abs();
1000                let mid: i32 = phase[MID_TAPS].iter().map(abs).sum();
1001                let outer: i32 = phase.iter().map(abs).sum::<i32>() - mid;
1002                worst = worst.max(mid).max(outer);
1003            }
1004        }
1005        assert!(worst <= MAX_PART_ABS_COEFF_SUM);
1006    }
1007
1008    /// The full 24-tap sum can exceed `i32` (sum(|coeff|) is ~2.1 for
1009    /// upsampling filters). Sign-matched full-scale input drives every phase
1010    /// to that worst case; the result must be the exact `i64` value, not a
1011    /// wrapped one.
1012    #[test]
1013    fn test_dot_q15_exact_beyond_i32_range() {
1014        let r = new_resampler(8000, 48000);
1015        let mut beyond_i32 = 0;
1016        for phase in r.coeffs.chunks(TAPS_PER_PHASE) {
1017            let phase: &[i16; TAPS_PER_PHASE] = phase.try_into().unwrap();
1018            let window: [i16; TAPS_PER_PHASE] =
1019                core::array::from_fn(|i| if phase[i] < 0 { i16::MIN } else { i16::MAX });
1020            let exact: i64 = window
1021                .iter()
1022                .zip(phase)
1023                .map(|(&h, &c)| h as i64 * c as i64)
1024                .sum();
1025            beyond_i32 += (exact > i32::MAX as i64) as usize;
1026            assert_eq!(
1027                Resampler::dot_q15(&window, phase) as i64,
1028                exact >> Q15_SHIFT,
1029                "phase {phase:?}"
1030            );
1031        }
1032        assert!(
1033            beyond_i32 > 0,
1034            "test no longer reaches the i32 overflow range"
1035        );
1036    }
1037
1038    /// Streaming in arbitrary chunk sizes (smaller than, around and larger
1039    /// than the history length and the internal block size) must be
1040    /// bit-identical to a single call.
1041    #[test]
1042    fn test_chunked_is_bit_identical() {
1043        let [input, _] = test_signals(3000);
1044        for (from, to) in RATIOS {
1045            let whole = resample(&mut new_resampler(from, to), &input);
1046            for chunk in [1, 2, 7, 23, 24, 25, 47, 160, 255, 256, 257, 1000] {
1047                let mut r = new_resampler(from, to);
1048                let mut chunked = Vec::new();
1049                for part in input.chunks(chunk) {
1050                    chunked.extend_from_slice(&resample(&mut r, part));
1051                }
1052                assert_eq!(whole, chunked, "{from}->{to} chunk={chunk}");
1053            }
1054        }
1055    }
1056
1057    /// Constant input must come out at the same level for every ratio
1058    /// (each polyphase branch has ~unity DC gain, up to Q15 quantisation).
1059    #[test]
1060    fn test_dc_gain_all_ratios() {
1061        for (from, to) in RATIOS {
1062            for level in [1000i16, -20000, i16::MAX] {
1063                let output = resample(&mut new_resampler(from, to), &vec![level; 2000]);
1064                // Skip the filter warm-up (history starts zeroed).
1065                let warmup = (TAPS_PER_PHASE * to).div_ceil(from) + 1;
1066                // 24 taps quantised to 1/32768 each: error well under 0.1%.
1067                let tolerance = 2 + (level as i32).abs() / 1000;
1068                for (i, &s) in output.iter().enumerate().skip(warmup) {
1069                    assert!(
1070                        (s as i32 - level as i32).abs() <= tolerance,
1071                        "{from}->{to}: sample {i} = {s}, expected ~{level}"
1072                    );
1073                }
1074            }
1075        }
1076    }
1077
1078    /// Output never exceeds `max_output_samples`, for any chunk length and
1079    /// any carried-over position.
1080    #[test]
1081    fn test_max_output_samples_is_upper_bound() {
1082        for (from, to) in RATIOS {
1083            let mut r = new_resampler(from, to);
1084            let input = vec![100i16; 200];
1085            for n in 1..=200 {
1086                let mut out = vec![0i16; r.max_output_samples(n)];
1087                r.resample_into(&input[..n], &mut out)
1088                    .unwrap_or_else(|e| panic!("{from}->{to} n={n}: {e:?}"));
1089            }
1090        }
1091    }
1092
1093    #[test]
1094    fn test_output_buffer_too_small() {
1095        let mut r = new_resampler(8000, 16000);
1096        let mut out = [0i16; 10];
1097        assert!(matches!(
1098            r.resample_into(&[0i16; 160], &mut out),
1099            Err(CodecError::BufferTooSmall)
1100        ));
1101
1102        // Same-rate passthrough checks the size too.
1103        let mut r = new_resampler(8000, 8000);
1104        assert!(matches!(
1105            r.resample_into(&[0i16; 160], &mut out),
1106            Err(CodecError::BufferTooSmall)
1107        ));
1108    }
1109
1110    #[test]
1111    fn test_same_rate_passthrough() {
1112        let [input, _] = test_signals(500);
1113        let mut r = new_resampler(16000, 16000);
1114        assert_eq!(r.max_output_samples(input.len()), input.len());
1115        assert_eq!(resample(&mut r, &input), input);
1116    }
1117
1118    #[test]
1119    fn test_new_rejects_bad_args() {
1120        let mut small = vec![0i16; COEFFS_LEN - 1];
1121        assert!(matches!(
1122            Resampler::new(8000, 16000, &mut small),
1123            Err(CodecError::BufferTooSmall)
1124        ));
1125        let mut coeffs = vec![0i16; COEFFS_LEN];
1126        assert!(matches!(
1127            Resampler::new(0, 16000, &mut coeffs),
1128            Err(CodecError::InvalidInput)
1129        ));
1130        assert!(matches!(
1131            Resampler::new(8000, 0, &mut coeffs),
1132            Err(CodecError::InvalidInput)
1133        ));
1134    }
1135
1136    /// `reset` must restore the exact initial state, even mid-stream.
1137    #[test]
1138    fn test_reset_mid_stream() {
1139        let [input, _] = test_signals(1000);
1140        let mut r = new_resampler(44100, 48000);
1141        let fresh = resample(&mut r, &input);
1142        // 13 samples leaves partial history and a non-zero `pos`.
1143        resample(&mut r, &input[..13]);
1144        r.reset();
1145        assert_eq!(resample(&mut r, &input), fresh);
1146    }
1147}