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	/// Rate the caller's input arrives at, for walking [`held`](Self::held) over
20	/// the frames each chunk consumes.
21	input_rate: u32,
22	/// Output frames per input frame, for sizing the flushed tail.
23	ratio: f64,
24	/// Output frames the sinc filter holds behind what it has already emitted.
25	delay: usize,
26	/// Whether any caller input has gone in, since the filter only owes a tail
27	/// once it has actually run.
28	started: bool,
29	/// Leading output frames still to be dropped: the filter opens by emitting its
30	/// own centring delay as silence, which is not audio anyone sent.
31	skip: usize,
32	channels: usize,
33	input_planar: Vec<Vec<f32>>,
34	output_planar: Vec<Vec<f32>>,
35	output_frames_max: usize,
36	pending: Vec<f32>,
37	/// Where the oldest input frame still buffered came from, walked forward as
38	/// chunks are consumed so it also names where the next input lands once
39	/// nothing is buffered. `None` until the first input.
40	held: Option<moq_net::Timestamp>,
41}
42
43impl Resampler {
44	/// Build a resampler that converts from `input_rate` to `output_rate`
45	/// for the given channel count.
46	///
47	/// `chunk_frames` is rubato's fixed input window size (per call to
48	/// the underlying resampler). The wrapper buffers caller input until
49	/// it has at least one chunk.
50	pub fn new(input_rate: u32, output_rate: u32, channels: u32, chunk_frames: usize) -> Result<Self, Error> {
51		if chunk_frames == 0 {
52			return Err(Error::Unsupported("chunk_frames must be > 0".into()));
53		}
54
55		let params = SincInterpolationParameters {
56			sinc_len: 128,
57			f_cutoff: Some(0.95),
58			interpolation: SincInterpolationType::Linear,
59			oversampling_factor: 128,
60			window: WindowFunction::BlackmanHarris2,
61		};
62		let ratio = output_rate as f64 / input_rate as f64;
63		let resampler =
64			Async::<f32>::new_sinc(ratio, 1.0, &params, chunk_frames, channels as usize, FixedAsync::Input)?;
65
66		let delay = resampler.output_delay();
67		let input_planar = (0..channels as usize).map(|_| vec![0.0f32; chunk_frames]).collect();
68		let output_frames_max = resampler.output_frames_max();
69		let output_planar = vec![vec![0.0f32; output_frames_max]; channels as usize];
70
71		Ok(Self {
72			resampler,
73			chunk_frames,
74			input_rate,
75			ratio,
76			delay,
77			started: false,
78			skip: delay,
79			channels: channels as usize,
80			input_planar,
81			output_planar,
82			output_frames_max,
83			pending: Vec::new(),
84			held: None,
85		})
86	}
87
88	/// Output frames dropped so far as the filter's startup silence.
89	///
90	/// The output runs that much shorter than the input it was built from, so a
91	/// caller stamping its output has to reach back this far from the buffered
92	/// input's source timestamp.
93	pub fn skipped(&self) -> usize {
94		self.delay - self.skip
95	}
96
97	/// Input frames buffered from earlier calls, waiting for enough to fill a chunk.
98	pub fn pending_frames(&self) -> usize {
99		self.pending.len() / self.channels
100	}
101
102	/// Where the input the next output starts with came from.
103	///
104	/// The resampler works in fixed chunks, so it holds back whatever didn't fill
105	/// one and the next output begins with those held frames rather than with the
106	/// samples just fed in. This is the stamp they arrived under, taken from
107	/// [`process`](Self::process) rather than derived by counting backwards from
108	/// the newest one, so a jump in the source timeline moves the audio after it
109	/// and leaves the audio before it where it belongs. Once nothing is buffered it
110	/// names where the next input lands, which is where a
111	/// [`drain`](Self::drain) or [`flush`](Self::flush) tail begins.
112	///
113	/// `None` until the first input, where the caller's own stamp is the answer.
114	pub(crate) fn held_at(&self) -> Option<moq_net::Timestamp> {
115		self.held
116	}
117
118	/// Drop everything held, buffered input and filter state alike, returning to
119	/// the just-constructed state.
120	///
121	/// The escape hatch for a *reported* discontinuity: where [`flush`](Self::flush)
122	/// ends the stream, this starts a new one in place, so audio from before the
123	/// gap can't bleed through the filter into audio from after it.
124	pub fn reset(&mut self) {
125		self.resampler.reset();
126		self.pending.clear();
127		self.skip = self.delay;
128		self.started = false;
129		self.held = None;
130	}
131
132	/// Resample what is still buffered, ending the stream.
133	///
134	/// The resampler only consumes whole chunks, so without this the last partial
135	/// chunk of a track is never converted and its audio is simply lost. Pads the
136	/// chunk out with silence and keeps only the output the real input earned, so
137	/// the padding costs a filter tail on the final samples rather than extra
138	/// audio.
139	///
140	/// Takes `self` because that padding runs the filter through silence the
141	/// caller never supplied: a stream that continues afterwards is a different
142	/// stream, which is what [`drain`](Self::drain) says out loud.
143	pub fn flush(mut self) -> Result<Vec<f32>, Error> {
144		self.drain()
145	}
146
147	/// End the current stream and start a new one in place, returning everything
148	/// the old one was still holding.
149	///
150	/// [`flush`](Self::flush) for a gap: the buffered input and the filter's tail
151	/// belong *before* the hole, so they come out as their own audio rather than
152	/// being filtered together with whatever follows it. Equivalent to a `flush`
153	/// followed by a fresh [`Resampler`], without rebuilding the filter tables.
154	pub fn drain(&mut self) -> Result<Vec<f32>, Error> {
155		let out = self.drained()?;
156		self.reset();
157		Ok(out)
158	}
159
160	fn drained(&mut self) -> Result<Vec<f32>, Error> {
161		// Not `pending == 0`: what the filter owes has nothing to do with what is
162		// buffered, so a stream that happens to end on a chunk boundary owes a tail
163		// just the same. Only one that never ran owes nothing.
164		if !self.started {
165			return Ok(Vec::new());
166		}
167
168		let pending = self.pending_frames();
169
170		// The filter runs centred, so every output frame is built from input around
171		// `delay` frames earlier and it still holds that much real audio no amount of
172		// input has pushed out. Ask for that much beyond what the pending input
173		// earns, feeding silence until it arrives, or a track converts its own
174		// ending into frames nobody reads.
175		//
176		// Only as much as `process` actually dropped off the front, though. That is
177		// the whole delay for a stream long enough to have emitted anything, and
178		// nothing at all for one that ended before it filled a chunk, where the skip
179		// still lies ahead and comes out of this call's own output.
180		let repaid = self.delay - self.skip;
181		let wanted = ((pending as f64 * self.ratio).round() as usize + repaid) * self.channels;
182
183		let mut out = Vec::new();
184		while out.len() < wanted {
185			// An empty result does not mean the filter is done: with a chunk smaller
186			// than the delay, a whole chunk's output can disappear into the skip while
187			// the audio behind it is still coming. Stop only when a chunk moves
188			// neither the output nor the skip, which cannot repeat.
189			let skip_before = self.skip;
190			self.pending.resize(self.chunk_frames * self.channels, 0.0);
191			let produced = self.convert()?;
192			if produced.is_empty() && self.skip == skip_before {
193				break;
194			}
195			out.extend_from_slice(&produced);
196		}
197
198		out.truncate(wanted);
199		Ok(out)
200	}
201
202	/// Resample interleaved `f32` input into interleaved `f32` output.
203	///
204	/// `at` is where the first of `samples` was presented, so buffered input keeps
205	/// its source timestamp across calls.
206	///
207	/// Returns whatever the resampler can produce given the input and
208	/// the chunk size; remaining samples are buffered for the next call.
209	pub fn process(&mut self, samples: &[f32], at: moq_net::Timestamp) -> Result<Vec<f32>, Error> {
210		if !samples.len().is_multiple_of(self.channels) {
211			return Err(Error::Misaligned {
212				got: samples.len(),
213				expected: samples.len().next_multiple_of(self.channels),
214			});
215		}
216
217		// Nothing buffered means the output resumes with these samples.
218		if self.pending.is_empty() {
219			self.held = Some(at);
220		}
221
222		self.started |= !samples.is_empty();
223		self.pending.extend_from_slice(samples);
224		let buffered = self.pending.len();
225		let out = self.convert()?;
226
227		// Earlier calls leave less than one chunk, so consuming any chunk also
228		// consumes all their samples. The remainder belongs to this packet.
229		// Convert its total consumed duration once to preserve fractional progress.
230		if self.pending.len() < buffered {
231			let consumed = (samples.len() - self.pending.len()) / self.channels;
232			let elapsed =
233				moq_net::Timestamp::from_scale(consumed as u64, self.input_rate as u64)?.convert(at.scale())?;
234			self.held = Some(at.checked_add(elapsed)?);
235		}
236
237		Ok(out)
238	}
239
240	/// Convert every whole chunk that is buffered, keeping the remainder.
241	fn convert(&mut self) -> Result<Vec<f32>, Error> {
242		let chunk_samples = self.chunk_frames * self.channels;
243		let mut out = Vec::new();
244		while self.pending.len() >= chunk_samples {
245			for (frame_idx, frame) in self.pending[..chunk_samples].chunks_exact(self.channels).enumerate() {
246				for (ch, &sample) in frame.iter().enumerate() {
247					self.input_planar[ch][frame_idx] = sample;
248				}
249			}
250
251			let input = SequentialSliceOfVecs::new(&self.input_planar, self.channels, self.chunk_frames)
252				.expect("resampler input buffer dimensions");
253			let mut output =
254				SequentialSliceOfVecs::new_mut(&mut self.output_planar, self.channels, self.output_frames_max)
255					.expect("resampler output buffer dimensions");
256			let (_, produced) = self.resampler.process_into_buffer(&input, &mut output, None)?;
257
258			let prev_len = out.len();
259			out.resize(prev_len + produced * self.channels, 0.0);
260			for frame_idx in 0..produced {
261				for ch in 0..self.channels {
262					out[prev_len + frame_idx * self.channels + ch] = self.output_planar[ch][frame_idx];
263				}
264			}
265
266			self.pending.drain(..chunk_samples);
267		}
268
269		// Drop the filter's startup silence rather than passing it on as audio. What
270		// it costs is paid back by `flush`, which drains the same amount at the end,
271		// so the output keeps the duration of the input that produced it.
272		if self.skip > 0 {
273			let drop = self.skip.min(out.len() / self.channels) * self.channels;
274			out.drain(..drop);
275			self.skip -= drop / self.channels;
276		}
277
278		Ok(out)
279	}
280}
281
282/// Whether [`remix`] can produce this channel count, checked up front so a
283/// consumer fails at construction rather than on its first frame.
284pub(crate) fn validate_channels(count: u32) -> Result<(), Error> {
285	match count {
286		1 | 2 => Ok(()),
287		other => Err(Error::Unsupported(format!(
288			"channel remix only supports mono and stereo (got {other})"
289		))),
290	}
291}
292
293/// Remix interleaved mono/stereo PCM into the requested channel count.
294pub(crate) fn remix(samples: &[f32], input_channels: u32, output_channels: u32) -> Result<Vec<f32>, Error> {
295	match (input_channels, output_channels) {
296		(1, 1) | (2, 2) => Ok(samples.to_vec()),
297		(1, 2) => {
298			let mut output = Vec::with_capacity(samples.len() * 2);
299			for &sample in samples {
300				output.extend_from_slice(&[sample, sample]);
301			}
302			Ok(output)
303		}
304		(2, 1) => Ok(samples.chunks_exact(2).map(|pair| (pair[0] + pair[1]) * 0.5).collect()),
305		_ => Err(Error::Unsupported(format!(
306			"channel remix only supports mono and stereo (got {input_channels} to {output_channels})"
307		))),
308	}
309}
310
311#[cfg(test)]
312mod tests {
313	use super::*;
314
315	/// `frames` into a stream at `rate`, as a timestamp in the source's own scale.
316	fn at(frames: u64, rate: u64) -> moq_net::Timestamp {
317		moq_net::Timestamp::from_scale(frames, rate).unwrap()
318	}
319
320	#[test]
321	fn rejects_zero_chunk_frames() {
322		let r = Resampler::new(48_000, 48_000, 2, 0);
323		assert!(matches!(r, Err(Error::Unsupported(_))));
324	}
325
326	#[test]
327	fn upsample_44100_to_48000_preserves_energy_roughly() {
328		let mut r = Resampler::new(44_100, 48_000, 1, 1024).unwrap();
329		let input: Vec<f32> = (0..44_100)
330			.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44_100.0).sin() * 0.5)
331			.collect();
332		let mut out = r.process(&input, at(0, 44_100)).unwrap();
333		out.extend(r.process(&vec![0.0; 1024], at(44_100, 44_100)).unwrap());
334		assert!(
335			(47_000..50_000).contains(&out.len()),
336			"expected ~48k samples, got {}",
337			out.len()
338		);
339	}
340
341	/// The sinc filter is centred, so the end of a track only reaches the output
342	/// once further input has passed through it. Without draining that, a track
343	/// converts its own ending into frames nobody ever reads, and the tail comes
344	/// out silent however loud it was.
345	#[test]
346	fn flush_drains_the_delayed_tail() {
347		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
348
349		// A full-scale sample near the end of the track, silence around it. Not the
350		// very last one: draining stops at the filter's centre rather than emitting
351		// its ringing past the end of the signal, so the final sample keeps only
352		// half its response however far this drains.
353		let mut input = vec![0.0f32; 1024];
354		input[1000] = 1.0;
355
356		let body = r.process(&input, at(0, 44_100)).unwrap();
357		let tail = r.flush().unwrap();
358
359		let peak = |samples: &[f32]| samples.iter().fold(0.0f32, |max, s| max.max(s.abs()));
360		assert!(peak(&body) < 0.01, "the sample emerged early: peak {}", peak(&body));
361		assert!(peak(&tail) > 0.5, "the tail lost the sample: peak {}", peak(&tail));
362	}
363
364	/// The filter owes its tail whether or not anything is buffered, so a track
365	/// whose length lands exactly on a chunk boundary has to drain too. With
366	/// 1024-sample frames at 48 kHz that lands every fifteenth one against the
367	/// 960-frame chunk, so it is not a corner a real stream avoids.
368	#[test]
369	fn flush_drains_on_an_exact_chunk_boundary() {
370		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
371
372		// Exactly two chunks of input, so nothing is left pending.
373		let mut input = vec![0.0f32; 1764];
374		input[1750] = 1.0;
375
376		let body = r.process(&input, at(0, 44_100)).unwrap();
377		assert_eq!(r.pending_frames(), 0, "the input should divide evenly");
378
379		let tail = r.flush().unwrap();
380
381		// Not exactly zero: a centred sinc has a precursor, so a trace of the sample
382		// leads it into the body. The audio itself is still all in the tail.
383		let peak = |samples: &[f32]| samples.iter().fold(0.0f32, |max, s| max.max(s.abs()));
384		assert!(peak(&body) < 0.01, "the sample emerged early: peak {}", peak(&body));
385		assert!(peak(&tail) > 0.5, "the tail lost the sample: peak {}", peak(&tail));
386	}
387
388	/// A stream that ends before it fills a chunk never emitted anything, so the
389	/// filter's startup silence is still ahead of it and comes out of the flush's
390	/// own output. Repaying a skip that has not happened yet hands back a stream
391	/// longer than its source.
392	#[test]
393	fn flush_sizes_a_stream_shorter_than_a_chunk() {
394		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
395
396		let body = r.process(&[0.25f32; 441], at(0, 44_100)).unwrap();
397		let tail = r.flush().unwrap();
398
399		// 441 frames at 44.1 kHz is 480 at 48 kHz, and that is all it can be.
400		let total = body.len() + tail.len();
401		assert!((475..=485).contains(&total), "unexpected total: {total}");
402	}
403
404	/// `chunk_frames` is the caller's to choose, and a small one can be shorter
405	/// than the filter's delay. Then a whole chunk's output disappears into the
406	/// startup skip, which used to read as "the filter is done" and drop the
407	/// entire stream.
408	#[test]
409	fn flush_survives_a_chunk_smaller_than_the_delay() {
410		let mut r = Resampler::new(44_100, 48_000, 1, 32).unwrap();
411
412		let body = r.process(&[0.5f32; 20], at(0, 44_100)).unwrap();
413		let tail = r.flush().unwrap();
414
415		let total = body.len() + tail.len();
416		assert!((18..=26).contains(&total), "unexpected total: {total}");
417		assert!(
418			tail.iter().any(|s| s.abs() > 0.25),
419			"the stream came back silent: peak {}",
420			tail.iter().fold(0.0f32, |m, s| m.max(s.abs()))
421		);
422	}
423
424	/// A gap ends one stream and starts another through the same filter, so the
425	/// drain has to hand back everything `flush` would and then be usable again,
426	/// with none of the first stream's audio reaching the second.
427	#[test]
428	fn drain_ends_the_stream_and_starts_a_new_one() {
429		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
430
431		let mut input = vec![0.0f32; 1024];
432		input[1000] = 1.0;
433		let body = r.process(&input, at(0, 44_100)).unwrap();
434		let tail = r.drain().unwrap();
435
436		let peak = |samples: &[f32]| samples.iter().fold(0.0f32, |max, s| max.max(s.abs()));
437		assert!(peak(&tail) > 0.5, "the tail lost the sample: peak {}", peak(&tail));
438		assert!(
439			(1105..=1120).contains(&(body.len() + tail.len())),
440			"unexpected total: {}",
441			body.len() + tail.len()
442		);
443
444		// Silence in, silence out: nothing is carried over the gap.
445		assert_eq!(r.pending_frames(), 0);
446		assert_eq!(r.held_at(), None, "the drain should forget where the old stream was");
447		let after = r.process(&vec![0.0f32; 1024], at(2048, 44_100)).unwrap();
448		assert!(peak(&after) < 0.01, "audio crossed the gap: peak {}", peak(&after));
449	}
450
451	/// The output starts with the frames held back from an earlier call, so it
452	/// begins where those arrived rather than where the newest input did. Counting
453	/// backwards from the newest stamp gets the same answer only while the source
454	/// runs contiguous; a jump moves it by the whole jump.
455	#[test]
456	fn held_frames_keep_the_stamp_they_arrived_under() {
457		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
458
459		// Half a chunk, so all of it is held and nothing comes out.
460		assert!(r.process(&[0.25f32; 441], at(0, 44_100)).unwrap().is_empty());
461		assert_eq!(r.held_at(), Some(at(0, 44_100)));
462
463		// A second later, and the output it completes still starts back at zero.
464		assert!(!r.process(&[0.25f32; 441], at(44_100, 44_100)).unwrap().is_empty());
465		assert_eq!(
466			r.held_at(),
467			Some(at(44_541, 44_100)),
468			"the tail starts at the end of the last packet consumed"
469		);
470	}
471
472	/// With nothing buffered the next output starts with the next input, so a
473	/// stream that resumes somewhere else stamps from there and not from where the
474	/// old one left off.
475	#[test]
476	fn an_emptied_buffer_re_anchors_on_the_next_input() {
477		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
478
479		r.process(&[0.25f32; 882], at(0, 44_100)).unwrap();
480		assert_eq!(r.pending_frames(), 0);
481
482		r.process(&[0.25f32; 441], at(44_100, 44_100)).unwrap();
483		assert_eq!(r.held_at(), Some(at(44_100, 44_100)));
484	}
485
486	#[test]
487	fn leftover_frames_keep_the_new_packet_timestamp() {
488		let mut r = Resampler::new(44_100, 48_000, 1, 882).unwrap();
489		r.process(&[0.25; 441], at(0, 44_100)).unwrap();
490		r.process(&[0.25; 882], at(44_100, 44_100)).unwrap();
491		assert_eq!(r.pending_frames(), 441);
492		assert_eq!(r.held_at(), Some(at(44_541, 44_100)));
493	}
494
495	#[test]
496	fn held_timestamp_preserves_fractional_chunk_progress() {
497		let mut r = Resampler::new(11_025, 48_000, 1, 220).unwrap();
498		r.process(&vec![0.25; 11_025], at(0, 1000)).unwrap();
499		assert_eq!(r.pending_frames(), 25);
500		assert_eq!(r.held_at(), Some(at(997, 1000)));
501	}
502
503	#[test]
504	fn remix_mono_to_stereo_duplicates_samples() {
505		assert_eq!(remix(&[1.0, 2.0], 1, 2).unwrap(), [1.0, 1.0, 2.0, 2.0]);
506	}
507
508	#[test]
509	fn remix_stereo_to_mono_averages_channels() {
510		assert_eq!(remix(&[1.0, 3.0, 2.0, 4.0], 2, 1).unwrap(), [2.0, 3.0]);
511	}
512}