Skip to main content

moq_mux/codec/opus/
mod.rs

1//! Opus.
2//!
3//! RFC 7845 OpusHead parse and encode lives in [`Config`]. [`Import`]
4//! publishes raw Opus frames (no Ogg framing) to a moq broadcast.
5
6mod import;
7pub mod mapping;
8
9pub use import::*;
10pub use mapping::Mapping;
11
12use bytes::{Buf, Bytes};
13
14const OPUS_HEAD: u64 = u64::from_be_bytes(*b"OpusHead");
15
16/// Opus parsing errors.
17#[derive(Debug, Clone, thiserror::Error)]
18#[non_exhaustive]
19pub enum Error {
20	/// The OpusHead packet was shorter than the 19-byte minimum (RFC 7845 §5.1).
21	#[error("OpusHead must be at least 19 bytes")]
22	HeadTooShort,
23
24	/// The packet did not start with the `OpusHead` magic signature.
25	#[error("invalid OpusHead signature")]
26	InvalidSignature,
27
28	/// The OpusHead major version (the upper four bits) is not one this parser
29	/// understands, so none of its fields can be trusted (RFC 7845 §5.1).
30	#[error("unsupported OpusHead version {0}")]
31	UnsupportedVersion(u8),
32
33	/// The channel count is zero, or more than the channel mapping family allows:
34	/// family 0 covers mono/stereo and family 1 up to eight channels.
35	#[error("channel mapping family does not allow {0} channels")]
36	UnsupportedChannelCount(u32),
37
38	/// A nonzero channel mapping family without its complete mapping table.
39	#[error("OpusHead channel mapping table is truncated")]
40	MappingTableTooShort,
41
42	/// The channel mapping table names no streams, more coupled streams than
43	/// streams, or a channel index past the decoded streams.
44	#[error("invalid OpusHead channel mapping table")]
45	InvalidMappingTable,
46
47	/// A channel mapping family the operation cannot carry: family 0 given to
48	/// [`Mapping::new`], which has no table; a [`Config::encode`] whose
49	/// `mapping_family` is not its `mapping`'s; or a container with no room for
50	/// the table.
51	#[error("unsupported channel mapping family {0}")]
52	UnsupportedMappingFamily(u8),
53}
54
55pub type Result<T> = std::result::Result<T, Error>;
56
57/// Typed Opus configuration mirroring the parsed fields of an OpusHead packet.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59#[non_exhaustive]
60pub struct Config {
61	/// Original input sample rate in Hz, or 0 when unknown.
62	///
63	/// Metadata only: Opus always runs on a 48 kHz clock, and this need not be a
64	/// rate Opus can decode at (44.1 kHz is common).
65	pub sample_rate: u32,
66	/// Number of output channels.
67	pub channel_count: u32,
68	/// Number of decoded 48 kHz samples to discard at stream start.
69	pub pre_skip: u16,
70	/// Gain to apply to the decoded output, in Q7.8 dB.
71	pub output_gain: i16,
72	/// Channel mapping family; 0 is mono/stereo with no mapping table.
73	///
74	/// Always `mapping`'s family, or 0 without one.
75	pub mapping_family: u8,
76	/// The channel mapping table, or `None` for family 0 (mono/stereo, one stream).
77	pub mapping: Option<Mapping>,
78}
79
80impl Config {
81	/// Build a mono/stereo Opus config with no pre-skip or gain.
82	pub fn new(sample_rate: u32, channel_count: u32) -> Self {
83		Self {
84			sample_rate,
85			channel_count,
86			pre_skip: 0,
87			output_gain: 0,
88			mapping_family: 0,
89			mapping: None,
90		}
91	}
92
93	/// Set the number of decoded 48 kHz samples to discard at stream start.
94	pub fn with_pre_skip(mut self, pre_skip: u16) -> Self {
95		self.pre_skip = pre_skip;
96		self
97	}
98
99	/// Parse an OpusHead buffer (RFC 7845 §5.1).
100	///
101	/// Accepts any minor version and refuses a new major version. Reads the
102	/// channel mapping, refusing a channel count the family forbids or an
103	/// inconsistent mapping table. Bytes after the header are left in `buf`.
104	pub fn parse<T: Buf>(buf: &mut T) -> Result<Self> {
105		if buf.remaining() < 19 {
106			return Err(Error::HeadTooShort);
107		}
108		let signature = buf.get_u64();
109		if signature != OPUS_HEAD {
110			return Err(Error::InvalidSignature);
111		}
112
113		let version = buf.get_u8();
114		if version > 15 {
115			return Err(Error::UnsupportedVersion(version));
116		}
117		let channel_count = buf.get_u8() as u32;
118		let pre_skip = buf.get_u16_le();
119		let sample_rate = buf.get_u32_le();
120		let output_gain = buf.get_i16_le();
121		let mapping_family = buf.get_u8();
122
123		let mapping = match mapping_family {
124			0 if (1..=2).contains(&channel_count) => None,
125			0 => return Err(Error::UnsupportedChannelCount(channel_count)),
126			family => Some(Mapping::parse(buf, family, channel_count as u8)?),
127		};
128
129		Ok(Self {
130			sample_rate,
131			channel_count,
132			pre_skip,
133			output_gain,
134			mapping_family,
135			mapping,
136		})
137	}
138
139	/// Encode an OpusHead packet (RFC 7845 §5.1), followed by the channel mapping
140	/// table when there is one.
141	///
142	/// Errors with [`Error::UnsupportedChannelCount`] when `channel_count` is not
143	/// 1 or 2 without a `mapping` (family 0 is only defined for mono/stereo), or
144	/// is not the mapping's own channel count, and with
145	/// [`Error::UnsupportedMappingFamily`] when `mapping_family` is not the
146	/// mapping's family.
147	pub fn encode(&self) -> Result<Bytes> {
148		if self.mapping_family != self.mapping.map_or(0, |mapping| mapping.family()) {
149			return Err(Error::UnsupportedMappingFamily(self.mapping_family));
150		}
151		let valid = match &self.mapping {
152			None => (1..=2).contains(&self.channel_count),
153			Some(mapping) => self.channel_count == mapping.table().len() as u32,
154		};
155		if !valid {
156			return Err(Error::UnsupportedChannelCount(self.channel_count));
157		}
158
159		let mut head = Vec::with_capacity(21 + self.channel_count as usize);
160		head.extend_from_slice(b"OpusHead");
161		head.push(1); // version
162		head.push(self.channel_count as u8);
163		head.extend_from_slice(&self.pre_skip.to_le_bytes());
164		head.extend_from_slice(&self.sample_rate.to_le_bytes());
165		head.extend_from_slice(&self.output_gain.to_le_bytes());
166		match &self.mapping {
167			None => head.push(0),
168			Some(mapping) => {
169				head.extend_from_slice(&[mapping.family(), mapping.streams(), mapping.coupled()]);
170				head.extend_from_slice(mapping.table());
171			}
172		}
173		Ok(Bytes::from(head))
174	}
175}
176
177/// Number of 48 kHz samples in an Opus packet, read from its TOC byte (RFC 6716 §3.1).
178///
179/// MPEG-TS aggregates several Opus packets into one PES, so the importer advances each
180/// packet's timestamp by this. Opus timing is always reckoned at 48 kHz regardless of the
181/// encoder's internal bandwidth. Returns `None` for an empty packet or a code-3 packet
182/// missing its frame-count byte.
183pub(crate) fn packet_samples(packet: &[u8]) -> Option<u32> {
184	let toc = *packet.first()?;
185	let frames = match toc & 0b11 {
186		0 => 1,
187		1 | 2 => 2,
188		// Code 3: the frame count is the low 6 bits of the following byte.
189		_ => (packet.get(1)? & 0b0011_1111) as u32,
190	};
191	Some(config_samples(toc >> 3) * frames)
192}
193
194/// 48 kHz samples per frame for an Opus TOC config index (0..=31), per RFC 6716 Table 1.
195fn config_samples(config: u8) -> u32 {
196	match config {
197		// SILK NB/MB/WB: 10, 20, 40, 60 ms.
198		0 | 4 | 8 => 480,
199		1 | 5 | 9 => 960,
200		2 | 6 | 10 => 1920,
201		3 | 7 | 11 => 2880,
202		// Hybrid SWB/FB: 10, 20 ms.
203		12 | 14 => 480,
204		13 | 15 => 960,
205		// CELT NB/WB/SWB/FB: 2.5, 5, 10, 20 ms.
206		16 | 20 | 24 | 28 => 120,
207		17 | 21 | 25 | 29 => 240,
208		18 | 22 | 26 | 30 => 480,
209		// 19, 23, 27, 31 are the 20 ms CELT configs.
210		_ => 960,
211	}
212}
213
214#[cfg(test)]
215mod tests {
216	use super::*;
217
218	#[test]
219	fn packet_samples_reads_toc() {
220		// config 16 (CELT NB 2.5 ms = 120 samples), code 0 (1 frame).
221		assert_eq!(packet_samples(&[16 << 3]), Some(120));
222		// config 3 (SILK NB 60 ms = 2880), code 0.
223		assert_eq!(packet_samples(&[3 << 3]), Some(2880));
224		// config 1 (SILK NB 20 ms = 960), code 1 (2 frames) -> 1920.
225		assert_eq!(packet_samples(&[(1 << 3) | 1]), Some(1920));
226		// config 1, code 3 with 4 frames -> 3840.
227		assert_eq!(packet_samples(&[(1 << 3) | 3, 4]), Some(3840));
228		assert_eq!(packet_samples(&[]), None);
229	}
230
231	#[test]
232	fn parses_valid_opus_head() {
233		let cfg = Config::new(48_000, 2).with_pre_skip(312);
234		let encoded = cfg.encode().unwrap();
235		assert_eq!(encoded.len(), 19);
236		let parsed = Config::parse(&mut encoded.as_ref()).unwrap();
237		assert_eq!(parsed.sample_rate, 48_000);
238		assert_eq!(parsed.channel_count, 2);
239		assert_eq!(parsed.pre_skip, 312);
240		assert_eq!(parsed, cfg);
241	}
242
243	#[test]
244	fn parse_rejects_invalid_signature() {
245		let mut bytes = Config::new(48_000, 1).encode().unwrap().to_vec();
246		bytes[0] = b'X';
247		assert!(Config::parse(&mut bytes.as_slice()).is_err());
248	}
249
250	#[test]
251	fn encode_rejects_multichannel() {
252		let err = Config::new(48_000, 6).encode().unwrap_err();
253		assert!(matches!(err, Error::UnsupportedChannelCount(6)));
254	}
255
256	/// A 19-byte head with the given version, channels, gain, and family.
257	fn head(version: u8, channels: u8, gain: i16, family: u8) -> Vec<u8> {
258		let mut head = b"OpusHead".to_vec();
259		head.push(version);
260		head.push(channels);
261		head.extend_from_slice(&312u16.to_le_bytes());
262		head.extend_from_slice(&44_100u32.to_le_bytes());
263		head.extend_from_slice(&gain.to_le_bytes());
264		head.push(family);
265		head
266	}
267
268	#[test]
269	fn parses_gain_and_input_rate() {
270		let bytes = head(1, 2, -1536, 0);
271		let parsed = Config::parse(&mut bytes.as_slice()).unwrap();
272		assert_eq!(parsed.sample_rate, 44_100);
273		assert_eq!(parsed.pre_skip, 312);
274		assert_eq!(parsed.output_gain, -1536);
275		assert_eq!(parsed.mapping_family, 0);
276
277		// The gain survives a round trip.
278		let encoded = parsed.encode().unwrap();
279		assert_eq!(encoded.as_ref(), bytes.as_slice());
280	}
281
282	#[test]
283	fn parse_accepts_minor_versions_only() {
284		for version in [0, 1, 15] {
285			assert!(Config::parse(&mut head(version, 2, 0, 0).as_slice()).is_ok());
286		}
287		assert!(matches!(
288			Config::parse(&mut head(16, 2, 0, 0).as_slice()),
289			Err(Error::UnsupportedVersion(16))
290		));
291	}
292
293	#[test]
294	fn parses_a_mapping_table() {
295		// 5.1 in family 1: four streams, two coupled, Vorbis order.
296		let mut bytes = head(1, 6, 0, 1);
297		bytes.extend_from_slice(&[4, 2, 0, 4, 1, 2, 3, 5]);
298		let trailing = [bytes.as_slice(), b"trailing"].concat();
299		let mut buf = trailing.as_slice();
300		let parsed = Config::parse(&mut buf).unwrap();
301		assert_eq!(buf, b"trailing");
302		assert_eq!(parsed.channel_count, 6);
303		assert_eq!(parsed.mapping_family, 1);
304
305		let mapping = parsed.mapping.unwrap();
306		assert_eq!(mapping.family(), 1);
307		assert_eq!(mapping.streams(), 4);
308		assert_eq!(mapping.coupled(), 2);
309		assert_eq!(mapping.table(), &[0, 4, 1, 2, 3, 5]);
310
311		// The table survives a re-encode.
312		assert_eq!(parsed.encode().unwrap(), bytes);
313	}
314
315	#[test]
316	fn encodes_the_vorbis_mappings() {
317		for channels in 1..=8u8 {
318			let mut config = Config::new(48_000, channels as u32);
319			config.mapping_family = 1;
320			config.mapping = Some(Mapping::vorbis(channels).unwrap());
321			let head = config.encode().unwrap();
322			assert_eq!(head.len(), 21 + channels as usize, "{channels} channels");
323			assert_eq!(
324				Config::parse(&mut head.as_ref()).unwrap(),
325				config,
326				"{channels} channels"
327			);
328		}
329
330		// 5.1 is the table libopus and ffmpeg write.
331		let five_one = Mapping::vorbis(6).unwrap();
332		assert_eq!((five_one.streams(), five_one.coupled()), (4, 2));
333		assert_eq!(five_one.table(), &[0, 4, 1, 2, 3, 5]);
334
335		assert!(matches!(Mapping::vorbis(0), Err(Error::UnsupportedChannelCount(0))));
336		assert!(matches!(Mapping::vorbis(9), Err(Error::UnsupportedChannelCount(9))));
337
338		// The channel count must agree with the table.
339		let mut config = Config::new(48_000, 5);
340		config.mapping_family = 1;
341		config.mapping = Some(five_one);
342		assert!(matches!(config.encode(), Err(Error::UnsupportedChannelCount(5))));
343
344		// So must the family, both ways.
345		let mut config = Config::new(48_000, 6);
346		config.mapping = Some(five_one);
347		assert!(matches!(config.encode(), Err(Error::UnsupportedMappingFamily(0))));
348		let mut config = Config::new(48_000, 2);
349		config.mapping_family = 1;
350		assert!(matches!(config.encode(), Err(Error::UnsupportedMappingFamily(1))));
351	}
352
353	#[test]
354	fn parse_rejects_channel_counts_the_family_does_not_allow() {
355		for (channels, family) in [(0, 0), (3, 0), (0, 1), (9, 1), (0, 255)] {
356			let mut bytes = head(1, channels, 0, family);
357			bytes.extend_from_slice(&[1, 0]);
358			bytes.extend(std::iter::repeat_n(0, channels as usize));
359			assert!(
360				matches!(
361					Config::parse(&mut bytes.as_slice()),
362					Err(Error::UnsupportedChannelCount(_))
363				),
364				"{channels} channels in family {family}"
365			);
366		}
367	}
368
369	#[test]
370	fn parse_rejects_a_bad_mapping_table() {
371		// Family 1 promising a table that is not there, or only half of it.
372		assert!(matches!(
373			Config::parse(&mut head(1, 2, 0, 1).as_slice()),
374			Err(Error::MappingTableTooShort)
375		));
376		let mut short = head(1, 2, 0, 1);
377		short.extend_from_slice(&[1, 1, 0]);
378		assert!(matches!(
379			Config::parse(&mut short.as_slice()),
380			Err(Error::MappingTableTooShort)
381		));
382
383		for table in [
384			// No streams.
385			[0, 0, 0, 1],
386			// More coupled streams than streams.
387			[1, 2, 0, 1],
388			// A channel index past the two decoded channels.
389			[1, 1, 0, 2],
390		] {
391			let mut bytes = head(1, 2, 0, 1);
392			bytes.extend_from_slice(&table);
393			assert!(
394				matches!(Config::parse(&mut bytes.as_slice()), Err(Error::InvalidMappingTable)),
395				"{table:?}"
396			);
397		}
398
399		// 255 is a silent channel, not an index.
400		let mut silent = head(1, 2, 0, 1);
401		silent.extend_from_slice(&[1, 0, 0, 255]);
402		assert_eq!(
403			Config::parse(&mut silent.as_slice()).unwrap().mapping.unwrap().table(),
404			&[0, 255]
405		);
406	}
407}