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