Skip to main content

alpha2_audio/
resample.rs

1//! The sample-rate converter every host uses to bring source audio to the project rate.
2//!
3//! Output sample `n` sits at the exact source position `n * source_rate / target_rate` and is a
4//! Kaiser-windowed sinc interpolation shaped like `libswresample`'s default (32 taps at unity,
5//! cutoff 0.97 of the lower Nyquist, beta 9), widened when downsampling. Output depends only on
6//! the source samples, not on call boundaries, so seeking and linear reads agree. Coefficients
7//! use `libm` and sums run in a fixed order, so native and wasm output is bit-identical.
8
9#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
10
11use core::fmt;
12use core::ops::Range;
13
14/// Taps on each side of an output sample when the rate does not fall.
15const HALF_TAPS: u64 = 16;
16/// Kaiser window shape.
17const KAISER_BETA: f64 = 9.0;
18/// Passband edge as a fraction of the lower of the two Nyquist frequencies.
19const CUTOFF: f64 = 0.97;
20/// The most coefficients one converter keeps (1 MiB). Rate pairs with more phases share the
21/// nearest of evenly spaced phases; filters that do not fit at all are refused.
22const MAX_TABLE_COEFFICIENTS: u64 = 1 << 18;
23/// The highest rate either side may have.
24pub const MAX_RESAMPLE_RATE: u32 = 768_000;
25/// The largest source-to-target rate ratio. Downsampling widens the filter by 16 taps a side per
26/// factor, and beyond this it would not fit the table (768 kHz converts down to 94 Hz).
27pub const MAX_DOWNSAMPLE_FACTOR: u32 = (MAX_TABLE_COEFFICIENTS / (2 * HALF_TAPS)) as u32;
28
29/// Why a converter could not be made.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct ResampleError {
32    /// The source rate asked for.
33    pub source_rate: u32,
34    /// The target rate asked for.
35    pub target_rate: u32,
36}
37
38impl fmt::Display for ResampleError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(
41            f,
42            "cannot resample {} Hz to {} Hz: both rates must be 1 to {MAX_RESAMPLE_RATE} Hz \
43             and the source rate at most {MAX_DOWNSAMPLE_FACTOR} times the target rate",
44            self.source_rate, self.target_rate
45        )
46    }
47}
48
49impl std::error::Error for ResampleError {}
50
51/// A band-limited converter from one sample rate to another.
52#[derive(Clone, Debug)]
53pub struct Resampler {
54    source_rate: u32,
55    target_rate: u32,
56    /// `source_rate / gcd`: source samples per `target_step` output samples.
57    source_step: u64,
58    /// `target_rate / gcd`: the number of distinct output phases.
59    target_step: u64,
60    /// Taps on each side; zero for the identity.
61    half: u64,
62    /// Phases kept in `table`: `target_step`, or fewer when they do not fit.
63    phases: u64,
64    /// `phases` rows of `2 * half` coefficients. Row `q` weighs source samples
65    /// `i - half + 1 ..= i + half` for an output at `i + q / phases`.
66    table: Vec<f32>,
67}
68
69impl Resampler {
70    /// A converter from `source_rate` to `target_rate`.
71    ///
72    /// Builds the whole coefficient table, so call it off the real-time thread.
73    ///
74    /// # Errors
75    /// Either rate is zero or above [`MAX_RESAMPLE_RATE`], or the ratio exceeds
76    /// [`MAX_DOWNSAMPLE_FACTOR`].
77    pub fn new(source_rate: u32, target_rate: u32) -> Result<Self, ResampleError> {
78        if !(1..=MAX_RESAMPLE_RATE).contains(&source_rate)
79            || !(1..=MAX_RESAMPLE_RATE).contains(&target_rate)
80            || u64::from(source_rate) > u64::from(MAX_DOWNSAMPLE_FACTOR) * u64::from(target_rate)
81        {
82            return Err(ResampleError {
83                source_rate,
84                target_rate,
85            });
86        }
87        let divisor = gcd(u64::from(source_rate), u64::from(target_rate));
88        let source_step = u64::from(source_rate) / divisor;
89        let target_step = u64::from(target_rate) / divisor;
90        if source_rate == target_rate {
91            return Ok(Self {
92                source_rate,
93                target_rate,
94                source_step,
95                target_step,
96                half: 0,
97                phases: 1,
98                table: Vec::new(),
99            });
100        }
101        // Downsampling narrows the passband to the target Nyquist and widens the filter to match.
102        let factor = (f64::from(target_rate) / f64::from(source_rate)).min(1.0);
103        let half = (HALF_TAPS * u64::from(source_rate)).div_ceil(u64::from(target_rate));
104        let half = half.max(HALF_TAPS);
105        let taps = 2 * half;
106        debug_assert!(taps <= MAX_TABLE_COEFFICIENTS, "the fall is bounded above");
107        let phases = target_step.min((MAX_TABLE_COEFFICIENTS / taps).max(1));
108        let cutoff = CUTOFF * factor;
109        let window_scale = 1.0 / bessel_i0(KAISER_BETA);
110        let mut table = Vec::with_capacity(usize::try_from(phases * taps).unwrap_or(0));
111        let mut row = vec![0.0_f64; usize::try_from(taps).unwrap_or(0)];
112        for phase in 0..phases {
113            let offset = phase as f64 / phases as f64;
114            let mut sum = 0.0;
115            for (tap, weight) in row.iter_mut().enumerate() {
116                // The distance from the output to this source sample, in source samples.
117                let distance = (tap as f64 + 1.0 - half as f64) - offset;
118                let x = distance / half as f64;
119                let window = if x.abs() >= 1.0 {
120                    0.0
121                } else {
122                    bessel_i0(KAISER_BETA * libm::sqrt(1.0 - x * x)) * window_scale
123                };
124                *weight = cutoff * sinc(cutoff * distance) * window;
125                sum += *weight;
126            }
127            // Unity gain at DC in every phase: a constant source stays constant.
128            table.extend(row.iter().map(|weight| (*weight / sum) as f32));
129        }
130        Ok(Self {
131            source_rate,
132            target_rate,
133            source_step,
134            target_step,
135            half,
136            phases,
137            table,
138        })
139    }
140
141    /// Source samples per second.
142    #[must_use]
143    pub const fn source_rate(&self) -> u32 {
144        self.source_rate
145    }
146
147    /// Output samples per second.
148    #[must_use]
149    pub const fn target_rate(&self) -> u32 {
150        self.target_rate
151    }
152
153    /// Whether the two rates are equal: output sample `n` is source sample `n`.
154    #[must_use]
155    pub const fn is_identity(&self) -> bool {
156        self.half == 0
157    }
158
159    /// Source samples on each side of an output sample that it weighs.
160    #[must_use]
161    pub fn half_taps(&self) -> i64 {
162        i64::try_from(self.half).unwrap_or(i64::MAX)
163    }
164
165    /// The first output sample at or after source sample `source`.
166    #[must_use]
167    pub fn target_at_or_after(&self, source: i64) -> i64 {
168        div_ceil(
169            i128::from(source) * i128::from(self.target_step),
170            i128::from(self.source_step),
171        )
172    }
173
174    /// The first output sample that weighs a source sample at or after `source_end`.
175    #[must_use]
176    pub fn target_known_before(&self, source_end: i64) -> i64 {
177        // A phase rounded up to the next sample reads one sample further.
178        let rounded = i64::from(self.phases < self.target_step);
179        self.target_at_or_after(source_end.saturating_sub(self.half_taps() + rounded))
180    }
181
182    /// The source samples outputs `target` weigh.
183    #[must_use]
184    pub fn source_span(&self, target: Range<i64>) -> Range<i64> {
185        if target.is_empty() {
186            return 0..0;
187        }
188        let (first, _) = self.position(target.start);
189        let (last, _) = self.position(target.end - 1);
190        let half = self.half_taps();
191        if half == 0 {
192            return first..last + 1;
193        }
194        first - half + 1..last + half + 1
195    }
196
197    /// Writes outputs `target_start ..` from `input`, which starts at source sample
198    /// `input_start` and is silent outside its range.
199    pub fn resample(&self, input: &[f32], input_start: i64, target_start: i64, output: &mut [f32]) {
200        let len = i64::try_from(input.len()).unwrap_or(i64::MAX);
201        let half = self.half_taps();
202        let taps = usize::try_from(2 * self.half).unwrap_or(0);
203        for (n, sample) in (target_start..).zip(output.iter_mut()) {
204            let (index, phase) = self.position(n);
205            if half == 0 {
206                let at = index - input_start;
207                *sample = if (0..len).contains(&at) {
208                    input[usize::try_from(at).unwrap_or(0)]
209                } else {
210                    0.0
211                };
212                continue;
213            }
214            let first = index - half + 1 - input_start;
215            let row = &self.table[phase * taps..(phase + 1) * taps];
216            let mut sum = 0.0_f64;
217            if first >= 0 && first + 2 * half <= len {
218                let from = usize::try_from(first).unwrap_or(0);
219                for (weight, value) in row.iter().zip(&input[from..from + taps]) {
220                    sum += f64::from(*weight) * f64::from(*value);
221                }
222            } else {
223                for (offset, weight) in (first..).zip(row) {
224                    if (0..len).contains(&offset) {
225                        sum += f64::from(*weight)
226                            * f64::from(input[usize::try_from(offset).unwrap_or(0)]);
227                    }
228                }
229            }
230            *sample = sum as f32;
231        }
232    }
233
234    /// The source sample at or before output `target`, and the table row for the fraction.
235    fn position(&self, target: i64) -> (i64, usize) {
236        let numerator = i128::from(target) * i128::from(self.source_step);
237        let step = i128::from(self.target_step);
238        let mut index = numerator.div_euclid(step);
239        let remainder = numerator.rem_euclid(step);
240        let phases = i128::from(self.phases);
241        // The nearest kept phase, exact when every phase is kept.
242        let mut phase = (remainder * phases * 2 + step).div_euclid(step * 2);
243        if phase == phases {
244            index += 1;
245            phase = 0;
246        }
247        (
248            i64::try_from(index).unwrap_or(i64::MAX),
249            usize::try_from(phase).unwrap_or(0),
250        )
251    }
252}
253
254/// The fastest speed whose filter narrows with it. Faster clips keep this filter, bounding an
255/// output sample to `2 * HALF_TAPS * MAX_FILTERED_SPEED` source samples.
256pub const MAX_FILTERED_SPEED: f64 = 64.0;
257/// Table entries per source sample of the interpolation kernel at unity.
258const KERNEL_RESOLUTION: usize = 1_024;
259
260/// Band-limited interpolation for clips at non-unity speed, using the same kernel as
261/// [`Resampler`] at any real source position.
262///
263/// Faster than unity, the cutoff falls to the output Nyquist and the filter widens so content
264/// above it is removed rather than aliased; slower, the source Nyquist bounds it. Weights come
265/// from one `libm` table, interpolated linearly and summed in a fixed order, so output is
266/// bit-identical natively and in wasm.
267#[derive(Clone, Copy, Debug)]
268pub struct Interpolator {
269    /// Source samples per kernel unit: the speed clamped to `1..=MAX_FILTERED_SPEED`.
270    scale: f64,
271    /// Taps on each side.
272    half: i64,
273}
274
275impl Interpolator {
276    /// The filter for `speed` source samples per output sample; invalid speeds use unity.
277    #[must_use]
278    pub fn new(speed: f64) -> Self {
279        let scale = if speed.is_finite() {
280            speed.clamp(1.0, MAX_FILTERED_SPEED)
281        } else {
282            1.0
283        };
284        Self {
285            scale,
286            half: libm::ceil(HALF_TAPS as f64 * scale) as i64,
287        }
288    }
289
290    /// Source samples weighed on each side: a position `whole + fraction` reads
291    /// `whole - half + 1 ..= whole + half`.
292    #[must_use]
293    pub const fn half_taps(&self) -> i64 {
294        self.half
295    }
296
297    /// Writes the `2 * half` normalized weights for a position `fraction` (in `0..1`) past a
298    /// source sample.
299    pub fn weights(&self, fraction: f64, weights: &mut Vec<f64>) {
300        let table = kernel_table();
301        let taps = usize::try_from(2 * self.half).unwrap_or(0);
302        weights.resize(taps, 0.0);
303        let step = KERNEL_RESOLUTION as f64 / self.scale;
304        let last = table.len() - 1;
305        let weigh = |distance: f64| {
306            let at = distance * step;
307            // Distances are never negative.
308            #[allow(clippy::cast_sign_loss)]
309            let index = at as usize;
310            if index >= last {
311                0.0
312            } else {
313                let low = table[index];
314                low + (table[index + 1] - low) * (at - index as f64)
315            }
316        };
317        // Taps `0 ..= half - 1` lie at or before the position, the rest after it.
318        let before = usize::try_from(self.half).unwrap_or(0);
319        let mut sum = 0.0;
320        for (tap, weight) in weights[..before].iter_mut().enumerate() {
321            *weight = weigh((before - 1 - tap) as f64 + fraction);
322            sum += *weight;
323        }
324        for (tap, weight) in weights[before..].iter_mut().enumerate() {
325            *weight = weigh((tap + 1) as f64 - fraction);
326            sum += *weight;
327        }
328        // Unity gain at DC at every position: a constant source stays constant.
329        let scale = 1.0 / sum;
330        for weight in weights.iter_mut() {
331            *weight *= scale;
332        }
333    }
334}
335
336/// The interpolation kernel at unity, `sinc(CUTOFF * u) * kaiser(u / 16)`,
337/// at `KERNEL_RESOLUTION` points per unit of `u` over `0..=16`.
338fn kernel_table() -> &'static [f64] {
339    static TABLE: std::sync::OnceLock<Vec<f64>> = std::sync::OnceLock::new();
340    TABLE.get_or_init(|| {
341        let points = HALF_TAPS as usize * KERNEL_RESOLUTION;
342        let window_scale = 1.0 / bessel_i0(KAISER_BETA);
343        (0..=points)
344            .map(|index| {
345                let u = index as f64 / KERNEL_RESOLUTION as f64;
346                let x = u / HALF_TAPS as f64;
347                if x >= 1.0 {
348                    return 0.0;
349                }
350                let window = bessel_i0(KAISER_BETA * libm::sqrt(1.0 - x * x)) * window_scale;
351                CUTOFF * sinc(CUTOFF * u) * window
352            })
353            .collect()
354    })
355}
356
357fn div_ceil(numerator: i128, denominator: i128) -> i64 {
358    let quotient =
359        numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
360    i64::try_from(quotient).unwrap_or(if quotient < 0 { i64::MIN } else { i64::MAX })
361}
362
363const fn gcd(mut a: u64, mut b: u64) -> u64 {
364    while b != 0 {
365        (a, b) = (b, a % b);
366    }
367    a
368}
369
370fn sinc(x: f64) -> f64 {
371    if x == 0.0 {
372        return 1.0;
373    }
374    let angle = core::f64::consts::PI * x;
375    libm::sin(angle) / angle
376}
377
378/// The zeroth-order modified Bessel function of the first kind, by its series.
379fn bessel_i0(x: f64) -> f64 {
380    let quarter = x * x / 4.0;
381    let mut term = 1.0;
382    let mut sum = 1.0;
383    let mut k = 1.0;
384    while term > sum * 1e-17 {
385        term *= quarter / (k * k);
386        sum += term;
387        k += 1.0;
388    }
389    sum
390}