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::{OPUS_OK, OpusDecoder, opus_decode_float, opus_decoder_create, opus_decoder_destroy};
9
10use crate::opus;
11use crate::pcm;
12use crate::{Error, Format};
13
14/// Opus packets cap at 120 ms (RFC 6716 ยง2.1.4).
15const MAX_FRAME_MS: usize = 120;
16
17/// Decoder configuration: the PCM layout to emit, plus the subscription's
18/// latency budget.
19///
20/// The mirror of [`encode::Config`](crate::encode::Config): it describes the
21/// output, since the codec's own shape is read from the catalog.
22///
23/// `#[non_exhaustive]`: build via [`Config::new`] (or `default()`) and set the
24/// optional fields, so future knobs don't break callers.
25#[derive(Clone, Debug, Default)]
26#[non_exhaustive]
27pub struct Config {
28	/// How to pack samples in each emitted frame.
29	pub format: Format,
30	/// Sample rate to emit at. `None` uses the codec's native rate from the
31	/// catalog; anything else resamples.
32	pub sample_rate: Option<u32>,
33	/// Channel count to emit. `None` uses the codec's native count; anything
34	/// else remixes mono and stereo at the decode boundary.
35	pub channels: Option<u32>,
36	/// Upper bound on buffering before skipping a stalled group.
37	///
38	/// Forwarded to [`moq_mux::container::Consumer::with_latency`]: if a group is
39	/// stuck and a newer group is more than this far ahead, the consumer skips.
40	/// `None` keeps the moq-mux default of zero, which skips aggressively. Set it
41	/// to the playout buffer you can tolerate (typically tens to a few hundred ms)
42	/// for the best congestion-vs-quality trade-off. The `_max` suffix is a
43	/// reminder that we never *add* latency here: the consumer skips only when
44	/// newer data is already this far ahead. A companion `latency_min` for
45	/// jitter-buffer padding will land in a follow-up.
46	pub latency_max: Option<Duration>,
47}
48
49impl Config {
50	/// A default config: the codec's native rate and channel count, interleaved
51	/// `f32`, and the moq-mux default latency.
52	pub fn new() -> Self {
53		Self::default()
54	}
55}
56
57/// Decodes codec packets into interleaved `f32` PCM.
58///
59/// The bring-your-own-payload layer under [`Consumer`](super::Consumer): use it
60/// when the packets don't come from a plain track subscription.
61pub struct Decoder {
62	backend: Backend,
63	sample_rate: u32,
64	channel_count: u32,
65}
66
67enum Backend {
68	Opus(Opus),
69	Pcm { bytes_per_frame: usize },
70}
71
72struct Opus {
73	inner: *mut OpusDecoder,
74	pre_skip_remaining: usize,
75	max_frame_size: usize,
76}
77
78// SAFETY: see Encoder.
79unsafe impl Send for Opus {}
80
81impl Decoder {
82	/// Build a decoder from a catalog [`AudioConfig`](hang::catalog::AudioConfig).
83	///
84	/// Parses the OpusHead `description` if present; falls back to the catalog's
85	/// declared sample rate / channel count. PCM uses those catalog fields
86	/// directly and requires an absent `description`.
87	pub fn new(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
88		match &catalog.codec {
89			hang::catalog::AudioCodec::Opus => Self::new_opus(catalog),
90			hang::catalog::AudioCodec::Pcm => Self::new_pcm(catalog),
91			codec => Err(Error::Unsupported(format!("unsupported audio codec: {codec}"))),
92		}
93	}
94
95	fn new_opus(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
96		let (sample_rate, channel_count, pre_skip) = if let Some(desc) = &catalog.description {
97			let mut buf = desc.as_ref();
98			match moq_mux::codec::opus::Config::parse(&mut buf) {
99				Ok(head) => (head.sample_rate, head.channel_count, head.pre_skip),
100				Err(_) => (catalog.sample_rate, catalog.channel_count, 0),
101			}
102		} else {
103			(catalog.sample_rate, catalog.channel_count, 0)
104		};
105
106		opus::validate_rate(sample_rate)?;
107		let channels = opus::validate_channels(channel_count)?;
108
109		let mut err = 0i32;
110		// SAFETY: out-pointer is valid; inner is checked for null below.
111		let inner = unsafe { opus_decoder_create(sample_rate as i32, channels, &mut err) };
112		if err != OPUS_OK || inner.is_null() {
113			return Err(opus::error(err, "opus_decoder_create"));
114		}
115
116		let max_frame_size = (sample_rate as usize * MAX_FRAME_MS) / 1000;
117		let pre_skip_remaining = (pre_skip as usize * sample_rate as usize) / 48_000;
118
119		Ok(Self {
120			backend: Backend::Opus(Opus {
121				inner,
122				pre_skip_remaining,
123				max_frame_size,
124			}),
125			sample_rate,
126			channel_count,
127		})
128	}
129
130	fn new_pcm(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
131		if catalog.sample_rate == 0 {
132			return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
133		}
134		if catalog.channel_count == 0 {
135			return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
136		}
137		if catalog.description.is_some() {
138			return Err(Error::Unsupported("pcm catalog description must be absent".into()));
139		}
140		let bitrate = pcm::bitrate(catalog.sample_rate, catalog.channel_count)?;
141		if catalog.bitrate.is_some_and(|declared| declared != bitrate) {
142			return Err(Error::Unsupported(format!(
143				"pcm catalog bitrate must be {bitrate} bits per second"
144			)));
145		}
146		let bytes_per_frame = pcm::frame_bytes(1, catalog.channel_count)?;
147
148		Ok(Self {
149			backend: Backend::Pcm { bytes_per_frame },
150			sample_rate: catalog.sample_rate,
151			channel_count: catalog.channel_count,
152		})
153	}
154
155	/// The rate the codec decodes at, read from the catalog.
156	pub fn sample_rate(&self) -> u32 {
157		self.sample_rate
158	}
159
160	/// The channel count the codec decodes at, read from the catalog.
161	pub fn channel_count(&self) -> u32 {
162		self.channel_count
163	}
164
165	/// Decode one packet into interleaved `f32` PCM.
166	pub fn decode(&mut self, packet: &[u8]) -> Result<Vec<f32>, Error> {
167		match &mut self.backend {
168			Backend::Opus(opus) => {
169				let mut out = vec![0.0f32; opus.max_frame_size * self.channel_count as usize];
170				// SAFETY: `inner` owns a live OpusDecoder; packet/out slices are
171				// bounded by the lengths we pass.
172				let samples = unsafe {
173					opus_decode_float(
174						&mut *opus.inner,
175						packet.as_ptr(),
176						packet.len() as i32,
177						out.as_mut_ptr(),
178						opus.max_frame_size as i32,
179						0,
180					)
181				};
182				if samples < 0 {
183					return Err(crate::opus::error(samples, "opus_decode_float"));
184				}
185				out.truncate(samples as usize * self.channel_count as usize);
186				let trim_frames = opus.pre_skip_remaining.min(samples as usize);
187				if trim_frames > 0 {
188					let trim_samples = trim_frames * self.channel_count as usize;
189					out.copy_within(trim_samples.., 0);
190					out.truncate(out.len() - trim_samples);
191					opus.pre_skip_remaining -= trim_frames;
192				}
193				Ok(out)
194			}
195			Backend::Pcm { bytes_per_frame } => {
196				if packet.is_empty() || !packet.len().is_multiple_of(*bytes_per_frame) {
197					return Err(Error::Misaligned {
198						got: packet.len(),
199						expected: packet.len().max(1).next_multiple_of(*bytes_per_frame),
200					});
201				}
202
203				Ok(packet
204					.chunks_exact(pcm::BYTES_PER_SAMPLE)
205					.map(|sample| f32::from_le_bytes([sample[0], sample[1], sample[2], sample[3]]))
206					.collect())
207			}
208		}
209	}
210}
211
212impl Drop for Opus {
213	fn drop(&mut self) {
214		// SAFETY: `inner` is a live OpusDecoder that nothing else aliases.
215		unsafe { opus_decoder_destroy(self.inner) };
216	}
217}
218
219#[cfg(test)]
220mod tests {
221	use super::*;
222
223	#[test]
224	fn pcm_rejects_incomplete_channel_frame() {
225		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
226		let mut decoder = Decoder::new(&catalog).unwrap();
227
228		assert!(matches!(
229			decoder.decode(&[]),
230			Err(Error::Misaligned { got: 0, expected: 8 })
231		));
232		assert!(matches!(
233			decoder.decode(&[0; 4]),
234			Err(Error::Misaligned { got: 4, expected: 8 })
235		));
236	}
237
238	#[test]
239	fn decoder_rejects_unknown_codec() {
240		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Unknown("future".into()), 48_000, 2);
241
242		assert!(matches!(Decoder::new(&catalog), Err(Error::Unsupported(_))));
243	}
244
245	#[test]
246	fn pcm_rejects_incorrect_catalog_bitrate() {
247		let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
248		catalog.bitrate = Some(1);
249
250		assert!(matches!(Decoder::new(&catalog), Err(Error::Unsupported(_))));
251	}
252}