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	/// Output frames per input frame, for sizing the flushed tail.
20	ratio: f64,
21	/// Output frames the sinc filter holds behind what it has already emitted.
22	delay: usize,
23	/// Whether any caller input has gone in, since the filter only owes a tail
24	/// once it has actually run.
25	started: bool,
26	/// Leading output frames still to be dropped: the filter opens by emitting its
27	/// own centring delay as silence, which is not audio anyone sent.
28	skip: usize,
29	channels: usize,
30	input_planar: Vec<Vec<f32>>,
31	output_planar: Vec<Vec<f32>>,
32	output_frames_max: usize,
33	pending: Vec<f32>,
34}
35
36impl Resampler {
37	/// Build a resampler that converts from `input_rate` to `output_rate`
38	/// for the given channel count.
39	///
40	/// `chunk_frames` is rubato's fixed input window size (per call to
41	/// the underlying resampler). The wrapper buffers caller input until
42	/// it has at least one chunk.
43	pub fn new(input_rate: u32, output_rate: u32, channels: u32, chunk_frames: usize) -> Result<Self, Error> {
44		if chunk_frames == 0 {
45			return Err(Error::Unsupported("chunk_frames must be > 0".into()));
46		}
47
48		let params = SincInterpolationParameters {
49			sinc_len: 128,
50			f_cutoff: Some(0.95),
51			interpolation: SincInterpolationType::Linear,
52			oversampling_factor: 128,
53			window: WindowFunction::BlackmanHarris2,
54		};
55		let ratio = output_rate as f64 / input_rate as f64;
56		let resampler =
57			Async::<f32>::new_sinc(ratio, 1.0, &params, chunk_frames, channels as usize, FixedAsync::Input)?;
58
59		let delay = resampler.output_delay();
60		let input_planar = (0..channels as usize).map(|_| vec![0.0f32; chunk_frames]).collect();
61		let output_frames_max = resampler.output_frames_max();
62		let output_planar = vec![vec![0.0f32; output_frames_max]; channels as usize];
63
64		Ok(Self {
65			resampler,
66			chunk_frames,
67			ratio,
68			delay,
69			started: false,
70			skip: delay,
71			channels: channels as usize,
72			input_planar,
73			output_planar,
74			output_frames_max,
75			pending: Vec::new(),
76		})
77	}
78
79	/// Output frames dropped so far as the filter's startup silence.
80	///
81	/// The output runs that much shorter than the input it was built from, so a
82	/// caller stamping its output has to reach back over this as well as over what
83	/// is still buffered.
84	pub fn skipped(&self) -> usize {
85		self.delay - self.skip
86	}
87
88	/// Input frames buffered from earlier calls, waiting for enough to fill a chunk.
89	///
90	/// The next output starts with these, so a caller stamping its output has to
91	/// reach back this far.
92	pub fn pending_frames(&self) -> usize {
93		self.pending.len() / self.channels
94	}
95
96	/// Drop everything held, buffered input and filter state alike, returning to
97	/// the just-constructed state.
98	///
99	/// The escape hatch for a *reported* discontinuity: where [`flush`](Self::flush)
100	/// ends the stream, this starts a new one in place, so audio from before the
101	/// gap can't bleed through the filter into audio from after it.
102	pub fn reset(&mut self) {
103		self.resampler.reset();
104		self.pending.clear();
105		self.skip = self.delay;
106		self.started = false;
107	}
108
109	/// Resample what is still buffered, ending the stream.
110	///
111	/// The resampler only consumes whole chunks, so without this the last partial
112	/// chunk of a track is never converted and its audio is simply lost. Pads the
113	/// chunk out with silence and keeps only the output the real input earned, so
114	/// the padding costs a filter tail on the final samples rather than extra
115	/// audio.
116	///
117	/// Takes `self` because that padding runs the filter through silence the
118	/// caller never supplied: resampling more afterwards would carry that state
119	/// into it, across a gap nothing reported. Ending the stream is the only thing
120	/// this can be used for, so that is the only thing it can express.
121	pub fn flush(mut self) -> Result<Vec<f32>, Error> {
122		// Not `pending == 0`: what the filter owes has nothing to do with what is
123		// buffered, so a stream that happens to end on a chunk boundary owes a tail
124		// just the same. Only one that never ran owes nothing.
125		if !self.started {
126			return Ok(Vec::new());
127		}
128
129		let pending = self.pending_frames();
130
131		// The filter runs centred, so every output frame is built from input around
132		// `delay` frames earlier and it still holds that much real audio no amount of
133		// input has pushed out. Ask for that much beyond what the pending input
134		// earns, feeding silence until it arrives, or a track converts its own
135		// ending into frames nobody reads.
136		//
137		// Only as much as `process` actually dropped off the front, though. That is
138		// the whole delay for a stream long enough to have emitted anything, and
139		// nothing at all for one that ended before it filled a chunk, where the skip
140		// still lies ahead and comes out of this call's own output.
141		let repaid = self.delay - self.skip;
142		let wanted = ((pending as f64 * self.ratio).round() as usize + repaid) * self.channels;
143
144		let mut out = Vec::new();
145		while out.len() < wanted {
146			// An empty result does not mean the filter is done: with a chunk smaller
147			// than the delay, a whole chunk's output can disappear into the skip while
148			// the audio behind it is still coming. Stop only when a chunk moves
149			// neither the output nor the skip, which cannot repeat.
150			let skip_before = self.skip;
151			self.pending.resize(self.chunk_frames * self.channels, 0.0);
152			let produced = self.process(&[])?;
153			if produced.is_empty() && self.skip == skip_before {
154				break;
155			}
156			out.extend_from_slice(&produced);
157		}
158
159		out.truncate(wanted);
160		Ok(out)
161	}
162
163	/// Resample interleaved `f32` input into interleaved `f32` output.
164	///
165	/// Returns whatever the resampler can produce given the input and
166	/// the chunk size; remaining samples are buffered for the next call.
167	pub fn process(&mut self, samples: &[f32]) -> Result<Vec<f32>, Error> {
168		if !samples.len().is_multiple_of(self.channels) {
169			return Err(Error::Misaligned {
170				got: samples.len(),
171				expected: samples.len().next_multiple_of(self.channels),
172			});
173		}
174
175		self.started |= !samples.is_empty();
176		self.pending.extend_from_slice(samples);
177
178		let chunk_samples = self.chunk_frames * self.channels;
179		let mut out = Vec::new();
180		while self.pending.len() >= chunk_samples {
181			for (frame_idx, frame) in self.pending[..chunk_samples].chunks_exact(self.channels).enumerate() {
182				for (ch, &sample) in frame.iter().enumerate() {
183					self.input_planar[ch][frame_idx] = sample;
184				}
185			}
186
187			let input = SequentialSliceOfVecs::new(&self.input_planar, self.channels, self.chunk_frames)
188				.expect("resampler input buffer dimensions");
189			let mut output =
190				SequentialSliceOfVecs::new_mut(&mut self.output_planar, self.channels, self.output_frames_max)
191					.expect("resampler output buffer dimensions");
192			let (_, produced) = self.resampler.process_into_buffer(&input, &mut output, None)?;
193
194			let prev_len = out.len();
195			out.resize(prev_len + produced * self.channels, 0.0);
196			for frame_idx in 0..produced {
197				for ch in 0..self.channels {
198					out[prev_len + frame_idx * self.channels + ch] = self.output_planar[ch][frame_idx];
199				}
200			}
201
202			self.pending.drain(..chunk_samples);
203		}
204
205		// Drop the filter's startup silence rather than passing it on as audio. What
206		// it costs is paid back by `flush`, which drains the same amount at the end,
207		// so the output keeps the duration of the input that produced it.
208		if self.skip > 0 {
209			let drop = self.skip.min(out.len() / self.channels) * self.channels;
210			out.drain(..drop);
211			self.skip -= drop / self.channels;
212		}
213
214		Ok(out)
215	}
216}
217
218/// Whether [`remix`] can produce this channel count, checked up front so a
219/// consumer fails at construction rather than on its first frame.
220pub(crate) fn validate_channels(count: u32) -> Result<(), Error> {
221	match count {
222		1 | 2 => Ok(()),
223		other => Err(Error::Unsupported(format!(
224			"channel remix only supports mono and stereo (got {other})"
225		))),
226	}
227}
228
229/// Remix interleaved mono/stereo PCM into the requested channel count.
230pub(crate) fn remix(samples: &[f32], input_channels: u32, output_channels: u32) -> Result<Vec<f32>, Error> {
231	match (input_channels, output_channels) {
232		(1, 1) | (2, 2) => Ok(samples.to_vec()),
233		(1, 2) => {
234			let mut output = Vec::with_capacity(samples.len() * 2);
235			for &sample in samples {
236				output.extend_from_slice(&[sample, sample]);
237			}
238			Ok(output)
239		}
240		(2, 1) => Ok(samples.chunks_exact(2).map(|pair| (pair[0] + pair[1]) * 0.5).collect()),
241		_ => Err(Error::Unsupported(format!(
242			"channel remix only supports mono and stereo (got {input_channels} to {output_channels})"
243		))),
244	}
245}
246
247#[cfg(test)]
248mod tests {
249	use super::*;
250
251	#[test]
252	fn rejects_zero_chunk_frames() {
253		let r = Resampler::new(48_000, 48_000, 2, 0);
254		assert!(matches!(r, Err(Error::Unsupported(_))));
255	}
256
257	#[test]
258	fn upsample_44100_to_48000_preserves_energy_roughly() {
259		let mut r = Resampler::new(44_100, 48_000, 1, 1024).unwrap();
260		let input: Vec<f32> = (0..44_100)
261			.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44_100.0).sin() * 0.5)
262			.collect();
263		let mut out = r.process(&input).unwrap();
264		out.extend(r.process(&vec![0.0; 1024]).unwrap());
265		assert!(
266			(47_000..50_000).contains(&out.len()),
267			"expected ~48k samples, got {}",
268			out.len()
269		);
270	}
271
272	/// The sinc filter is centred, so the end of a track only reaches the output
273	/// once further input has passed through it. Without draining that, a track
274	/// converts its own ending into frames nobody ever reads, and the tail comes
275	/// out silent however loud it was.
276	#[test]
277	fn flush_drains_the_delayed_tail() {
278		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
279
280		// A full-scale sample near the end of the track, silence around it. Not the
281		// very last one: draining stops at the filter's centre rather than emitting
282		// its ringing past the end of the signal, so the final sample keeps only
283		// half its response however far this drains.
284		let mut input = vec![0.0f32; 1024];
285		input[1000] = 1.0;
286
287		let body = r.process(&input).unwrap();
288		let tail = r.flush().unwrap();
289
290		let peak = |samples: &[f32]| samples.iter().fold(0.0f32, |max, s| max.max(s.abs()));
291		assert!(peak(&body) < 0.01, "the sample emerged early: peak {}", peak(&body));
292		assert!(peak(&tail) > 0.5, "the tail lost the sample: peak {}", peak(&tail));
293	}
294
295	/// The filter owes its tail whether or not anything is buffered, so a track
296	/// whose length lands exactly on a chunk boundary has to drain too. With
297	/// 1024-sample frames at 48 kHz that lands every fifteenth one against the
298	/// 960-frame chunk, so it is not a corner a real stream avoids.
299	#[test]
300	fn flush_drains_on_an_exact_chunk_boundary() {
301		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
302
303		// Exactly two chunks of input, so nothing is left pending.
304		let mut input = vec![0.0f32; 1764];
305		input[1750] = 1.0;
306
307		let body = r.process(&input).unwrap();
308		assert_eq!(r.pending_frames(), 0, "the input should divide evenly");
309
310		let tail = r.flush().unwrap();
311
312		// Not exactly zero: a centred sinc has a precursor, so a trace of the sample
313		// leads it into the body. The audio itself is still all in the tail.
314		let peak = |samples: &[f32]| samples.iter().fold(0.0f32, |max, s| max.max(s.abs()));
315		assert!(peak(&body) < 0.01, "the sample emerged early: peak {}", peak(&body));
316		assert!(peak(&tail) > 0.5, "the tail lost the sample: peak {}", peak(&tail));
317	}
318
319	/// A stream that ends before it fills a chunk never emitted anything, so the
320	/// filter's startup silence is still ahead of it and comes out of the flush's
321	/// own output. Repaying a skip that has not happened yet hands back a stream
322	/// longer than its source.
323	#[test]
324	fn flush_sizes_a_stream_shorter_than_a_chunk() {
325		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
326
327		let body = r.process(&[0.25f32; 441]).unwrap();
328		let tail = r.flush().unwrap();
329
330		// 441 frames at 44.1 kHz is 480 at 48 kHz, and that is all it can be.
331		let total = body.len() + tail.len();
332		assert!((475..=485).contains(&total), "unexpected total: {total}");
333	}
334
335	/// `chunk_frames` is the caller's to choose, and a small one can be shorter
336	/// than the filter's delay. Then a whole chunk's output disappears into the
337	/// startup skip, which used to read as "the filter is done" and drop the
338	/// entire stream.
339	#[test]
340	fn flush_survives_a_chunk_smaller_than_the_delay() {
341		let mut r = Resampler::new(44_100, 48_000, 1, 32).unwrap();
342
343		let body = r.process(&[0.5f32; 20]).unwrap();
344		let tail = r.flush().unwrap();
345
346		let total = body.len() + tail.len();
347		assert!((18..=26).contains(&total), "unexpected total: {total}");
348		assert!(
349			tail.iter().any(|s| s.abs() > 0.25),
350			"the stream came back silent: peak {}",
351			tail.iter().fold(0.0f32, |m, s| m.max(s.abs()))
352		);
353	}
354
355	#[test]
356	fn remix_mono_to_stereo_duplicates_samples() {
357		assert_eq!(remix(&[1.0, 2.0], 1, 2).unwrap(), [1.0, 1.0, 2.0, 2.0]);
358	}
359
360	#[test]
361	fn remix_stereo_to_mono_averages_channels() {
362		assert_eq!(remix(&[1.0, 3.0, 2.0, 4.0], 2, 1).unwrap(), [2.0, 3.0]);
363	}
364}