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