Skip to main content

ebur128_stream/
resampler.rs

1//! Sample-rate conversion to one of the analyzer's supported rates.
2//!
3//! The analyzer rejects sample rates that aren't in the supported set
4//! (`{22 050, 32 000, 44 100, 48 000, 88 200, 96 000, 192 000}`) — but
5//! real-world audio comes at all kinds of rates (8 kHz telephony,
6//! 11 025 Hz legacy game audio, 11 .025 → 96 → 48 chained pipelines).
7//!
8//! This module provides a thin [`Resampler`] wrapper around the
9//! [`rubato`](https://crates.io/crates/rubato) crate so callers can
10//! adapt arbitrary input rates without re-deriving the polyphase math.
11//! Linear interpolation (cheap, lossy) and high-quality sinc paths are
12//! both available.
13//!
14//! Gated behind the `resampler` feature.
15//!
16//! # Example
17//!
18//! ```ignore
19//! use ebur128_stream::resampler::{Resampler, Quality};
20//! use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
21//!
22//! // Inbound stream at 11 025 Hz; analyzer wants 48 000 Hz.
23//! let mut resampler = Resampler::new(11_025, 48_000, 2, Quality::HighQuality)?;
24//! let mut analyzer = AnalyzerBuilder::new()
25//!     .sample_rate(48_000)
26//!     .channels(&[Channel::Left, Channel::Right])
27//!     .modes(Mode::Integrated)
28//!     .build()?;
29//!
30//! // Push input → resample → analyze.
31//! let input: Vec<f32> = vec![0.1; 11_025 * 2]; // 1 s stereo
32//! let output = resampler.process_interleaved(&input)?;
33//! analyzer.push_interleaved::<f32>(&output)?;
34//! # Ok::<(), Box<dyn std::error::Error>>(())
35//! ```
36
37#![allow(clippy::needless_range_loop)]
38
39use alloc::vec::Vec;
40
41/// Resampling quality preset.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Quality {
44    /// Linear interpolation. Cheap; introduces aliasing. Acceptable
45    /// for development / low-latency previews; not for measurement
46    /// publishing.
47    Linear,
48    /// Sinc interpolation with a moderate-length filter. The sweet
49    /// spot for streaming pipelines.
50    HighQuality,
51}
52
53/// Errors produced by the resampler.
54#[derive(Debug)]
55#[non_exhaustive]
56pub enum ResampleError {
57    /// `rubato` reported a conversion error.
58    Rubato(rubato::ResampleError),
59    /// Construction of the resampler failed.
60    Construction(rubato::ResamplerConstructionError),
61    /// The input buffer was not a whole multiple of the channel count.
62    UnalignedBuffer {
63        /// Number of samples in the offending buffer.
64        samples: usize,
65        /// Configured channel count.
66        channels: usize,
67    },
68}
69
70impl core::fmt::Display for ResampleError {
71    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
72        match self {
73            ResampleError::Rubato(e) => write!(f, "rubato: {e}"),
74            ResampleError::Construction(e) => write!(f, "rubato construction: {e}"),
75            ResampleError::UnalignedBuffer { samples, channels } => write!(
76                f,
77                "buffer length {samples} is not a multiple of channel count {channels}"
78            ),
79        }
80    }
81}
82
83impl std::error::Error for ResampleError {}
84
85impl From<rubato::ResampleError> for ResampleError {
86    fn from(e: rubato::ResampleError) -> Self {
87        ResampleError::Rubato(e)
88    }
89}
90impl From<rubato::ResamplerConstructionError> for ResampleError {
91    fn from(e: rubato::ResamplerConstructionError) -> Self {
92        ResampleError::Construction(e)
93    }
94}
95
96/// Internal dispatch: rubato's `Resampler` trait is not dyn-compatible
97/// (it has generic methods), so we hold the concrete impls in an enum.
98enum Inner {
99    Linear(rubato::FastFixedIn<f32>),
100    Sinc(rubato::SincFixedIn<f32>),
101}
102
103impl Inner {
104    fn input_frames_next(&self) -> usize {
105        use rubato::Resampler;
106        match self {
107            Inner::Linear(r) => r.input_frames_next(),
108            Inner::Sinc(r) => r.input_frames_next(),
109        }
110    }
111    fn output_frames_next(&self) -> usize {
112        use rubato::Resampler;
113        match self {
114            Inner::Linear(r) => r.output_frames_next(),
115            Inner::Sinc(r) => r.output_frames_next(),
116        }
117    }
118    fn process_into_buffer(
119        &mut self,
120        input: &[&[f32]],
121        output: &mut [&mut [f32]],
122    ) -> Result<(usize, usize), rubato::ResampleError> {
123        use rubato::Resampler;
124        match self {
125            Inner::Linear(r) => r.process_into_buffer(input, output, None),
126            Inner::Sinc(r) => r.process_into_buffer(input, output, None),
127        }
128    }
129}
130
131/// Streaming resampler that converts interleaved audio between two
132/// rates.
133pub struct Resampler {
134    inner: Inner,
135    in_rate: u32,
136    out_rate: u32,
137    channels: usize,
138    /// Buffer carry: input samples not yet consumed by the inner
139    /// resampler (rubato wants exact chunk sizes).
140    pending: Vec<Vec<f32>>,
141}
142
143impl core::fmt::Debug for Resampler {
144    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
145        f.debug_struct("Resampler")
146            .field("in_rate", &self.in_rate)
147            .field("out_rate", &self.out_rate)
148            .field("channels", &self.channels)
149            .finish_non_exhaustive()
150    }
151}
152
153impl Resampler {
154    /// Build a resampler converting `in_rate` → `out_rate` for the
155    /// given channel count.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`ResampleError::Construction`] if rubato can't be
160    /// constructed (extreme rate ratios, zero channels, etc.).
161    pub fn new(
162        in_rate: u32,
163        out_rate: u32,
164        channels: usize,
165        quality: Quality,
166    ) -> Result<Self, ResampleError> {
167        let ratio = out_rate as f64 / in_rate as f64;
168        let chunk_size = 1024;
169        let inner = match quality {
170            Quality::Linear => {
171                use rubato::{FastFixedIn, PolynomialDegree};
172                Inner::Linear(FastFixedIn::new(
173                    ratio,
174                    1.0,
175                    PolynomialDegree::Linear,
176                    chunk_size,
177                    channels,
178                )?)
179            }
180            Quality::HighQuality => {
181                use rubato::{
182                    SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
183                };
184                let params = SincInterpolationParameters {
185                    sinc_len: 128,
186                    f_cutoff: 0.95,
187                    interpolation: SincInterpolationType::Quadratic,
188                    oversampling_factor: 256,
189                    window: WindowFunction::Blackman2,
190                };
191                Inner::Sinc(SincFixedIn::new(ratio, 1.0, params, chunk_size, channels)?)
192            }
193        };
194        Ok(Self {
195            inner,
196            in_rate,
197            out_rate,
198            channels,
199            pending: (0..channels).map(|_| Vec::new()).collect(),
200        })
201    }
202
203    /// Resample one chunk of interleaved audio.
204    ///
205    /// Returns the resampled output as interleaved f32. Latency: the
206    /// resampler buffers up to `chunk_size` input frames; trailing
207    /// samples that don't fill a chunk remain in `self` and emerge on
208    /// the next call.
209    pub fn process_interleaved(&mut self, input: &[f32]) -> Result<Vec<f32>, ResampleError> {
210        if self.channels == 0 {
211            return Ok(Vec::new());
212        }
213        if input.len() % self.channels != 0 {
214            return Err(ResampleError::UnalignedBuffer {
215                samples: input.len(),
216                channels: self.channels,
217            });
218        }
219        // De-interleave into per-channel pending buffers.
220        let frames = input.len() / self.channels;
221        for f in 0..frames {
222            for c in 0..self.channels {
223                self.pending[c].push(input[f * self.channels + c]);
224            }
225        }
226        let mut output_frames: Vec<Vec<f32>> = (0..self.channels).map(|_| Vec::new()).collect();
227
228        // Drain whole chunks.
229        loop {
230            let needed = self.inner.input_frames_next();
231            if self.pending[0].len() < needed {
232                break;
233            }
234            // Build planar slices for rubato.
235            let chunks: Vec<&[f32]> = (0..self.channels)
236                .map(|c| &self.pending[c][..needed])
237                .collect();
238            let mut out: Vec<Vec<f32>> = (0..self.channels)
239                .map(|_| Vec::with_capacity(self.inner.output_frames_next()))
240                .collect();
241            for buf in &mut out {
242                buf.resize(self.inner.output_frames_next(), 0.0);
243            }
244            let mut out_slices: Vec<&mut [f32]> =
245                out.iter_mut().map(|v| v.as_mut_slice()).collect();
246            let (consumed_in, produced_out) =
247                self.inner.process_into_buffer(&chunks, &mut out_slices)?;
248            // Drop consumed input.
249            for c in 0..self.channels {
250                self.pending[c].drain(..consumed_in);
251                output_frames[c].extend_from_slice(&out[c][..produced_out]);
252            }
253        }
254
255        // Re-interleave.
256        let frames_out = output_frames[0].len();
257        let mut out = Vec::with_capacity(frames_out * self.channels);
258        for f in 0..frames_out {
259            for c in 0..self.channels {
260                out.push(output_frames[c][f]);
261            }
262        }
263        Ok(out)
264    }
265
266    /// Configured input rate, in Hz.
267    #[must_use]
268    pub fn input_rate(&self) -> u32 {
269        self.in_rate
270    }
271    /// Configured output rate, in Hz.
272    #[must_use]
273    pub fn output_rate(&self) -> u32 {
274        self.out_rate
275    }
276    /// Configured channel count.
277    #[must_use]
278    pub fn channels(&self) -> usize {
279        self.channels
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn upsample_8k_to_48k_preserves_signal_length_approximately() {
289        let mut r = Resampler::new(8_000, 48_000, 2, Quality::Linear).unwrap();
290        // 1 second of stereo at 8 kHz = 16 000 samples.
291        let input = vec![0.1f32; 16_000];
292        let out = r.process_interleaved(&input).unwrap();
293        // Resampler buffers some input internally; output frames
294        // should be ≈ 1 s × 48 kHz × 2 channels = 96 000 samples,
295        // minus latency.
296        assert!(
297            out.len() > 80_000 && out.len() < 100_000,
298            "got {} output samples, expected near 96000",
299            out.len()
300        );
301    }
302
303    #[test]
304    fn highquality_path_works_at_44_1_to_48() {
305        let mut r = Resampler::new(44_100, 48_000, 2, Quality::HighQuality).unwrap();
306        // 0.5 s of -6 dBFS sine.
307        let n = 22_050usize;
308        let omega = 2.0 * std::f32::consts::PI * 1000.0 / 44_100.0;
309        let mut input = Vec::with_capacity(n * 2);
310        for i in 0..n {
311            let v = 0.5 * (omega * i as f32).sin();
312            input.push(v);
313            input.push(v);
314        }
315        let out = r.process_interleaved(&input).unwrap();
316        assert!(!out.is_empty());
317    }
318
319    #[test]
320    fn unaligned_buffer_rejected() {
321        let mut r = Resampler::new(48_000, 48_000, 2, Quality::Linear).unwrap();
322        let err = r.process_interleaved(&[0.0, 0.0, 0.0]).unwrap_err();
323        assert!(matches!(err, ResampleError::UnalignedBuffer { .. }));
324    }
325}