Skip to main content

alpha2_audio/
resample.rs

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