Skip to main content

moq_audio/
resample.rs

1//! Sample-rate conversion.
2//!
3//! Wraps [`rubato`] with a small interleaved-`f32` interface so the
4//! producer/consumer doesn't have to convert to planar on every call.
5//! Currently sample-rate only; channel up/downmix is rejected upstream.
6
7use rubato::audioadapter_buffers::direct::SequentialSliceOfVecs;
8use rubato::{
9	Async, FixedAsync, Resampler as RubatoTrait, SincInterpolationParameters, SincInterpolationType, WindowFunction,
10};
11
12use crate::AudioError;
13
14/// Sample-rate converter over interleaved `f32` PCM.
15pub struct Resampler {
16	resampler: Async<f32>,
17	chunk_frames: usize,
18	channels: usize,
19	input_planar: Vec<Vec<f32>>,
20	output_planar: Vec<Vec<f32>>,
21	output_frames_max: usize,
22	pending: Vec<f32>,
23}
24
25impl Resampler {
26	/// Build a resampler that converts from `input_rate` to `output_rate`
27	/// for the given channel count.
28	///
29	/// `chunk_frames` is rubato's fixed input window size (per call to
30	/// the underlying resampler). The wrapper buffers caller input until
31	/// it has at least one chunk.
32	pub fn new(input_rate: u32, output_rate: u32, channels: u32, chunk_frames: usize) -> Result<Self, AudioError> {
33		if chunk_frames == 0 {
34			return Err(AudioError::Unsupported("chunk_frames must be > 0".into()));
35		}
36
37		let params = SincInterpolationParameters {
38			sinc_len: 128,
39			f_cutoff: 0.95,
40			interpolation: SincInterpolationType::Linear,
41			oversampling_factor: 128,
42			window: WindowFunction::BlackmanHarris2,
43		};
44		let resampler = Async::<f32>::new_sinc(
45			output_rate as f64 / input_rate as f64,
46			1.0,
47			&params,
48			chunk_frames,
49			channels as usize,
50			FixedAsync::Input,
51		)?;
52
53		let input_planar = (0..channels as usize).map(|_| vec![0.0f32; chunk_frames]).collect();
54		let output_frames_max = resampler.output_frames_max();
55		let output_planar = vec![vec![0.0f32; output_frames_max]; channels as usize];
56
57		Ok(Self {
58			resampler,
59			chunk_frames,
60			channels: channels as usize,
61			input_planar,
62			output_planar,
63			output_frames_max,
64			pending: Vec::new(),
65		})
66	}
67
68	/// Resample interleaved `f32` input into interleaved `f32` output.
69	///
70	/// Returns whatever the resampler can produce given the input and
71	/// the chunk size; remaining samples are buffered for the next call.
72	pub fn process(&mut self, samples: &[f32]) -> Result<Vec<f32>, AudioError> {
73		if samples.len() % self.channels != 0 {
74			return Err(AudioError::Misaligned {
75				got: samples.len(),
76				expected: samples.len().next_multiple_of(self.channels),
77			});
78		}
79
80		self.pending.extend_from_slice(samples);
81
82		let chunk_samples = self.chunk_frames * self.channels;
83		let mut out = Vec::new();
84		while self.pending.len() >= chunk_samples {
85			for (frame_idx, frame) in self.pending[..chunk_samples].chunks_exact(self.channels).enumerate() {
86				for (ch, &sample) in frame.iter().enumerate() {
87					self.input_planar[ch][frame_idx] = sample;
88				}
89			}
90
91			let input = SequentialSliceOfVecs::new(&self.input_planar, self.channels, self.chunk_frames)
92				.expect("resampler input buffer dimensions");
93			let mut output =
94				SequentialSliceOfVecs::new_mut(&mut self.output_planar, self.channels, self.output_frames_max)
95					.expect("resampler output buffer dimensions");
96			let (_, produced) = self.resampler.process_into_buffer(&input, &mut output, None)?;
97
98			let prev_len = out.len();
99			out.resize(prev_len + produced * self.channels, 0.0);
100			for frame_idx in 0..produced {
101				for ch in 0..self.channels {
102					out[prev_len + frame_idx * self.channels + ch] = self.output_planar[ch][frame_idx];
103				}
104			}
105
106			self.pending.drain(..chunk_samples);
107		}
108
109		Ok(out)
110	}
111}
112
113#[cfg(test)]
114mod tests {
115	use super::*;
116
117	#[test]
118	fn rejects_zero_chunk_frames() {
119		let r = Resampler::new(48_000, 48_000, 2, 0);
120		assert!(matches!(r, Err(AudioError::Unsupported(_))));
121	}
122
123	#[test]
124	fn upsample_44100_to_48000_preserves_energy_roughly() {
125		let mut r = Resampler::new(44_100, 48_000, 1, 1024).unwrap();
126		let input: Vec<f32> = (0..44_100)
127			.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44_100.0).sin() * 0.5)
128			.collect();
129		let mut out = r.process(&input).unwrap();
130		out.extend(r.process(&vec![0.0; 1024]).unwrap());
131		assert!(
132			(47_000..50_000).contains(&out.len()),
133			"expected ~48k samples, got {}",
134			out.len()
135		);
136	}
137}