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;
7
8pub use import::*;
9
10use bytes::{Buf, Bytes};
11
12const OPUS_HEAD: u64 = u64::from_be_bytes(*b"OpusHead");
13
14/// Opus parsing errors.
15#[derive(Debug, Clone, thiserror::Error)]
16#[non_exhaustive]
17pub enum Error {
18	/// The OpusHead packet was shorter than the 19-byte minimum (RFC 7845 §5.1).
19	#[error("OpusHead must be at least 19 bytes")]
20	HeadTooShort,
21
22	/// The packet did not start with the `OpusHead` magic signature.
23	#[error("invalid OpusHead signature")]
24	InvalidSignature,
25
26	/// [`Config::encode`] was asked to emit an OpusHead for a channel count other
27	/// than mono or stereo; channel mapping family 0 only covers 1 or 2 channels.
28	#[error("channel mapping family 0 only supports mono/stereo (got {0} channels)")]
29	UnsupportedChannelCount(u32),
30}
31
32pub type Result<T> = std::result::Result<T, Error>;
33
34/// Typed Opus configuration mirroring the parsed fields of an OpusHead packet.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36#[non_exhaustive]
37pub struct Config {
38	/// Original input sample rate in Hz.
39	pub sample_rate: u32,
40	/// Number of encoded channels.
41	pub channel_count: u32,
42	/// Number of decoded 48 kHz samples to discard at stream start.
43	pub pre_skip: u16,
44}
45
46impl Config {
47	/// Build a mono/stereo Opus config with no pre-skip.
48	pub fn new(sample_rate: u32, channel_count: u32) -> Self {
49		Self {
50			sample_rate,
51			channel_count,
52			pre_skip: 0,
53		}
54	}
55
56	/// Set the number of decoded 48 kHz samples to discard at stream start.
57	pub fn with_pre_skip(mut self, pre_skip: u16) -> Self {
58		self.pre_skip = pre_skip;
59		self
60	}
61
62	/// Parse an OpusHead buffer (RFC 7845 §5.1).
63	///
64	/// Verifies the magic signature; reads channel count, pre-skip, and sample
65	/// rate; ignores gain and channel mapping. Any trailing bytes are consumed.
66	pub fn parse<T: Buf>(buf: &mut T) -> Result<Self> {
67		if buf.remaining() < 19 {
68			return Err(Error::HeadTooShort);
69		}
70		let signature = buf.get_u64();
71		if signature != OPUS_HEAD {
72			return Err(Error::InvalidSignature);
73		}
74
75		buf.advance(1); // Skip version
76		let channel_count = buf.get_u8() as u32;
77		let pre_skip = buf.get_u16_le();
78		let sample_rate = buf.get_u32_le();
79
80		// Skip gain, channel mapping until if/when we support them.
81		if buf.remaining() > 0 {
82			buf.advance(buf.remaining());
83		}
84
85		Ok(Self {
86			sample_rate,
87			channel_count,
88			pre_skip,
89		})
90	}
91
92	/// Encode the minimal OpusHead packet (19 bytes; channel mapping family
93	/// 0 and zero gain).
94	///
95	/// Errors with [`Error::UnsupportedChannelCount`] unless `channel_count` is 1
96	/// or 2, since mapping family 0 is only defined for mono/stereo per RFC 7845 §5.1.
97	/// Multi-channel streams need family 1 with a channel mapping table, which
98	/// this helper does not emit.
99	pub fn encode(&self) -> Result<Bytes> {
100		if !(1..=2).contains(&self.channel_count) {
101			return Err(Error::UnsupportedChannelCount(self.channel_count));
102		}
103		let mut head = Vec::with_capacity(19);
104		head.extend_from_slice(b"OpusHead");
105		head.push(1); // version
106		head.push(self.channel_count as u8);
107		head.extend_from_slice(&self.pre_skip.to_le_bytes());
108		head.extend_from_slice(&self.sample_rate.to_le_bytes());
109		head.extend_from_slice(&0i16.to_le_bytes()); // output gain
110		head.push(0); // channel mapping family (0 = mono/stereo)
111		Ok(Bytes::from(head))
112	}
113}
114
115/// Number of 48 kHz samples in an Opus packet, read from its TOC byte (RFC 6716 §3.1).
116///
117/// MPEG-TS aggregates several Opus packets into one PES, so the importer advances each
118/// packet's timestamp by this. Opus timing is always reckoned at 48 kHz regardless of the
119/// encoder's internal bandwidth. Returns `None` for an empty packet or a code-3 packet
120/// missing its frame-count byte.
121pub(crate) fn packet_samples(packet: &[u8]) -> Option<u32> {
122	let toc = *packet.first()?;
123	let frames = match toc & 0b11 {
124		0 => 1,
125		1 | 2 => 2,
126		// Code 3: the frame count is the low 6 bits of the following byte.
127		_ => (packet.get(1)? & 0b0011_1111) as u32,
128	};
129	Some(config_samples(toc >> 3) * frames)
130}
131
132/// 48 kHz samples per frame for an Opus TOC config index (0..=31), per RFC 6716 Table 1.
133fn config_samples(config: u8) -> u32 {
134	match config {
135		// SILK NB/MB/WB: 10, 20, 40, 60 ms.
136		0 | 4 | 8 => 480,
137		1 | 5 | 9 => 960,
138		2 | 6 | 10 => 1920,
139		3 | 7 | 11 => 2880,
140		// Hybrid SWB/FB: 10, 20 ms.
141		12 | 14 => 480,
142		13 | 15 => 960,
143		// CELT NB/WB/SWB/FB: 2.5, 5, 10, 20 ms.
144		16 | 20 | 24 | 28 => 120,
145		17 | 21 | 25 | 29 => 240,
146		18 | 22 | 26 | 30 => 480,
147		// 19, 23, 27, 31 are the 20 ms CELT configs.
148		_ => 960,
149	}
150}
151
152#[cfg(test)]
153mod tests {
154	use super::*;
155
156	#[test]
157	fn packet_samples_reads_toc() {
158		// config 16 (CELT NB 2.5 ms = 120 samples), code 0 (1 frame).
159		assert_eq!(packet_samples(&[16 << 3]), Some(120));
160		// config 3 (SILK NB 60 ms = 2880), code 0.
161		assert_eq!(packet_samples(&[3 << 3]), Some(2880));
162		// config 1 (SILK NB 20 ms = 960), code 1 (2 frames) -> 1920.
163		assert_eq!(packet_samples(&[(1 << 3) | 1]), Some(1920));
164		// config 1, code 3 with 4 frames -> 3840.
165		assert_eq!(packet_samples(&[(1 << 3) | 3, 4]), Some(3840));
166		assert_eq!(packet_samples(&[]), None);
167	}
168
169	#[test]
170	fn parses_valid_opus_head() {
171		let cfg = Config::new(48_000, 2).with_pre_skip(312);
172		let encoded = cfg.encode().unwrap();
173		assert_eq!(encoded.len(), 19);
174		let parsed = Config::parse(&mut encoded.as_ref()).unwrap();
175		assert_eq!(parsed.sample_rate, 48_000);
176		assert_eq!(parsed.channel_count, 2);
177		assert_eq!(parsed.pre_skip, 312);
178		assert_eq!(parsed, cfg);
179	}
180
181	#[test]
182	fn parse_rejects_invalid_signature() {
183		let mut bytes = Config::new(48_000, 1).encode().unwrap().to_vec();
184		bytes[0] = b'X';
185		assert!(Config::parse(&mut bytes.as_slice()).is_err());
186	}
187
188	#[test]
189	fn encode_rejects_multichannel() {
190		let err = Config::new(48_000, 6).encode().unwrap_err();
191		assert!(matches!(err, Error::UnsupportedChannelCount(6)));
192	}
193}