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