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};
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}
23
24impl Consumer {
25	/// Subscribe to `name` in `broadcast`, using the catalog entry to pick the
26	/// codec.
27	pub async fn new(
28		broadcast: &moq_net::broadcast::Consumer,
29		catalog: &hang::catalog::AudioConfig,
30		name: impl Into<String>,
31		config: Config,
32	) -> Result<Self, Error> {
33		let decoder = Decoder::new(catalog)?;
34		let sample_rate = config.sample_rate.unwrap_or_else(|| decoder.sample_rate());
35		let channels = config.channels.unwrap_or_else(|| decoder.channel_count());
36		crate::opus::validate_channels(channels)?;
37
38		let resampler = if sample_rate == decoder.sample_rate() {
39			None
40		} else {
41			let chunk_frames = (decoder.sample_rate() as usize * 20) / 1000;
42			Some(Resampler::new(
43				decoder.sample_rate(),
44				sample_rate,
45				decoder.channel_count(),
46				chunk_frames,
47			)?)
48		};
49
50		let name = name.into();
51		let track = broadcast
52			.track(&name)?
53			.subscribe(moq_net::track::Subscription::default().with_priority(hang::catalog::PRIORITY.audio))
54			.await?;
55		// The catalog says how the track is framed, and it is not always the legacy
56		// wire: `moq import fmp4` publishes CMAF. Reading a moof+mdat fragment as a
57		// varint timestamp plus a payload decodes to garbage rather than failing.
58		let container = moq_mux::catalog::hang::Container::try_from(&catalog.container)?;
59		let mut track = moq_mux::container::Consumer::new(track, container);
60		if let Some(latency) = config.latency_max {
61			track = track.with_latency(latency);
62		}
63
64		Ok(Self {
65			decoder,
66			track,
67			resampler,
68			config,
69			resolved_sample_rate: sample_rate,
70			resolved_channels: channels,
71		})
72	}
73
74	/// The config this consumer was built with.
75	pub fn config(&self) -> &Config {
76		&self.config
77	}
78
79	/// Sample rate samples are actually delivered at, which is
80	/// [`Config::sample_rate`] resolved against the catalog.
81	pub fn sample_rate(&self) -> u32 {
82		self.resolved_sample_rate
83	}
84
85	/// Channel count samples are actually delivered at, which is
86	/// [`Config::channels`] resolved against the catalog.
87	pub fn channels(&self) -> u32 {
88		self.resolved_channels
89	}
90
91	/// Read the next decoded PCM frame, or `None` when the track ends.
92	pub async fn read(&mut self) -> Result<Option<Frame>, Error> {
93		let Some(mux_frame) = self.track.read().await? else {
94			return Ok(None);
95		};
96
97		let decoded = self.decoder.decode(&mux_frame.payload)?;
98		let pcm = match self.resampler.as_mut() {
99			Some(r) => r.process(&decoded)?,
100			None => decoded,
101		};
102		let pcm = if self.decoder.channel_count() == self.resolved_channels {
103			pcm
104		} else {
105			remix(&pcm, self.decoder.channel_count(), self.resolved_channels)?
106		};
107
108		let bytes = self.config.format.from_interleaved_f32(&pcm, self.resolved_channels)?;
109		Ok(Some(Frame {
110			timestamp: mux_frame.timestamp,
111			data: Bytes::from(bytes),
112		}))
113	}
114}
115
116#[cfg(test)]
117mod tests {
118	use moq_net::Timestamp;
119
120	use super::*;
121	use crate::Format;
122	use crate::encode::{Encoder, Input, Options, Producer};
123
124	#[tokio::test]
125	async fn remixes_mono_stream_to_stereo_output() {
126		let mut broadcast = moq_net::broadcast::Info::new().produce();
127		let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
128		let subscriber = broadcast.consume();
129		let input = Input {
130			format: Format::F32,
131			sample_rate: 48_000,
132			channels: 1,
133		};
134		let options = Options {
135			track: Some("audio".to_string()),
136			..Options::default()
137		};
138		let mut producer = Producer::new(&mut broadcast, catalog, input.clone(), &options).unwrap();
139		let catalog = Encoder::new(&crate::encode::Config::new(input)).unwrap().catalog();
140		let mut consumer = Consumer::new(
141			&subscriber,
142			&catalog,
143			"audio",
144			Config {
145				channels: Some(2),
146				..Config::new()
147			},
148		)
149		.await
150		.unwrap();
151
152		let samples = vec![0.1f32; 960];
153		let mut data = Vec::with_capacity(samples.len() * size_of::<f32>());
154		for sample in samples {
155			data.extend_from_slice(&sample.to_le_bytes());
156		}
157		producer
158			.write(&Frame {
159				timestamp: Timestamp::ZERO,
160				data: data.into(),
161			})
162			.unwrap();
163
164		let frame = consumer.read().await.unwrap().expect("decoded frame");
165		let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
166		assert_eq!(samples.len(), (960 - 312) * 2);
167		for pair in samples.chunks_exact(2) {
168			assert_eq!(pair[0], pair[1]);
169		}
170	}
171
172	#[tokio::test]
173	async fn reads_the_container_the_catalog_declares() {
174		let mut broadcast = moq_net::broadcast::Info::new().produce();
175		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
176		let subscriber = broadcast.consume();
177
178		let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 1);
179		catalog.container = hang::catalog::Container::Loc;
180
181		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Loc);
182		let mut consumer = Consumer::new(
183			&subscriber,
184			&catalog,
185			"audio",
186			Config {
187				format: Format::F32,
188				..Config::new()
189			},
190		)
191		.await
192		.unwrap();
193
194		let samples = [0.25f32, -0.5, 0.75, -1.0];
195		let payload: Vec<u8> = samples.iter().flat_map(|sample| sample.to_le_bytes()).collect();
196		producer
197			.write(moq_mux::container::Frame {
198				timestamp: Timestamp::ZERO,
199				duration: None,
200				payload: payload.into(),
201				keyframe: true,
202			})
203			.unwrap();
204
205		let frame = consumer.read().await.unwrap().expect("decoded frame");
206		assert_eq!(
207			Format::F32.as_interleaved_f32(&frame.data, 1).unwrap().as_ref(),
208			samples
209		);
210	}
211
212	/// The catalog picks the framing, not this crate. Hardcoding the legacy wire
213	/// read a CMAF fragment as a varint timestamp plus a payload, which handed the
214	/// codec garbage instead of failing, so anything published by `moq import
215	/// fmp4` was undecodable.
216	#[tokio::test]
217	async fn decodes_a_cmaf_framed_track() {
218		let input = Input {
219			format: Format::F32,
220			sample_rate: 48_000,
221			channels: 2,
222		};
223
224		// One real Opus packet, so a mis-framed read can't accidentally decode.
225		let mut encoder = Encoder::new(&crate::encode::Config::new(input.clone())).unwrap();
226		let mut catalog = encoder.catalog();
227		let pcm = vec![0.0f32; encoder.frame_size() * encoder.codec_channels() as usize];
228		let packet = encoder.encode(&pcm).unwrap();
229
230		// Re-describe the same rendition as CMAF and publish it that way.
231		let muxer = moq_mux::container::fmp4::Muxer::audio(&catalog).unwrap();
232		let init = muxer.init().unwrap().expect("an out-of-band codec has an init segment");
233		catalog.container = hang::catalog::Container::Cmaf { init };
234
235		let mut broadcast = moq_net::broadcast::Info::new().produce();
236		let subscriber = broadcast.consume();
237		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
238		let container = moq_mux::catalog::hang::Container::try_from(&catalog.container).unwrap();
239		let mut producer = moq_mux::container::Producer::new(track, container);
240
241		let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
242			.await
243			.unwrap();
244
245		producer
246			.write(moq_mux::container::Frame {
247				timestamp: Timestamp::ZERO,
248				payload: packet,
249				keyframe: true,
250				duration: None,
251			})
252			.unwrap();
253		producer.cut(None).unwrap();
254
255		// The whole packet decodes: one 20 ms Opus frame at 48 kHz, less the pre-skip
256		// trimmed off the first packet. Reading the fragment as legacy hands the codec
257		// a slice of the moof instead, which still decodes, just to a shorter buffer.
258		let frame = consumer.read().await.unwrap().expect("decoded frame");
259		// `as_micros`, not `==`: the CMAF path carries the fmp4 timescale and
260		// `Timestamp`'s equality is structural, so the scales would have to match too.
261		assert_eq!(frame.timestamp.as_micros(), 0);
262		let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
263		assert_eq!(samples.len(), (960 - 312) * 2);
264	}
265}