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, Timestamp};
10
11/// MP3 parsing errors.
12#[derive(Debug, Clone, thiserror::Error)]
13#[non_exhaustive]
14pub enum Error {
15	/// The buffer was shorter than the 4-byte MPEG audio frame header.
16	#[error("MP3 frame header must be at least 4 bytes")]
17	HeaderTooShort,
18
19	/// The 11-bit frame sync (`0xFFE`) was missing.
20	#[error("missing MP3 frame sync")]
21	MissingSync,
22
23	/// The MPEG version field was the reserved value `01`.
24	#[error("reserved MPEG version")]
25	ReservedVersion,
26
27	/// The layer field was not Layer III, so this is not an MP3 frame (Layer I/II
28	/// are MP1/MP2, the reserved value is invalid).
29	#[error("not an MPEG Layer III (MP3) frame")]
30	NotLayer3,
31
32	/// The sample-rate index was the reserved value `11`.
33	#[error("reserved MP3 sample rate")]
34	ReservedSampleRate,
35}
36
37pub type Result<T> = std::result::Result<T, Error>;
38
39/// Typed MP3 configuration parsed from an MPEG audio frame header.
40pub struct Config {
41	/// Sampling frequency in Hz.
42	pub sample_rate: u32,
43	/// Channel count (1 for the mono channel mode, 2 otherwise).
44	pub channel_count: u32,
45}
46
47impl Config {
48	/// Parse the catalog config from the start of an MPEG Layer III frame.
49	///
50	/// Reads the 4-byte frame header (ISO/IEC 11172-3 ยง2.4.1.2): verifies the
51	/// frame sync and that the layer is III, then derives the sample rate from
52	/// the version + sample-rate index and the channel count from the channel
53	/// mode. The buffer is not advanced; the frame is published whole.
54	pub fn parse(data: &[u8]) -> Result<Self> {
55		if data.len() < 4 {
56			return Err(Error::HeaderTooShort);
57		}
58
59		// 11-bit frame sync: all of byte 0 plus the top 3 bits of byte 1.
60		if data[0] != 0xFF || (data[1] & 0xE0) != 0xE0 {
61			return Err(Error::MissingSync);
62		}
63
64		let version = (data[1] >> 3) & 0x03;
65		let layer = (data[1] >> 1) & 0x03;
66		// Layer is encoded inverted: 0b01 == Layer III.
67		if layer != 0b01 {
68			return Err(Error::NotLayer3);
69		}
70
71		let sr_index = ((data[2] >> 2) & 0x03) as usize;
72		if sr_index == 0b11 {
73			return Err(Error::ReservedSampleRate);
74		}
75
76		let sample_rate = match version {
77			0b11 => [44100, 48000, 32000][sr_index], // MPEG-1
78			0b10 => [22050, 24000, 16000][sr_index], // MPEG-2
79			0b00 => [11025, 12000, 8000][sr_index],  // MPEG-2.5
80			_ => return Err(Error::ReservedVersion),
81		};
82
83		// Channel mode 0b11 is single channel (mono); the rest are two-channel.
84		let channel_count = if (data[3] >> 6) & 0x03 == 0b11 { 1 } else { 2 };
85
86		Ok(Self {
87			sample_rate,
88			channel_count,
89		})
90	}
91}
92
93/// MP3 importer.
94///
95/// Publishes raw MP3 frames to a single moq track. Build it with [`new`](Self::new),
96/// passing the track producer and the [`catalog::Producer`](crate::catalog::Producer)
97/// it publishes its rendition into.
98///
99/// Each frame handed to [`decode`](Self::decode) is published in its own group so the
100/// relay can forward it immediately. MP3 carries its config in band, so the rendition
101/// has no out-of-band description.
102pub struct Import<E: CatalogExt = ()> {
103	track: crate::container::Producer<crate::catalog::hang::Container>,
104	rendition: crate::catalog::AudioTrack<E>,
105}
106
107impl<E: CatalogExt> Import<E> {
108	/// Publish on an existing track producer, registering the rendition in `catalog`.
109	pub fn new(
110		track: moq_net::TrackProducer,
111		catalog: crate::catalog::Producer<E>,
112		config: Config,
113	) -> crate::Result<Self> {
114		let mut audio =
115			hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Mp3, config.sample_rate, config.channel_count);
116		audio.container = hang::catalog::Container::Legacy;
117
118		tracing::debug!(name = ?track.name(), config = ?audio, "starting track");
119
120		let mut rendition = catalog.audio_track(track.name());
121		rendition.set(audio);
122
123		Ok(Self {
124			track: crate::container::Producer::new(track, crate::catalog::hang::Container::Legacy),
125			rendition,
126		})
127	}
128
129	/// A watch-only handle to this track's subscriber demand.
130	pub fn demand(&self) -> moq_net::TrackDemand {
131		self.track.track().demand()
132	}
133
134	/// Finish the track, flushing the current group.
135	pub fn finish(&mut self) -> crate::Result<()> {
136		self.track.finish()?;
137		Ok(())
138	}
139
140	/// Close the current group and open the next one at `sequence`.
141	pub fn seek(&mut self, sequence: u64) -> crate::Result<()> {
142		self.track.seek(sequence)?;
143		Ok(())
144	}
145
146	/// Publish one MP3 frame as its own group, stamping `pts` or a wall clock when absent.
147	pub fn decode(&mut self, frame: &[u8], pts: Option<Timestamp>) -> crate::Result<()> {
148		let timestamp = self.rendition.timestamp(pts)?;
149		self.track.write(Frame {
150			timestamp,
151			payload: bytes::Bytes::copy_from_slice(frame),
152			keyframe: true,
153			duration: None,
154		})?;
155		self.track.finish_group()?;
156		Ok(())
157	}
158}
159
160#[cfg(test)]
161mod tests {
162	use super::*;
163
164	#[test]
165	fn parses_mpeg1_stereo() {
166		// MPEG-1 Layer III, 128 kbps, 44.1 kHz, joint stereo.
167		let header = [0xFF, 0xFB, 0x90, 0x44];
168		let cfg = Config::parse(&header).unwrap();
169		assert_eq!(cfg.sample_rate, 44100);
170		assert_eq!(cfg.channel_count, 2);
171	}
172
173	#[test]
174	fn parses_mpeg1_mono() {
175		// Same header but channel mode 0b11 (mono) in the top bits of byte 3.
176		let header = [0xFF, 0xFB, 0x90, 0xC4];
177		let cfg = Config::parse(&header).unwrap();
178		assert_eq!(cfg.channel_count, 1);
179	}
180
181	#[test]
182	fn parses_mpeg2_sample_rate() {
183		// MPEG-2 (version 0b10), Layer III, sample-rate index 0 -> 22.05 kHz.
184		let header = [0xFF, 0xF3, 0x90, 0x44];
185		let cfg = Config::parse(&header).unwrap();
186		assert_eq!(cfg.sample_rate, 22050);
187	}
188
189	#[test]
190	fn rejects_layer2() {
191		// Layer II is 0b10, i.e. an MP2 (not MP3) frame.
192		let header = [0xFF, 0xFD, 0x90, 0x44];
193		assert!(matches!(Config::parse(&header), Err(Error::NotLayer3)));
194	}
195
196	#[test]
197	fn rejects_missing_sync() {
198		assert!(matches!(
199			Config::parse(&[0x00, 0x00, 0x00, 0x00]),
200			Err(Error::MissingSync)
201		));
202	}
203
204	#[test]
205	fn rejects_short() {
206		assert!(matches!(Config::parse(&[0xFF, 0xFB]), Err(Error::HeaderTooShort)));
207	}
208}