Skip to main content

moq_audio/decode/
decoder.rs

1//! Audio decoder front end.
2//!
3//! Mirror of [`encode::Encoder`](crate::encode::Encoder): dispatches over the
4//! catalog codec and produces interleaved `f32` PCM.
5
6use std::time::Duration;
7
8use unsafe_libopus::{
9	OPUS_OK, OPUS_RESET_STATE, OpusDecoder, opus_decode_float, opus_decoder_create, opus_decoder_ctl_impl,
10	opus_decoder_destroy, varargs,
11};
12
13#[cfg(feature = "aac")]
14use symphonia_core::codecs::audio::AudioDecoder;
15
16use super::Decoded;
17#[cfg(feature = "aac")]
18use crate::aac;
19use crate::opus;
20use crate::pcm;
21use crate::{Activity, Error, Format};
22
23/// Opus packets cap at 120 ms (RFC 6716 ยง2.1.4).
24const MAX_FRAME_MS: usize = 120;
25
26/// Where a decoder starts on a track that already holds groups.
27///
28/// A track keeps its groups for a while after they are read, so a decoder does
29/// not always open on an empty one: a player rebuilding its decoder subscribes
30/// while its predecessor still holds groups, and a rendition switched away from
31/// and back to stays warm for the track's idle linger. What to do with that
32/// backlog depends on the consumer, and the two answers are opposites, so it is
33/// asked rather than guessed.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum Start {
37	/// The oldest group the track still holds, decoding everything cached.
38	///
39	/// What a recorder, an export, or anything reading a complete track wants,
40	/// and the default because dropping media a caller has not asked to drop is
41	/// the worse mistake.
42	#[default]
43	Oldest,
44	/// The newest group, skipping whatever is already cached.
45	///
46	/// What a live player wants. Without it a rebuilt decoder walks the whole
47	/// backlog at decode speed before reaching live media, which a viewer sees
48	/// as playback jumping backwards and then sprinting to catch up.
49	Latest,
50}
51
52/// Decoder configuration: the PCM layout to emit, plus the subscription's
53/// latency budget.
54///
55/// The mirror of [`encode::Config`](crate::encode::Config): it describes the
56/// output, since the codec's own shape is read from the catalog.
57///
58/// `#[non_exhaustive]`: build via [`Config::new`] (or `default()`) and set the
59/// optional fields, so future knobs don't break callers.
60#[derive(Clone, Debug, Default)]
61#[non_exhaustive]
62pub struct Config {
63	/// How to pack samples in each emitted frame.
64	pub format: Format,
65	/// Sample rate to emit at. `None` uses the codec's native rate from the
66	/// catalog; anything else resamples.
67	pub sample_rate: Option<u32>,
68	/// Channel count to emit. `None` uses the codec's native count; anything
69	/// else remixes mono and stereo at the decode boundary.
70	pub channels: Option<u32>,
71	/// Upper bound on buffering before skipping a stalled group.
72	///
73	/// Forwarded to [`moq_mux::container::Consumer::with_latency`]: if a group is
74	/// stuck and a newer group is more than this far ahead, the consumer skips.
75	/// `None` keeps the moq-mux default of zero, which skips aggressively. Set it
76	/// to the playout buffer you can tolerate (typically tens to a few hundred ms)
77	/// for the best congestion-vs-quality trade-off. The `_max` suffix is a
78	/// reminder that we never *add* latency here: the consumer skips only when
79	/// newer data is already this far ahead. A companion `latency_min` for
80	/// jitter-buffer padding will land in a follow-up.
81	pub latency_max: Option<Duration>,
82	/// Where to start on a track that already holds groups.
83	pub start: Start,
84}
85
86impl Config {
87	/// A default config: the codec's native rate and channel count, interleaved
88	/// `f32`, and the moq-mux default latency.
89	pub fn new() -> Self {
90		Self::default()
91	}
92}
93
94/// Decodes codec packets into interleaved `f32` PCM.
95///
96/// The bring-your-own-payload layer under [`Consumer`](super::Consumer): use it
97/// when the packets don't come from a plain track subscription.
98pub struct Decoder {
99	backend: Backend,
100	sample_rate: u32,
101	channel_count: u32,
102	delay: usize,
103}
104
105enum Backend {
106	Opus(Opus),
107	Pcm {
108		bytes_per_frame: usize,
109	},
110	#[cfg(feature = "aac")]
111	Aac(Box<Aac>),
112}
113
114struct Opus {
115	inner: *mut OpusDecoder,
116	pre_skip_remaining: usize,
117	max_frame_size: usize,
118	in_dtx: bool,
119}
120
121// SAFETY: see Encoder.
122unsafe impl Send for Opus {}
123
124/// Boxed in [`Backend`]: the symphonia decoder carries its own filterbank state,
125/// which is far larger than the other backends' handles.
126#[cfg(feature = "aac")]
127struct Aac {
128	inner: symphonia_codec_aac::AacDecoder,
129}
130
131impl Decoder {
132	/// Build a decoder from a catalog [`AudioConfig`](hang::catalog::AudioConfig).
133	///
134	/// Parses the OpusHead `description` if present; falls back to the catalog's
135	/// declared sample rate / channel count. PCM uses those catalog fields
136	/// directly and requires an absent `description`.
137	pub fn new(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
138		match &catalog.codec {
139			hang::catalog::AudioCodec::Opus => Self::new_opus(catalog),
140			hang::catalog::AudioCodec::Pcm => Self::new_pcm(catalog),
141			#[cfg(feature = "aac")]
142			hang::catalog::AudioCodec::AAC(aac) => Self::new_aac(catalog, aac.profile),
143			codec => Err(Error::Unsupported(format!("unsupported audio codec: {codec}"))),
144		}
145	}
146
147	fn new_opus(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
148		let (sample_rate, channel_count, pre_skip) = if let Some(desc) = &catalog.description {
149			let mut buf = desc.as_ref();
150			match moq_mux::codec::opus::Config::parse(&mut buf) {
151				Ok(head) => (head.sample_rate, head.channel_count, head.pre_skip),
152				Err(_) => (catalog.sample_rate, catalog.channel_count, 0),
153			}
154		} else {
155			(catalog.sample_rate, catalog.channel_count, 0)
156		};
157
158		opus::validate_rate(sample_rate)?;
159		let channels = opus::validate_channels(channel_count)?;
160
161		let mut err = 0i32;
162		// SAFETY: out-pointer is valid; inner is checked for null below.
163		let inner = unsafe { opus_decoder_create(sample_rate as i32, channels, &mut err) };
164		if err != OPUS_OK || inner.is_null() {
165			return Err(opus::error(err, "opus_decoder_create"));
166		}
167
168		let max_frame_size = (sample_rate as usize * MAX_FRAME_MS) / 1000;
169		let pre_skip_remaining = (pre_skip as usize * sample_rate as usize) / 48_000;
170
171		Ok(Self {
172			backend: Backend::Opus(Opus {
173				inner,
174				pre_skip_remaining,
175				max_frame_size,
176				in_dtx: false,
177			}),
178			sample_rate,
179			channel_count,
180			delay: pre_skip_remaining,
181		})
182	}
183
184	/// AAC-LC only, which is what every gateway that feeds this crate publishes.
185	///
186	/// HE-AAC is rejected however its config spells it: leading with SBR or PS
187	/// (mp4a.40.5 / .29), or leading with LC and declaring SBR in a sync extension
188	/// after the core. Symphonia decodes no SBR either way, so the alternative is
189	/// half-rate audio that sounds like a fault rather than an unsupported codec.
190	/// A stream that signals SBR only in band is indistinguishable from LC in the
191	/// config, and does decode as the core.
192	#[cfg(feature = "aac")]
193	fn new_aac(catalog: &hang::catalog::AudioConfig, profile: u8) -> Result<Self, Error> {
194		use symphonia_core::codecs::audio::well_known::CODEC_ID_AAC;
195		use symphonia_core::codecs::audio::{AudioCodecParameters, AudioDecoderOptions};
196
197		let description = aac::description(catalog, profile)?;
198
199		let mut params = AudioCodecParameters::new();
200		params
201			.for_codec(CODEC_ID_AAC)
202			.with_extra_data(description.to_vec().into_boxed_slice());
203
204		let inner = symphonia_codec_aac::AacDecoder::try_new(&params, &AudioDecoderOptions::default())
205			.map_err(|err| Error::Unsupported(format!("aac decoder: {err}")))?;
206
207		// Resolved by the decoder from the config, so this is what it will emit
208		// even when the catalog's own fields say otherwise.
209		let params = inner.codec_params();
210		let sample_rate = params
211			.sample_rate
212			.ok_or_else(|| Error::Unsupported("aac config declares no sample rate".into()))?;
213		let channel_count = params
214			.channels
215			.as_ref()
216			.map(|channels| channels.count())
217			.ok_or_else(|| Error::Unsupported("aac config declares no channels".into()))?;
218
219		Ok(Self {
220			backend: Backend::Aac(Box::new(Aac { inner })),
221			sample_rate,
222			channel_count: channel_count as u32,
223			delay: 0,
224		})
225	}
226
227	fn new_pcm(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
228		if catalog.sample_rate == 0 {
229			return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
230		}
231		if catalog.channel_count == 0 {
232			return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
233		}
234		if catalog.description.is_some() {
235			return Err(Error::Unsupported("pcm catalog description must be absent".into()));
236		}
237		let bitrate = pcm::bitrate(catalog.sample_rate, catalog.channel_count)?;
238		if catalog.bitrate.is_some_and(|declared| declared != bitrate) {
239			return Err(Error::Unsupported(format!(
240				"pcm catalog bitrate must be {bitrate} bits per second"
241			)));
242		}
243		let bytes_per_frame = pcm::frame_bytes(1, catalog.channel_count)?;
244
245		Ok(Self {
246			backend: Backend::Pcm { bytes_per_frame },
247			sample_rate: catalog.sample_rate,
248			channel_count: catalog.channel_count,
249			delay: 0,
250		})
251	}
252
253	/// The rate the codec decodes at, read from the catalog.
254	pub fn sample_rate(&self) -> u32 {
255		self.sample_rate
256	}
257
258	/// The channel count the codec decodes at, read from the catalog.
259	pub fn channel_count(&self) -> u32 {
260		self.channel_count
261	}
262
263	/// Reset codec history and reapply startup delay for a new discontinuous epoch.
264	pub fn reset(&mut self) -> Result<(), Error> {
265		self.reset_prediction()?;
266		if let Backend::Opus(opus) = &mut self.backend {
267			opus.pre_skip_remaining = self.delay;
268		}
269		Ok(())
270	}
271
272	/// Reset codec prediction after packet loss without reapplying stream startup delay.
273	pub(super) fn reset_prediction(&mut self) -> Result<(), Error> {
274		match &mut self.backend {
275			Backend::Opus(opus) => {
276				// SAFETY: `inner` owns a live decoder and OPUS_RESET_STATE takes no arguments.
277				let rc = unsafe { opus_decoder_ctl_impl(opus.inner, OPUS_RESET_STATE, varargs![]) };
278				if rc != OPUS_OK {
279					return Err(crate::opus::error(rc, "OPUS_RESET_STATE"));
280				}
281				opus.in_dtx = false;
282			}
283			Backend::Pcm { .. } => {}
284			#[cfg(feature = "aac")]
285			Backend::Aac(aac) => aac.inner.reset(),
286		}
287		Ok(())
288	}
289
290	/// How much startup delay is still to be trimmed, in native-rate frames.
291	///
292	/// Trimmed samples are media the packet covered even though nothing came out of
293	/// it, so a caller tracking where a packet ends has to add back whatever this
294	/// dropped across the call.
295	pub(super) fn delay_remaining(&self) -> usize {
296		match &self.backend {
297			Backend::Opus(opus) => opus.pre_skip_remaining,
298			Backend::Pcm { .. } => 0,
299			#[cfg(feature = "aac")]
300			Backend::Aac(_) => 0,
301		}
302	}
303
304	/// Decode one packet into interleaved `f32` PCM and report its codec activity.
305	///
306	/// Empty Opus packets invoke packet-loss concealment. Loss during DTX remains
307	/// classified as DTX, while loss during active audio remains active.
308	pub fn decode(&mut self, packet: &[u8]) -> Result<Decoded, Error> {
309		match &mut self.backend {
310			Backend::Opus(opus) => {
311				let mut out = vec![0.0f32; opus.max_frame_size * self.channel_count as usize];
312				// SAFETY: `inner` owns a live OpusDecoder; packet/out slices are
313				// bounded by the lengths we pass.
314				let samples = unsafe {
315					opus_decode_float(
316						&mut *opus.inner,
317						packet.as_ptr(),
318						packet.len() as i32,
319						out.as_mut_ptr(),
320						opus.max_frame_size as i32,
321						0,
322					)
323				};
324				if samples < 0 {
325					return Err(crate::opus::decode_error(samples));
326				}
327				out.truncate(samples as usize * self.channel_count as usize);
328				let trim_frames = opus.pre_skip_remaining.min(samples as usize);
329				if trim_frames > 0 {
330					let trim_samples = trim_frames * self.channel_count as usize;
331					out.copy_within(trim_samples.., 0);
332					out.truncate(out.len() - trim_samples);
333					opus.pre_skip_remaining -= trim_frames;
334				}
335				let activity = crate::opus::activity(packet, opus.in_dtx);
336				opus.in_dtx = activity.is_dtx();
337				Ok(Decoded { samples: out, activity })
338			}
339			Backend::Pcm { bytes_per_frame } => {
340				if packet.is_empty() || !packet.len().is_multiple_of(*bytes_per_frame) {
341					return Err(Error::Misaligned {
342						got: packet.len(),
343						expected: packet.len().max(1).next_multiple_of(*bytes_per_frame),
344					});
345				}
346
347				let out = packet
348					.chunks_exact(pcm::BYTES_PER_SAMPLE)
349					.map(|sample| f32::from_le_bytes([sample[0], sample[1], sample[2], sample[3]]))
350					.collect();
351				Ok(Decoded {
352					samples: out,
353					activity: Activity::Active,
354				})
355			}
356			#[cfg(feature = "aac")]
357			Backend::Aac(aac) => {
358				// The packet is a raw AAC frame, not ADTS, so there is nothing to
359				// timestamp it with here: the container carries the timestamp and the
360				// decoder only reads the payload.
361				let packet = symphonia_core::packet::PacketRef::new(
362					0,
363					symphonia_core::units::Timestamp::ZERO,
364					symphonia_core::units::Duration::ZERO,
365					packet,
366				);
367
368				let decoded = aac
369					.inner
370					.decode_ref(&packet)
371					.map_err(|err| Error::Decode(format!("aac: {err}")))?;
372
373				let mut out = Vec::new();
374				decoded.copy_to_vec_interleaved(&mut out);
375				Ok(Decoded {
376					samples: out,
377					activity: Activity::Active,
378				})
379			}
380		}
381	}
382}
383
384impl Drop for Opus {
385	fn drop(&mut self) {
386		// SAFETY: `inner` is a live OpusDecoder that nothing else aliases.
387		unsafe { opus_decoder_destroy(self.inner) };
388	}
389}
390
391#[cfg(test)]
392mod tests {
393	use super::*;
394
395	/// Three consecutive AAC-LC frames of a 440 Hz full-scale sine, mono at
396	/// 44.1 kHz, and the AudioSpecificConfig that opens them. Generated with:
397	///
398	/// ```text
399	/// ffmpeg -f lavfi -i "sine=frequency=440:sample_rate=44100:duration=0.2" -af volume=8 -ac 1 -c:a aac -b:a 32k -f adts sine.aac
400	/// ```
401	///
402	/// then stripping the ADTS header off each frame, since the wire carries raw
403	/// AAC. These are frames 2 to 4, past the encoder's priming. lavfi's sine is
404	/// an eighth of full scale, which is what the volume filter is undoing.
405	#[cfg(feature = "aac")]
406	const AAC_DESCRIPTION: &[u8] = b"\x12\x08";
407
408	#[cfg(feature = "aac")]
409	const AAC_FRAMES: [&[u8]; 3] = [
410		b"\x01\x52\xf2\x8b\x1a\xd7\x8e\x7b\xfd\xa7\xef\xe7\xe3\x55\xd3\x4d\x2f\x55\x2e\x47\x1c\x92\x49\x11\x20\x77\x3f\xbe\x74\xdd\x99\xb3\x7b\xfb\x90\xc9\xf0\x61\x9f\xdc\x0c\x9f\x06\x19\xfd\xe1\x1f\x1f\x00\x67\xf7\x03\x87\xc0\x19\xfd\xc0\xc9\xf0\x07",
411		b"\x01\x1e\x32\x89\xe2\x9d\x6b\x33\xe7\xff\xe2\xfe\xbf\xfa\xff\xe7\x2f\x8b\xd5\xd5\xe7\x5f\x3f\x59\xeb\xf1\xcb\xba\xa5\x5e\x52\x4a\xbd\x8d\x74\x50\x8c\x08\xa8\xa0\xd4\x51\x40\xa1\x86\x5d\x06\xb4\x6c\x32\xe6\x25\x9a\x66\x75\xcd\xf9\xbf\x6f\x83\xb7\x53\x80",
412		b"\x01\x1e\x32\x8a\x22\x7d\x40\x87\x48\xdb\xdf\xff\xf9\x4f\xff\x87\xde\xef\x8b\xeb\x1e\x77\x5d\xfc\x67\x8f\x8c\x77\x8a\xd6\x29\x96\x1f\x29\xe7\x39\xd4\x53\xcf\x3c\xf3\xce\x79\xd4\x27\x9c\xf5\x65\x2a\x9b\xe9\x80\xb7\xba\xa9\xf9\x58\xc7\x3c\x58\x27\x8a\x60\xa1\x57",
413	];
414
415	#[cfg(feature = "aac")]
416	fn aac_catalog() -> hang::catalog::AudioConfig {
417		let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AAC { profile: 2 }, 44_100, 1);
418		catalog.description = Some(bytes::Bytes::from_static(AAC_DESCRIPTION));
419		catalog
420	}
421
422	#[cfg(feature = "aac")]
423	#[test]
424	fn aac_decodes_a_sine() {
425		let mut decoder = Decoder::new(&aac_catalog()).unwrap();
426		assert_eq!(decoder.sample_rate(), 44_100);
427		assert_eq!(decoder.channel_count(), 1);
428
429		let decoded: Vec<Vec<f32>> = AAC_FRAMES
430			.iter()
431			.map(|frame| decoder.decode(frame).unwrap().samples)
432			.collect();
433
434		// AAC-LC frames are 1024 samples each, whatever the packet size.
435		for pcm in &decoded {
436			assert_eq!(pcm.len(), 1024);
437		}
438
439		// The first frame is missing the previous frame's overlap, so measure the
440		// last one. ffmpeg decodes this same frame to 0.744 RMS, near the 0.707 of
441		// an ideal full-scale sine.
442		let last = decoded.last().unwrap();
443		let rms = (last.iter().map(|s| s * s).sum::<f32>() / last.len() as f32).sqrt();
444		assert!((0.65..0.8).contains(&rms), "expected a full-scale sine, got {rms} RMS");
445	}
446
447	#[cfg(feature = "aac")]
448	#[test]
449	fn aac_reports_a_truncated_packet_as_decode() {
450		let mut decoder = Decoder::new(&aac_catalog()).unwrap();
451
452		let truncated = &AAC_FRAMES[0][..16];
453		assert!(matches!(decoder.decode(truncated), Err(Error::Decode(_))));
454	}
455
456	#[cfg(feature = "aac")]
457	#[test]
458	fn aac_synthesizes_a_missing_description() {
459		// An MSF catalog carries the shape in its own fields instead.
460		let mut catalog = aac_catalog();
461		catalog.description = None;
462
463		let mut decoder = Decoder::new(&catalog).unwrap();
464		assert_eq!(decoder.sample_rate(), 44_100);
465		assert_eq!(decoder.decode(AAC_FRAMES[0]).unwrap().samples.len(), 1024);
466	}
467
468	/// A packet libopus rejects is that packet's problem, not the
469	/// configuration's. The distinction is what lets a consumer drop the frame and
470	/// keep the subscription instead of ending the stream over one bad packet.
471	#[test]
472	fn opus_reports_a_rejected_packet_as_decode() {
473		let head = moq_mux::codec::opus::Config::new(48_000, 2).encode().unwrap();
474		let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2);
475		catalog.description = Some(head);
476
477		let mut decoder = Decoder::new(&catalog).unwrap();
478
479		// Not a valid TOC byte sequence: libopus reports OPUS_INVALID_PACKET.
480		assert!(matches!(decoder.decode(&[0xFF; 3]), Err(Error::Decode(_))));
481	}
482
483	#[test]
484	fn pcm_rejects_incomplete_channel_frame() {
485		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
486		let mut decoder = Decoder::new(&catalog).unwrap();
487
488		assert!(matches!(
489			decoder.decode(&[]),
490			Err(Error::Misaligned { got: 0, expected: 8 })
491		));
492		assert!(matches!(
493			decoder.decode(&[0; 4]),
494			Err(Error::Misaligned { got: 4, expected: 8 })
495		));
496	}
497
498	#[test]
499	fn decoder_rejects_unknown_codec() {
500		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Unknown("future".into()), 48_000, 2);
501
502		assert!(matches!(Decoder::new(&catalog), Err(Error::Unsupported(_))));
503	}
504
505	#[test]
506	fn pcm_rejects_incorrect_catalog_bitrate() {
507		let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
508		catalog.bitrate = Some(1);
509
510		assert!(matches!(Decoder::new(&catalog), Err(Error::Unsupported(_))));
511	}
512}