Skip to main content

rill_core_dsp/generators/
resampler.rs

1//! Sample-rate converter built on [`InterpolatedReader`].
2//!
3//! Provides [`Resampler`] for converting audio between different sample rates
4//! using linear or cubic interpolation. Accepts a pre-loaded buffer at
5//! `source_rate` and outputs at `target_rate` (set via [`Algorithm::init`]).
6//!
7//! # Use case
8//!
9//! A WAV loaded at 44100 Hz must play back through a JACK backend that
10//! negotiated 48000 Hz. `Resampler` computes the ratio `44100/48000` and
11//! reads the buffer with interpolation, producing correct-speed output.
12//!
13//! # RT safety
14//!
15//! All heap allocation happens at construction time. The `process` method
16//! performs only reads and math — no allocation, no locking.
17
18use crate::generators::InterpolatedReader;
19use rill_core::traits::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
20use rill_core::traits::ProcessResult;
21use rill_core::Transcendental;
22
23/// Sample-rate converter wrapping [`InterpolatedReader`].
24///
25/// Accepts a pre-loaded buffer with a known source sample rate and outputs
26/// at a target rate that can differ from the source rate.
27///
28/// # Examples
29///
30/// ```
31/// use rill_core::traits::Algorithm;
32/// use rill_core_dsp::generators::Resampler;
33///
34/// let source_rate = 44100.0;
35/// let samples = vec![0.0f32; 1024];
36/// let mut rs = Resampler::new(samples, source_rate);
37/// rs.init(48000.0);
38/// let mut out = vec![0.0f32; 512];
39/// rs.process(None, &mut out).unwrap();
40/// assert!((rs.position() - 512.0 * 44100.0 / 48000.0).abs() < 1.0);
41/// ```
42pub struct Resampler<T: Transcendental> {
43    reader: InterpolatedReader<T>,
44    source_rate: f64,
45    target_rate: f64,
46}
47
48impl<T: Transcendental> Resampler<T> {
49    /// Create a new resampler from a sample buffer and its source sample rate.
50    ///
51    /// The resampler starts with `target_rate = source_rate` (passthrough).
52    /// Call [`Algorithm::init`] or [`set_target_rate`] to convert to a
53    /// different rate.
54    pub fn new(buffer: Vec<T>, source_rate: f64) -> Self {
55        let mut reader = InterpolatedReader::new(buffer);
56        reader.set_rate(1.0);
57        Self {
58            reader,
59            source_rate,
60            target_rate: source_rate,
61        }
62    }
63
64    /// Create a resampler from a pre-allocated boxed slice.
65    pub fn from_boxed(buffer: Box<[T]>, source_rate: f64) -> Self {
66        let mut reader = InterpolatedReader::from_boxed(buffer);
67        reader.set_rate(1.0);
68        Self {
69            reader,
70            source_rate,
71            target_rate: source_rate,
72        }
73    }
74
75    /// Number of samples in the source buffer.
76    #[inline(always)]
77    pub fn len(&self) -> usize {
78        self.reader.len()
79    }
80
81    /// Returns `true` if the source buffer is empty.
82    #[inline(always)]
83    pub fn is_empty(&self) -> bool {
84        self.reader.is_empty()
85    }
86
87    /// Source sample rate in Hz.
88    #[inline(always)]
89    pub fn source_rate(&self) -> f64 {
90        self.source_rate
91    }
92
93    /// Set the source sample rate and recompute the interpolation ratio.
94    #[inline(always)]
95    pub fn set_source_rate(&mut self, hz: f64) {
96        self.source_rate = hz;
97        self.update_ratio();
98    }
99
100    /// Target sample rate in Hz.
101    #[inline(always)]
102    pub fn target_rate(&self) -> f64 {
103        self.target_rate
104    }
105
106    /// Set the target sample rate and recompute the interpolation ratio.
107    #[inline(always)]
108    pub fn set_target_rate(&mut self, hz: f64) {
109        self.target_rate = hz;
110        self.update_ratio();
111    }
112
113    /// Enable (`true`) or disable (`false`) cubic Hermite interpolation.
114    ///
115    /// Linear interpolation (default) is faster; cubic gives higher quality
116    /// at the cost of more computation.
117    #[inline(always)]
118    pub fn set_cubic(&mut self, cubic: bool) {
119        self.reader.set_cubic(cubic);
120    }
121
122    /// Returns `true` if cubic interpolation is enabled.
123    #[inline(always)]
124    pub fn is_cubic(&self) -> bool {
125        self.reader.is_cubic()
126    }
127
128    /// Current read position in the source buffer (in source samples).
129    #[inline(always)]
130    pub fn position(&self) -> f64 {
131        self.reader.position()
132    }
133
134    /// Set the read position in the source buffer.
135    #[inline(always)]
136    pub fn set_position(&mut self, pos: f64) {
137        self.reader.set_position(pos);
138    }
139
140    /// Replace the source buffer and reset position to 0.
141    ///
142    /// The new buffer is assumed to have the same source rate.
143    pub fn set_buffer(&mut self, buffer: Vec<T>) {
144        self.reader.set_buffer(buffer);
145    }
146
147    /// Return the internal buffer as an immutable slice.
148    #[inline(always)]
149    pub fn as_slice(&self) -> &[T] {
150        self.reader.as_slice()
151    }
152
153    /// The computed interpolation ratio (`source_rate / target_rate`).
154    ///
155    /// When `ratio > 1.0` the source runs faster → downsampling.
156    /// When `ratio < 1.0` the source runs slower → upsampling.
157    #[inline(always)]
158    pub fn ratio(&self) -> f64 {
159        self.reader.rate()
160    }
161
162    /// Recompute the reader's rate from source and target rates.
163    fn update_ratio(&mut self) {
164        let ratio = if self.target_rate > 0.0 {
165            self.source_rate / self.target_rate
166        } else {
167            1.0
168        };
169        self.reader.set_rate(ratio);
170    }
171}
172
173impl<T: Transcendental> Algorithm<T> for Resampler<T> {
174    fn init(&mut self, sample_rate: f32) {
175        self.set_target_rate(sample_rate as f64);
176    }
177
178    fn reset(&mut self) {
179        self.reader.set_position(0.0);
180    }
181
182    fn process(&mut self, _input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
183        self.reader.render_block(output);
184        Ok(())
185    }
186
187    fn metadata(&self) -> AlgorithmMetadata {
188        AlgorithmMetadata {
189            name: "Resampler",
190            category: AlgorithmCategory::Utility,
191            description: "Sample-rate converter using linear or cubic interpolation",
192            author: "Rill",
193            version: env!("CARGO_PKG_VERSION"),
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    fn process(rs: &mut Resampler<f64>, out: &mut [f64]) {
203        rs.process(None, out).unwrap();
204    }
205
206    #[test]
207    fn test_passthrough() {
208        let buf = vec![1.0f64, 2.0, 3.0, 4.0];
209        let mut rs = Resampler::new(buf, 44100.0);
210        rs.init(44100.0);
211
212        let mut out = [0.0f64; 4];
213        process(&mut rs, &mut out);
214        assert_eq!(out, [1.0, 2.0, 3.0, 4.0]);
215    }
216
217    #[test]
218    fn test_upsample_2x() {
219        let buf = vec![0.0f64, 10.0];
220        let mut rs = Resampler::new(buf, 22050.0);
221        rs.init(44100.0);
222
223        // ratio = 0.5 → each source sample spans 2 output samples
224        let mut out = [0.0f64; 4];
225        process(&mut rs, &mut out);
226        // position: 0.0 → 0.5 → 1.0 → 1.5
227        assert!((out[0] - 0.0).abs() < 1e-10, "pos 0.0, got {}", out[0]);
228        assert!((out[1] - 5.0).abs() < 1e-10, "pos 0.5, got {}", out[1]);
229        assert!((out[2] - 10.0).abs() < 1e-10, "pos 1.0, got {}", out[2]);
230        assert!(
231            (out[3] - 10.0).abs() < 1e-10,
232            "pos 1.5 clamped, got {}",
233            out[3]
234        );
235    }
236
237    #[test]
238    fn test_downsample_2x() {
239        let buf: Vec<f64> = (0..8).map(|i| i as f64 * 100.0).collect();
240        let mut rs = Resampler::new(buf, 88200.0);
241        rs.init(44100.0);
242
243        // ratio = 2.0 → skip every other source sample
244        let mut out = [0.0f64; 4];
245        process(&mut rs, &mut out);
246        assert!((out[0] - 0.0).abs() < 1e-10);
247        assert!((out[1] - 200.0).abs() < 1e-10);
248        assert!((out[2] - 400.0).abs() < 1e-10);
249        assert!((out[3] - 600.0).abs() < 1e-10);
250    }
251
252    #[test]
253    fn test_44k1_to_48k_non_integer_ratio() {
254        let buf: Vec<f64> = (0..441).map(|i| i as f64 * 0.01).collect();
255        let mut rs = Resampler::new(buf, 44100.0);
256        rs.init(48000.0);
257        rs.set_cubic(true);
258
259        // ratio = 44100/48000 ≈ 0.91875
260        let abs_diff = (rs.ratio() - 44100.0 / 48000.0).abs();
261        assert!(abs_diff < 1e-10, "ratio mismatch: {}", rs.ratio());
262
263        let mut out = [0.0f64; 480];
264        process(&mut rs, &mut out);
265        // position after 480 output samples ≈ 480 * 0.91875 = 441
266        let expected_pos = 480.0 * 44100.0 / 48000.0;
267        let pos_diff = (rs.position() - expected_pos).abs();
268        assert!(pos_diff < 1e-9, "position mismatch: {}", rs.position());
269    }
270
271    #[test]
272    fn test_empty_buffer() {
273        let buf: Vec<f64> = vec![];
274        let mut rs = Resampler::new(buf, 44100.0);
275        rs.init(48000.0);
276        let mut out = [1.0f64; 4];
277        process(&mut rs, &mut out);
278        assert_eq!(out, [0.0; 4]);
279    }
280
281    #[test]
282    fn test_set_source_rate_dynamic() {
283        let buf = vec![0.0f64, 10.0];
284        let mut rs = Resampler::new(buf, 44100.0);
285        rs.init(44100.0);
286        assert!((rs.ratio() - 1.0).abs() < 1e-10);
287
288        rs.set_source_rate(22050.0);
289        assert!((rs.ratio() - 0.5).abs() < 1e-10);
290    }
291
292    #[test]
293    fn test_reset() {
294        let buf: Vec<f64> = (0..10).map(|i| i as f64).collect();
295        let mut rs = Resampler::new(buf, 44100.0);
296        rs.init(44100.0);
297
298        let mut out = [0.0f64; 3];
299        process(&mut rs, &mut out);
300        assert!((rs.position() - 3.0).abs() < 1e-10);
301
302        rs.reset();
303        assert!((rs.position() - 0.0).abs() < 1e-10);
304    }
305}