Skip to main content

moq_audio/decode/
consumer.rs

1//! Subscribe to an encoded audio track and emit raw PCM.
2
3use bytes::Bytes;
4
5use super::decoder::{Config, Decoder};
6use crate::resample::{Resampler, remix, validate_channels};
7use crate::{Error, Frame};
8
9/// Subscribe to a moq-mux audio track and emit decoded PCM in the layout
10/// declared by [`Config`].
11///
12/// The mirror of [`encode::Producer`](crate::encode::Producer): output format /
13/// sample rate / channel count are fixed at construction, and
14/// [`read`](Self::read) returns plain [`Frame`]s.
15pub struct Consumer {
16	decoder: Decoder,
17	track: moq_mux::container::Consumer<moq_mux::catalog::hang::Container>,
18	resampler: Option<Resampler>,
19	config: Config,
20	resolved_sample_rate: u32,
21	resolved_channels: u32,
22	/// One past the last sample handed to the resampler, so the tail it is still
23	/// holding at end of track can be stamped. `None` until the first packet.
24	tail: Option<moq_net::Timestamp>,
25	/// Timestamp of the first encoded packet, used to interpret a terminal marker.
26	epoch: Option<moq_net::Timestamp>,
27	/// Codec-rate terminal frames emitted since `terminal_start`.
28	frames_decoded: usize,
29	/// Logical endpoint carried by an empty legacy frame before terminal packets.
30	end: Option<moq_net::Timestamp>,
31	/// Presentation time of the first decoded terminal frame.
32	terminal_start: Option<moq_net::Timestamp>,
33	/// Last container discontinuity applied to codec and resampler state.
34	discontinuity: u64,
35}
36
37impl Consumer {
38	/// Subscribe to `name` in `broadcast`, using the catalog entry to pick the
39	/// codec.
40	pub async fn new(
41		broadcast: &moq_net::broadcast::Consumer,
42		catalog: &hang::catalog::AudioConfig,
43		name: impl Into<String>,
44		config: Config,
45	) -> Result<Self, Error> {
46		let decoder = Decoder::new(catalog)?;
47		let sample_rate = config.sample_rate.unwrap_or_else(|| decoder.sample_rate());
48		let channels = config.channels.unwrap_or_else(|| decoder.channel_count());
49		validate_channels(channels)?;
50
51		let resampler = if sample_rate == decoder.sample_rate() {
52			None
53		} else {
54			let chunk_frames = (decoder.sample_rate() as usize * 20) / 1000;
55			Some(Resampler::new(
56				decoder.sample_rate(),
57				sample_rate,
58				decoder.channel_count(),
59				chunk_frames,
60			)?)
61		};
62
63		let name = name.into();
64		let track = broadcast
65			.track(&name)?
66			.subscribe(moq_net::track::Subscription::default().with_priority(hang::catalog::PRIORITY.audio))
67			.await?;
68		// The catalog says how the track is framed, and it is not always the legacy
69		// wire: `moq import fmp4` publishes CMAF. Reading a moof+mdat fragment as a
70		// varint timestamp plus a payload decodes to garbage rather than failing.
71		let container = moq_mux::catalog::hang::Container::try_from(&catalog.container)?;
72		let mut track = moq_mux::container::Consumer::new(track, container);
73		if let Some(latency) = config.latency_max {
74			track = track.with_latency(latency);
75		}
76
77		Ok(Self {
78			decoder,
79			track,
80			resampler,
81			config,
82			resolved_sample_rate: sample_rate,
83			resolved_channels: channels,
84			tail: None,
85			epoch: None,
86			frames_decoded: 0,
87			end: None,
88			terminal_start: None,
89			discontinuity: 0,
90		})
91	}
92
93	/// The config this consumer was built with.
94	pub fn config(&self) -> &Config {
95		&self.config
96	}
97
98	/// Sample rate samples are actually delivered at, which is
99	/// [`Config::sample_rate`] resolved against the catalog.
100	pub fn sample_rate(&self) -> u32 {
101		self.resolved_sample_rate
102	}
103
104	/// Channel count samples are actually delivered at, which is
105	/// [`Config::channels`] resolved against the catalog.
106	pub fn channels(&self) -> u32 {
107		self.resolved_channels
108	}
109
110	/// Read the next decoded PCM frame, or `None` when the track ends.
111	pub async fn read(&mut self) -> Result<Option<Frame>, Error> {
112		loop {
113			let mux_frame = self.track.read().await?;
114			self.apply_discontinuity()?;
115			let Some(mux_frame) = mux_frame else {
116				return self.flush();
117			};
118
119			if let Some(end) = self.track.end()
120				&& self.end != Some(end)
121			{
122				self.end = Some(end);
123				self.frames_decoded = 0;
124				self.terminal_start = None;
125			}
126
127			let rate = self.decoder.sample_rate();
128			let epoch = *self.epoch.get_or_insert(mux_frame.timestamp);
129			let mut decoded = self.decoder.decode(&mux_frame.payload)?;
130			if let Some(end) = self.end {
131				let terminal_start = *self
132					.terminal_start
133					.get_or_insert(rewind(mux_frame.timestamp, self.decoder.delay(), rate)?.max(epoch));
134				let total = frames_between(terminal_start, end, rate)?;
135				let remaining = total.saturating_sub(self.frames_decoded);
136				decoded.truncate(remaining.saturating_mul(self.decoder.channel_count() as usize));
137			}
138
139			let frames = decoded.len() / self.decoder.channel_count().max(1) as usize;
140			let decoded_at = if let Some(terminal_start) = self.terminal_start {
141				advance(terminal_start, self.frames_decoded, rate)?
142			} else {
143				mux_frame.timestamp
144			};
145			if self.end.is_some() {
146				self.frames_decoded += frames;
147			}
148			if decoded.is_empty() {
149				continue;
150			}
151
152			let (pcm, timestamp) = match self.resampler.as_mut() {
153				// The resampler works in fixed chunks, so it holds back whatever didn't
154				// fill one. What comes out next starts with those held-back samples, which
155				// arrived before this packet did: stamping it with this packet's timestamp
156				// would place the audio late by up to a chunk, sawtoothing A/V sync.
157				Some(r) => {
158					let pending = r.pending_frames();
159					let skipped = r.skipped();
160					let pcm = r.process(&decoded)?;
161					(pcm, self.starts_at(decoded_at, pending, skipped, rate)?)
162				}
163				None => (decoded, decoded_at),
164			};
165
166			self.tail = Some(advance(decoded_at, frames, rate)?);
167
168			return Ok(Some(self.frame(pcm, timestamp)?));
169		}
170	}
171
172	/// Reset every stateful decode stage before the first packet of a new epoch.
173	fn apply_discontinuity(&mut self) -> Result<(), Error> {
174		let discontinuity = self.track.discontinuity();
175		if discontinuity == self.discontinuity {
176			return Ok(());
177		}
178
179		self.discontinuity = discontinuity;
180		self.decoder.reset()?;
181		if let Some(resampler) = self.resampler.as_mut() {
182			resampler.reset();
183		}
184		self.tail = None;
185		self.epoch = None;
186		self.frames_decoded = 0;
187		self.end = None;
188		self.terminal_start = None;
189		Ok(())
190	}
191
192	/// The tail the resampler is still holding when the track ends, once.
193	///
194	/// Without it the last partial chunk is dropped, which is up to a chunk of
195	/// audio missing from the end of every resampled track. Flushing consumes the
196	/// resampler, which is what makes calling this on every later poll return
197	/// `None` rather than more tails.
198	fn flush(&mut self) -> Result<Option<Frame>, Error> {
199		let (Some(resampler), Some(tail)) = (self.resampler.take(), self.tail) else {
200			return Ok(None);
201		};
202
203		let pending = resampler.pending_frames();
204		let skipped = resampler.skipped();
205		let pcm = resampler.flush()?;
206		if pcm.is_empty() {
207			return Ok(None);
208		}
209
210		let timestamp = self.starts_at(tail, pending, skipped, self.decoder.sample_rate())?;
211		Ok(Some(self.frame(pcm, timestamp)?))
212	}
213
214	/// Where the output the resampler is about to hand back actually begins.
215	///
216	/// Two things sit between a packet's timestamp and the audio that comes out of
217	/// it. The resampler is holding `pending` input frames from before this packet,
218	/// which the output starts with. And it has dropped `skipped` output frames of
219	/// its own startup silence, so everything it emits from then on runs that much
220	/// short of the input it was built from. Reach back over both, each in its own
221	/// rate, or the output is stamped after the audio it contains.
222	fn starts_at(
223		&self,
224		timestamp: moq_net::Timestamp,
225		pending: usize,
226		skipped: usize,
227		rate: u32,
228	) -> Result<moq_net::Timestamp, Error> {
229		let timestamp = rewind(timestamp, pending, rate)?;
230		rewind(timestamp, skipped, self.resolved_sample_rate)
231	}
232
233	/// Remix and pack decoded PCM into an output frame.
234	fn frame(&self, pcm: Vec<f32>, timestamp: moq_net::Timestamp) -> Result<Frame, Error> {
235		let pcm = if self.decoder.channel_count() == self.resolved_channels {
236			pcm
237		} else {
238			remix(&pcm, self.decoder.channel_count(), self.resolved_channels)?
239		};
240
241		let bytes = self.config.format.from_interleaved_f32(&pcm, self.resolved_channels)?;
242		Ok(Frame {
243			timestamp,
244			data: Bytes::from(bytes),
245		})
246	}
247}
248
249/// `timestamp` moved forward by `frames` at `sample_rate`, in its own timescale.
250fn advance(timestamp: moq_net::Timestamp, frames: usize, sample_rate: u32) -> Result<moq_net::Timestamp, Error> {
251	if frames == 0 {
252		return Ok(timestamp);
253	}
254
255	let offset = moq_net::Timestamp::from_scale(frames as u64, sample_rate as u64)?.convert(timestamp.scale())?;
256	Ok(timestamp.checked_add(offset)?)
257}
258
259/// Codec-rate frames in the interval, rounding a microsecond marker to the nearest frame.
260fn frames_between(start: moq_net::Timestamp, end: moq_net::Timestamp, sample_rate: u32) -> Result<usize, Error> {
261	let duration = end.checked_sub(start)?;
262	let frames = (std::time::Duration::from(duration).as_nanos() * sample_rate as u128 + 500_000_000) / 1_000_000_000;
263	usize::try_from(frames).map_err(|_| Error::Unsupported("audio duration does not fit in memory".into()))
264}
265
266/// `timestamp` moved back by `frames` at `sample_rate`, in its own timescale.
267///
268/// Saturates at zero rather than failing: a publisher whose first timestamps
269/// don't advance is odd, but it isn't a reason to end the track.
270fn rewind(timestamp: moq_net::Timestamp, frames: usize, sample_rate: u32) -> Result<moq_net::Timestamp, Error> {
271	if frames == 0 {
272		return Ok(timestamp);
273	}
274
275	let offset = moq_net::Timestamp::from_scale(frames as u64, sample_rate as u64)?.convert(timestamp.scale())?;
276	Ok(timestamp
277		.checked_sub(offset)
278		.unwrap_or(moq_net::Timestamp::new(0, timestamp.scale())?))
279}
280
281#[cfg(test)]
282mod tests {
283	use moq_net::Timestamp;
284
285	use super::*;
286	use crate::Format;
287	use crate::encode::{Encoder, Input, Options, Producer};
288
289	#[tokio::test]
290	async fn remixes_mono_stream_to_stereo_output() {
291		let mut broadcast = moq_net::broadcast::Info::new().produce();
292		let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
293		let subscriber = broadcast.consume();
294		let input = Input {
295			format: Format::F32,
296			sample_rate: 48_000,
297			channels: 1,
298		};
299		let options = Options {
300			track: Some("audio".to_string()),
301			..Options::default()
302		};
303		let mut producer = Producer::new(&mut broadcast, catalog, input.clone(), &options).unwrap();
304		let catalog = Encoder::new(&crate::encode::Config::new(input)).unwrap().catalog();
305		let mut consumer = Consumer::new(
306			&subscriber,
307			&catalog,
308			"audio",
309			Config {
310				channels: Some(2),
311				..Config::new()
312			},
313		)
314		.await
315		.unwrap();
316
317		let samples = vec![0.1f32; 960];
318		let mut data = Vec::with_capacity(samples.len() * size_of::<f32>());
319		for sample in samples {
320			data.extend_from_slice(&sample.to_le_bytes());
321		}
322		producer
323			.write(&Frame {
324				timestamp: Timestamp::ZERO,
325				data: data.into(),
326			})
327			.unwrap();
328
329		let frame = consumer.read().await.unwrap().expect("decoded frame");
330		let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
331		assert_eq!(samples.len(), (960 - 312) * 2);
332		for pair in samples.chunks_exact(2) {
333			assert_eq!(pair[0], pair[1]);
334		}
335	}
336
337	/// A packet whose sample count isn't a multiple of the resampler's chunk leaves
338	/// samples buffered, and the next output starts with those. Stamping that
339	/// output with the packet that completed the chunk puts it up to a chunk late,
340	/// which is a sawtooth in A/V sync rather than a constant offset. Any codec
341	/// whose frame is not a whole number of chunks reaches it: a 1024-sample frame
342	/// at 44.1 kHz never fills the 882-frame chunk evenly.
343	#[tokio::test]
344	async fn resampled_timestamps_follow_the_samples() {
345		let mut broadcast = moq_net::broadcast::Info::new().produce();
346		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
347		let subscriber = broadcast.consume();
348
349		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 44_100, 1);
350		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
351
352		let mut consumer = Consumer::new(
353			&subscriber,
354			&catalog,
355			"audio",
356			Config {
357				sample_rate: Some(48_000),
358				..Config::new()
359			},
360		)
361		.await
362		.unwrap();
363
364		// Two 1024-sample packets, back to back at the codec's own rate.
365		const FRAMES: u64 = 1024;
366		let payload: Bytes = vec![0u8; FRAMES as usize * size_of::<f32>()].into();
367		for packet in 0..2 {
368			producer
369				.write(moq_mux::container::Frame {
370					timestamp: moq_net::Timestamp::from_scale(packet * FRAMES, 44_100).unwrap(),
371					duration: None,
372					payload: payload.clone(),
373					keyframe: true,
374				})
375				.unwrap();
376		}
377
378		let first = consumer.read().await.unwrap().expect("decoded frame");
379		assert_eq!(first.timestamp.as_micros(), 0);
380
381		// Continuity, not a fixed number: the second frame starts where the first
382		// one's samples end, whatever they came to. Within a few frames rather than
383		// exactly, because the resampler emits whole frames and its count per chunk
384		// wobbles around the nominal ratio; a real hole (the samples it held back, or
385		// the startup silence it dropped) is twenty times this tolerance.
386		let second = consumer.read().await.unwrap().expect("decoded frame");
387		let first_frames = (first.data.len() / size_of::<f32>()) as u128;
388		let ends_at = first_frames * 1_000_000 / 48_000;
389		let gap = second.timestamp.as_micros().abs_diff(ends_at);
390		assert!(gap < 100, "expected the frames to meet, got a {gap} us gap");
391	}
392
393	/// The resampler only converts whole chunks, so the last partial one has to be
394	/// flushed at end of track or its audio is simply gone. A 1024-sample frame at
395	/// 44.1 kHz guarantees a remainder, never filling the 882-frame chunk evenly.
396	#[tokio::test]
397	async fn resampled_tail_survives_the_end_of_the_track() {
398		let mut broadcast = moq_net::broadcast::Info::new().produce();
399		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
400		let subscriber = broadcast.consume();
401
402		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 44_100, 1);
403		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
404
405		let mut consumer = Consumer::new(
406			&subscriber,
407			&catalog,
408			"audio",
409			Config {
410				sample_rate: Some(48_000),
411				..Config::new()
412			},
413		)
414		.await
415		.unwrap();
416
417		// One 1024-frame packet: 882 fill a chunk, 142 are left holding.
418		const FRAMES: usize = 1024;
419		let payload: Bytes = vec![0u8; FRAMES * size_of::<f32>()].into();
420		producer
421			.write(moq_mux::container::Frame {
422				timestamp: moq_net::Timestamp::ZERO,
423				duration: None,
424				payload,
425				keyframe: true,
426			})
427			.unwrap();
428		producer.finish().unwrap();
429
430		let first = consumer.read().await.unwrap().expect("decoded frame");
431		let first_frames = first.data.len() / size_of::<f32>();
432
433		let tail = consumer.read().await.unwrap().expect("flushed tail");
434		let tail_frames = tail.data.len() / size_of::<f32>();
435
436		// The 142 held-back frames at 44.1 kHz are ~155 at 48 kHz, plus the 69 the
437		// sinc filter still owes: it runs centred, so the end of the track only
438		// emerges once the flush has fed it silence to push it out.
439		assert!((215..=230).contains(&tail_frames), "unexpected tail: {tail_frames}");
440		// It picks up where the first frame's samples ended, within the same few
441		// frames of whole-frame rounding as above.
442		let ends_at = (first_frames as u128) * 1_000_000 / 48_000;
443		let gap = tail.timestamp.as_micros().abs_diff(ends_at);
444		assert!(gap < 100, "expected the tail to meet the body, got a {gap} us gap");
445
446		// Together they cover the packet and no more: 1024 frames at 44.1 kHz is
447		// ~1114 at 48 kHz. The filter's delay does not extend the stream, because
448		// what the drain adds here is what the start dropped off the front.
449		let total = first_frames + tail_frames;
450		assert!((1105..=1120).contains(&total), "unexpected total: {total}");
451		assert!(consumer.read().await.unwrap().is_none());
452	}
453
454	#[tokio::test]
455	async fn reads_the_container_the_catalog_declares() {
456		let mut broadcast = moq_net::broadcast::Info::new().produce();
457		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
458		let subscriber = broadcast.consume();
459
460		let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 1);
461		catalog.container = hang::catalog::Container::Loc;
462
463		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Loc);
464		let mut consumer = Consumer::new(
465			&subscriber,
466			&catalog,
467			"audio",
468			Config {
469				format: Format::F32,
470				..Config::new()
471			},
472		)
473		.await
474		.unwrap();
475
476		let samples = [0.25f32, -0.5, 0.75, -1.0];
477		let payload: Vec<u8> = samples.iter().flat_map(|sample| sample.to_le_bytes()).collect();
478		producer
479			.write(moq_mux::container::Frame {
480				timestamp: Timestamp::ZERO,
481				duration: None,
482				payload: payload.into(),
483				keyframe: true,
484			})
485			.unwrap();
486
487		let frame = consumer.read().await.unwrap().expect("decoded frame");
488		assert_eq!(
489			Format::F32.as_interleaved_f32(&frame.data, 1).unwrap().as_ref(),
490			samples
491		);
492	}
493
494	/// The catalog picks the framing, not this crate. Hardcoding the legacy wire
495	/// read a CMAF fragment as a varint timestamp plus a payload, which handed the
496	/// codec garbage instead of failing, so anything published by `moq import
497	/// fmp4` was undecodable.
498	#[tokio::test]
499	async fn decodes_a_cmaf_framed_track() {
500		let input = Input {
501			format: Format::F32,
502			sample_rate: 48_000,
503			channels: 2,
504		};
505
506		// One real Opus packet, so a mis-framed read can't accidentally decode.
507		let mut encoder = Encoder::new(&crate::encode::Config::new(input.clone())).unwrap();
508		let mut catalog = encoder.catalog();
509		let pcm = vec![0.0f32; encoder.frame_size() * encoder.codec_channels() as usize];
510		let packet = encoder.encode(&pcm).unwrap();
511
512		// Re-describe the same rendition as CMAF and publish it that way.
513		let muxer = moq_mux::container::fmp4::Muxer::audio(&catalog).unwrap();
514		let init = muxer.init().unwrap().expect("an out-of-band codec has an init segment");
515		catalog.container = hang::catalog::Container::Cmaf { init };
516
517		let mut broadcast = moq_net::broadcast::Info::new().produce();
518		let subscriber = broadcast.consume();
519		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
520		let container = moq_mux::catalog::hang::Container::try_from(&catalog.container).unwrap();
521		let mut producer = moq_mux::container::Producer::new(track, container);
522
523		let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
524			.await
525			.unwrap();
526
527		producer
528			.write(moq_mux::container::Frame {
529				timestamp: Timestamp::ZERO,
530				payload: packet,
531				keyframe: true,
532				duration: None,
533			})
534			.unwrap();
535		producer.cut(None).unwrap();
536
537		// The whole packet decodes: one 20 ms Opus frame at 48 kHz, less the pre-skip
538		// trimmed off the first packet. Reading the fragment as legacy hands the codec
539		// a slice of the moof instead, which still decodes, just to a shorter buffer.
540		let frame = consumer.read().await.unwrap().expect("decoded frame");
541		// `as_micros`, not `==`: the CMAF path carries the fmp4 timescale and
542		// `Timestamp`'s equality is structural, so the scales would have to match too.
543		assert_eq!(frame.timestamp.as_micros(), 0);
544		let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
545		assert_eq!(samples.len(), (960 - 312) * 2);
546	}
547}