//! MPEG-1/2/2.5 audio (MP1/MP2/MP3) decodability gate. //! //! Per ISO/IEC 11172-3 / ISO/IEC 13818-3, an MPEG-audio frame is validated by //! header sanity + framing resync, not a payload CRC (the optional 16-bit CRC in //! the header protects only the side-information and is absent unless the //! protection bit says otherwise). The gate mirrors that header-only check and //! ACCEPTS free-format (`bitrate_index == 0`) as a legal decodable mode — it //! deliberately does NOT apply the stricter free-format reject that a full //! decoder would (see the note at the `bitrate_index` check). So the gate rejects //! only the truly invalid headers: a packet that begins with the 11-bit //! MPEG-audio sync but whose version / layer / sample-rate fields (or the //! reserved bitrate index 15) are reserved/invalid is undecodable → drop it (a //! silence gap; each packet keeps its own PTS). A packet with no leading sync is //! not a frame we can validate (raw payload / continuation), so it passes through //! unchanged — never false-dropped. use super::dropgate::DropTally; use super::{CodecParser, Frame, PesPacket, pts_to_ns}; /// Decoded validity of a candidate MPEG-audio header. enum MpaVerdict { /// No 11-bit sync at the packet head — not a frame we can validate. NoSync, /// Sync present and every field is legal — decodable. Valid, /// Sync present but a field is reserved/invalid — a conformant header parser /// rejects this exactly. Invalid, } /// Header-only validity check per ISO/IEC 11172-3 / ISO/IEC 13818-3 (which /// ACCEPTS free-format, `bitrate_index == 0`) — deliberately NOT the stricter /// free-format reject a full decoder applies. A dropped MPEG-audio frame has a /// corrupt header, so no duration is computed (the fields it would come from are /// the invalid ones). fn mpa_verdict(data: &[u8]) -> MpaVerdict { if data.len() < 4 { return MpaVerdict::NoSync; } let h = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); // 11-bit sync (0x7FF at the top). if (h & 0xffe0_0000) != 0xffe0_0000 { return MpaVerdict::NoSync; } // Reject per spec: version field 01, layer field 00, bitrate_index 15, // sample-rate field 3. if (h & (3 << 19)) == (1 << 19) || (h & (3 << 17)) == 0 || (h & (0xf << 12)) == (0xf << 12) || (h & (3 << 10)) == (3 << 10) { return MpaVerdict::Invalid; } // NOTE: bitrate_index == 0 (free format) is NOT rejected. It is a legal, // decodable MPEG-audio mode (the spec permits it and a decoder derives the // frame size from the sync spacing). Dropping it would be a false positive on // a clean stream, so it passes the gate. MpaVerdict::Valid } pub struct MpegAudioParser { tally: DropTally, /// Last emitted PTS (ns), carried forward across a PES with no PTS rather than /// resetting the timeline to 0 (see the AC-3/DTS parsers) — preserves A/V sync. last_pts_ns: i64, } impl Default for MpegAudioParser { fn default() -> Self { Self::new() } } impl MpegAudioParser { pub fn new() -> Self { Self { tally: DropTally::new("mpegaudio"), last_pts_ns: 0, } } pub fn dropped_frames(&self) -> u64 { self.tally.dropped_frames() } pub fn dropped_duration_ns(&self) -> u64 { self.tally.dropped_duration_ns() } } impl CodecParser for MpegAudioParser { fn parse(&mut self, pes: &PesPacket) -> Vec { if pes.data.is_empty() { return Vec::new(); } let pts_ns = pes .pts .or(pes.dts) .map(pts_to_ns) .unwrap_or(self.last_pts_ns); self.last_pts_ns = pts_ns; let drop = self.tally.is_poisoned() || matches!(mpa_verdict(&pes.data), MpaVerdict::Invalid); if drop { let reason = if self.tally.is_poisoned() { "track-poisoned" } else { "header" }; self.tally.record_drop(pts_ns, 0, pes.data.len(), reason); return Vec::new(); } self.tally.record_kept(); vec![Frame { discontinuity: pes.discontinuity, coding: None, source: None, pts_ns, keyframe: true, data: pes.data.clone(), duration_ns: None, }] } fn flush(&mut self) -> Vec { self.tally.log_summary(); Vec::new() } fn codec_private(&self) -> Option> { None } } #[cfg(test)] mod tests { use super::*; fn make_pes(data: Vec, pts: Option) -> PesPacket { PesPacket { source: None, pid: 0x1100, pts, dts: None, data, discontinuity: false, } } /// A valid MPEG-1 Layer III header: sync 0xFFF, version MPEG-1 (11), layer /// III (01), bitrate_index 9, sample-rate 0 (44.1 kHz), no CRC. Bytes: /// 0xFF 0xFB 0x90 0x00 — the canonical MP3 frame header. fn mp3_frame(payload: usize) -> Vec { let mut f = vec![0xFF, 0xFB, 0x90, 0x00]; f.extend(std::iter::repeat(0xAA).take(payload)); f } #[test] fn valid_header_is_kept() { let mut p = MpegAudioParser::new(); let f = p.parse(&make_pes(mp3_frame(400), Some(90000))); assert_eq!(f.len(), 1); assert_eq!(f[0].pts_ns, pts_to_ns(90000)); assert_eq!(p.dropped_frames(), 0); } #[test] fn pes_without_pts_carries_last_timestamp_not_zero() { // A PES with no PTS (legal for audio, e.g. after a discontinuity) must // carry the last known timestamp forward — resetting to 0 would corrupt // A/V sync. Mirrors the adts.rs guard test. let mut p = MpegAudioParser::new(); p.parse(&make_pes(mp3_frame(400), Some(90000))); let f = p.parse(&make_pes(mp3_frame(400), None)); assert_eq!(f.len(), 1); assert_eq!( f[0].pts_ns, pts_to_ns(90000), "carried forward, not reset to 0" ); } /// Same contract as the ADTS gate: an MPEG-audio frame is dropped because /// its header is invalid, and the header is exactly where a frame duration /// (samples-per-frame for the layer, divided by the sampling rate) would /// have to come from — see `mpa_verdict`'s doc comment. So the drop's /// duration is reported as zero, not derived from rejected fields. #[test] fn dropped_frames_are_counted_but_their_duration_is_not_invented() { let mut parser = MpegAudioParser::new(); // Reserved layer field (00) — rejected per ISO/IEC 11172-3. let mut bad = mp3_frame(32); bad[1] &= !0b0000_0110; for i in 0..3 { let out = parser.parse(&make_pes(bad.clone(), Some(i * 90_000))); assert!(out.is_empty(), "an invalid MPEG-audio frame is not emitted"); } assert_eq!(parser.dropped_frames(), 3, "every drop is counted"); assert_eq!( parser.dropped_duration_ns(), 0, "the duration comes from the header that just failed validation, so \ it is reported as unmeasured rather than guessed" ); } #[test] fn reserved_version_field_is_dropped() { // version field = 01 (reserved) → rejected. byte1 = 111_01_01_1 = 0xEB // keeps the 11-bit sync (0xFF + top 3 bits 111) but sets version bits to 01. let mut p = MpegAudioParser::new(); let mut frame = mp3_frame(400); frame[1] = 0xEB; let f = p.parse(&make_pes(frame, Some(90000))); assert!(f.is_empty(), "reserved version dropped"); assert_eq!(p.dropped_frames(), 1); } #[test] fn reserved_sample_rate_is_dropped() { // Sync present but sample-rate field = 3 (reserved) → rejected per spec. // 0xFF 0xFB then byte2 with bits 11..10 = 11: 0x9C. let mut p = MpegAudioParser::new(); let mut frame = mp3_frame(400); frame[2] = 0x9C; // freq field = 3 let f = p.parse(&make_pes(frame, Some(90000))); assert!(f.is_empty(), "reserved sample rate dropped"); assert_eq!(p.dropped_frames(), 1); } #[test] fn reserved_layer_is_dropped() { // Layer field 00 (reserved). byte1 bits 2..1 = 00 → 0xF9 keeps sync // (0xFFF needs byte1 top 3 bits set) and sets layer=00. let mut p = MpegAudioParser::new(); let mut frame = mp3_frame(400); frame[1] = 0xF9; // 1111_1001: sync ok (top 3 =111), version 11, layer 00 let f = p.parse(&make_pes(frame, Some(0))); assert!(f.is_empty(), "reserved layer dropped"); assert_eq!(p.dropped_frames(), 1); } #[test] fn bad_bitrate_index_15_is_dropped() { let mut p = MpegAudioParser::new(); let mut frame = mp3_frame(400); frame[2] = 0xF0; // bitrate_index = 1111 assert!(p.parse(&make_pes(frame, Some(0))).is_empty()); assert_eq!(p.dropped_frames(), 1); } #[test] fn free_format_bitrate_zero_is_kept() { // Free format (bitrate_index == 0) is legal and decodable — it must NOT // be dropped (that would be a false positive on a clean stream). let mut p = MpegAudioParser::new(); let mut frame = mp3_frame(400); frame[2] = 0x00; // bitrate_index = 0000 (free format); sync/layer/rate ok let f = p.parse(&make_pes(frame, Some(0))); assert_eq!(f.len(), 1, "free-format frame kept"); assert_eq!(p.dropped_frames(), 0); } #[test] fn non_sync_packet_passes_through() { // No 11-bit sync → not a validatable frame → keep (conservative). let mut p = MpegAudioParser::new(); let f = p.parse(&make_pes(vec![0x00, 0x11, 0x22, 0x33, 0x44], Some(0))); assert_eq!(f.len(), 1); assert_eq!(p.dropped_frames(), 0); } #[test] fn drop_preserves_sync_via_own_pts() { let mut p = MpegAudioParser::new(); let mut bad = mp3_frame(400); bad[2] = 0x9C; // reserved sample rate assert!(p.parse(&make_pes(bad, Some(90000))).is_empty()); let f = p.parse(&make_pes(mp3_frame(400), Some(96000))); assert_eq!(f.len(), 1); assert_eq!( f[0].pts_ns, pts_to_ns(96000), "next frame keeps its own PTS" ); } }