Skip to main content

moq_mux/codec/
mp3.rs

1//! MP3 (MPEG-1/2/2.5 Audio Layer III).
2//!
3//! Audio carried verbatim: each frame is published whole. The header is parsed
4//! only for the catalog config (sample rate, channels); the audio is never
5//! decoded and there is no out-of-band configuration record. [`Import`] publishes
6//! raw MP3 frames to a moq broadcast.
7
8use crate::catalog::hang::CatalogExt;
9use crate::container::Frame;
10use moq_net::Timestamp;
11
12/// MP3 parsing errors.
13#[derive(Debug, Clone, thiserror::Error)]
14#[non_exhaustive]
15pub enum Error {
16	/// The buffer was shorter than the 4-byte MPEG audio frame header.
17	#[error("MP3 frame header must be at least 4 bytes")]
18	HeaderTooShort,
19
20	/// The 11-bit frame sync (`0xFFE`) was missing.
21	#[error("missing MP3 frame sync")]
22	MissingSync,
23
24	/// The MPEG version field was the reserved value `01`.
25	#[error("reserved MPEG version")]
26	ReservedVersion,
27
28	/// The layer field was not Layer III, so this is not an MP3 frame (Layer I/II
29	/// are MP1/MP2, the reserved value is invalid).
30	#[error("not an MPEG Layer III (MP3) frame")]
31	NotLayer3,
32
33	/// The sample-rate index was the reserved value `11`.
34	#[error("reserved MP3 sample rate")]
35	ReservedSampleRate,
36}
37
38pub type Result<T> = std::result::Result<T, Error>;
39
40/// Typed MP3 configuration parsed from an MPEG audio frame header.
41pub struct Config {
42	/// Sampling frequency in Hz.
43	pub sample_rate: u32,
44	/// Channel count (1 for the mono channel mode, 2 otherwise).
45	pub channel_count: u32,
46}
47
48impl Config {
49	/// Parse the catalog config from the start of an MPEG Layer III frame.
50	///
51	/// Reads the 4-byte frame header (ISO/IEC 11172-3 ยง2.4.1.2): verifies the
52	/// frame sync and that the layer is III, then derives the sample rate from
53	/// the version + sample-rate index and the channel count from the channel
54	/// mode. The buffer is not advanced; the frame is published whole.
55	pub fn parse(data: &[u8]) -> Result<Self> {
56		if data.len() < 4 {
57			return Err(Error::HeaderTooShort);
58		}
59
60		// 11-bit frame sync: all of byte 0 plus the top 3 bits of byte 1.
61		if data[0] != 0xFF || (data[1] & 0xE0) != 0xE0 {
62			return Err(Error::MissingSync);
63		}
64
65		let version = (data[1] >> 3) & 0x03;
66		let layer = (data[1] >> 1) & 0x03;
67		// Layer is encoded inverted: 0b01 == Layer III.
68		if layer != 0b01 {
69			return Err(Error::NotLayer3);
70		}
71
72		let sr_index = ((data[2] >> 2) & 0x03) as usize;
73		if sr_index == 0b11 {
74			return Err(Error::ReservedSampleRate);
75		}
76
77		let sample_rate = match version {
78			0b11 => [44100, 48000, 32000][sr_index], // MPEG-1
79			0b10 => [22050, 24000, 16000][sr_index], // MPEG-2
80			0b00 => [11025, 12000, 8000][sr_index],  // MPEG-2.5
81			_ => return Err(Error::ReservedVersion),
82		};
83
84		// Channel mode 0b11 is single channel (mono); the rest are two-channel.
85		let channel_count = if (data[3] >> 6) & 0x03 == 0b11 { 1 } else { 2 };
86
87		Ok(Self {
88			sample_rate,
89			channel_count,
90		})
91	}
92}
93
94/// MP3 importer.
95///
96/// Publishes raw MP3 frames to a single moq track. Build it with [`new`](Self::new),
97/// passing the track producer and the [`catalog::Reserved`](crate::catalog::Reserved)
98/// it reserves its rendition from.
99///
100/// Every MP3 frame is independently decodable, so [`decode`](Self::decode) marks only the first frame
101/// of each group a keyframe (the rest extend it): frames accumulate into the current group until the
102/// caller [`cut`](Self::cut)s or [`seek`](Self::seek)s. The [`import::Track`](crate::import::Track)
103/// facade cuts after every frame by default (one group per frame); a caller driving its own
104/// boundaries cuts less often. MP3 carries its config in band, so the rendition has no out-of-band
105/// description.
106pub struct Import<E: CatalogExt = ()> {
107	track: crate::container::Producer<crate::catalog::hang::Container>,
108	rendition: crate::catalog::AudioTrack<E>,
109}
110
111impl<E: CatalogExt> Import<E> {
112	/// Publish on an existing track producer with a resolved catalog config.
113	///
114	/// Build one from a frame header with [`config`], or from an out-of-band [`Config`] via `into()`.
115	/// The rendition publishes immediately.
116	pub fn new(
117		track: moq_net::track::Producer,
118		reserved: crate::catalog::Reserved<E>,
119		mut config: hang::catalog::AudioConfig,
120	) -> crate::Result<Self> {
121		tracing::debug!(name = ?track.name(), ?config, "starting track");
122		// Advertise this rendition's timeline before publishing (the generic set() no longer does).
123		config.timeline = Some(reserved.producer().timeline(track.name())?.section());
124		let mut rendition = reserved.audio(track.name());
125		rendition.set(config);
126		Ok(Self {
127			track: reserved
128				.producer()
129				.media_producer(track, crate::catalog::hang::Container::Legacy)?,
130			rendition,
131		})
132	}
133
134	/// A watch-only handle to this track's subscriber demand.
135	pub fn demand(&self) -> moq_net::track::Demand {
136		self.track.track().demand()
137	}
138
139	/// Finish the track, flushing the current group.
140	pub fn finish(&mut self) -> crate::Result<()> {
141		self.track.finish()?;
142		self.estimate();
143		Ok(())
144	}
145
146	/// Abort the track with `err` instead of finishing it cleanly, so subscribers
147	/// see the real cause rather than [`moq_net::Error::Dropped`]. Consumes this importer.
148	pub fn abort(self, err: moq_net::Error) {
149		self.track.abort(err);
150	}
151
152	/// Publish what the track measured (bitrate, jitter) into the catalog rendition, filling only
153	/// the fields its config didn't supply.
154	fn estimate(&mut self) {
155		self.rendition.estimate(self.track.estimate());
156	}
157
158	/// Cut the current group at `end` without finishing the track.
159	pub fn cut(&mut self, end: Option<moq_net::Timestamp>) -> crate::Result<()> {
160		self.track.cut(end)?;
161		self.estimate();
162		Ok(())
163	}
164
165	/// Close the current group and open the next one at `sequence`.
166	pub fn seek(&mut self, sequence: u64) -> crate::Result<()> {
167		self.track.seek(sequence)?;
168		self.estimate();
169		Ok(())
170	}
171
172	/// Publish one MP3 frame, stamping `pts` or a wall clock when absent.
173	///
174	/// MP3 is independently decodable, so the frame is marked a keyframe only when it starts a group
175	/// (see [`Producer::needs_keyframe`](crate::container::Producer::needs_keyframe)); otherwise it
176	/// extends the current group. The caller bounds groups via [`cut`](Self::cut) / [`seek`](Self::seek).
177	pub fn decode<B: moq_net::IntoBytes>(&mut self, frame: B, pts: Option<Timestamp>) -> crate::Result<()> {
178		let timestamp = self.rendition.timestamp(pts)?;
179		// Only the first frame of each group is a keyframe, so the group spans until the caller cuts
180		// instead of opening one group (one QUIC stream) per packet.
181		let keyframe = self.track.needs_keyframe();
182		self.track.write(Frame {
183			timestamp,
184			payload: frame.into_bytes(),
185			keyframe,
186			duration: None,
187		})?;
188		self.estimate();
189		Ok(())
190	}
191}
192
193/// Build a catalog config from an MP3 frame header. Errors on a malformed or empty buffer.
194pub fn config(init: &[u8]) -> crate::Result<hang::catalog::AudioConfig> {
195	Ok(Config::parse(init)?.into())
196}
197
198impl From<Config> for hang::catalog::AudioConfig {
199	/// Build a catalog config from a config resolved out of band (e.g. gstreamer caps).
200	fn from(config: Config) -> Self {
201		let mut audio =
202			hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Mp3, config.sample_rate, config.channel_count);
203		audio.container = hang::catalog::Container::Legacy;
204		audio
205	}
206}
207
208#[cfg(test)]
209mod tests {
210	use super::*;
211
212	#[test]
213	fn parses_mpeg1_stereo() {
214		// MPEG-1 Layer III, 128 kbps, 44.1 kHz, joint stereo.
215		let header = [0xFF, 0xFB, 0x90, 0x44];
216		let cfg = Config::parse(&header).unwrap();
217		assert_eq!(cfg.sample_rate, 44100);
218		assert_eq!(cfg.channel_count, 2);
219	}
220
221	#[test]
222	fn parses_mpeg1_mono() {
223		// Same header but channel mode 0b11 (mono) in the top bits of byte 3.
224		let header = [0xFF, 0xFB, 0x90, 0xC4];
225		let cfg = Config::parse(&header).unwrap();
226		assert_eq!(cfg.channel_count, 1);
227	}
228
229	#[test]
230	fn parses_mpeg2_sample_rate() {
231		// MPEG-2 (version 0b10), Layer III, sample-rate index 0 -> 22.05 kHz.
232		let header = [0xFF, 0xF3, 0x90, 0x44];
233		let cfg = Config::parse(&header).unwrap();
234		assert_eq!(cfg.sample_rate, 22050);
235	}
236
237	#[test]
238	fn rejects_layer2() {
239		// Layer II is 0b10, i.e. an MP2 (not MP3) frame.
240		let header = [0xFF, 0xFD, 0x90, 0x44];
241		assert!(matches!(Config::parse(&header), Err(Error::NotLayer3)));
242	}
243
244	#[test]
245	fn rejects_missing_sync() {
246		assert!(matches!(
247			Config::parse(&[0x00, 0x00, 0x00, 0x00]),
248			Err(Error::MissingSync)
249		));
250	}
251
252	#[test]
253	fn rejects_short() {
254		assert!(matches!(Config::parse(&[0xFF, 0xFB]), Err(Error::HeaderTooShort)));
255	}
256}