#1402: import MP1 (A_MPEG/L1) audio from Matroska
Problem: the Matroska demuxer mapped A_MPEG/L2 and A_MPEG/L3 only, so a Layer I track was reported as an unsupported codec even though the MP1/MP2 decoder (#809) reads Layer I. Fix: add Codec::Mp1 for A_MPEG/L1 and route it through the same packet duration and decoder path as Mp2. Test remuxes synthetic Layer I frames into Matroska with ffmpeg and compares our decode (max error 3e-5). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
71438c627b
commit
3516207de7
5 files changed
+100
-5
No files matched your search
@@ -83,7 +83,7 @@ fn audio_starts(file: &MkvFile, bytes: &crate::Src, ti: usize, rate: i64) -> Vec
|
||||
let out_rate = u32::try_from(rate).unwrap_or(0);
|
||||
let frame = match &t.codec {
|
||||
Codec::Aac { asc } => fixed_packet_samples(FixedFrames::Aac(asc), &[], out_rate),
|
||||
Codec::Mp3 | Codec::Mp2 => fixed_packet_samples(FixedFrames::MpegAudio, &first(), out_rate),
|
||||
Codec::Mp3 | Codec::Mp2 | Codec::Mp1 => fixed_packet_samples(FixedFrames::MpegAudio, &first(), out_rate),
|
||||
Codec::Ac3 => fixed_packet_samples(FixedFrames::Ac3, &first(), out_rate),
|
||||
Codec::Eac3 => fixed_packet_samples(FixedFrames::Eac3, &first(), out_rate),
|
||||
_ => None,
|
||||
@@ -317,7 +317,7 @@ fn codec_label(c: &Codec) -> String {
|
||||
Codec::Flac { .. } => "FLAC".into(),
|
||||
Codec::Pcm { bits, float, .. } => format!("PCM {bits}-bit{}", if *float { " float" } else { "" }),
|
||||
Codec::Mp3 => "MP3".into(),
|
||||
Codec::Mp2 => "MPEG Audio".into(),
|
||||
Codec::Mp2 | Codec::Mp1 => "MPEG Audio".into(),
|
||||
Codec::Other(id) if id == "V_MPEG2" => "MPEG-2 Video".into(),
|
||||
Codec::Other(id) if id == "V_MPEG1" => "MPEG-1 Video".into(),
|
||||
other => other.name().to_string(),
|
||||
@@ -486,6 +486,7 @@ impl MkvSource {
|
||||
Codec::Opus { head } => PacketDecoder::opus(filmcraft_opus::OpusHead::parse(head).map_err(|e| CodecError::Unsupported(format!("Opus: {e}")))?),
|
||||
Codec::Mp3 => PacketDecoder::new(CODEC_TYPE_MP3, rate, None),
|
||||
Codec::Mp2 => PacketDecoder::mpeg_audio(2, rate),
|
||||
Codec::Mp1 => PacketDecoder::mpeg_audio(1, rate),
|
||||
Codec::Ac3 | Codec::Eac3 => PacketDecoder::ac3(),
|
||||
// symphonia wants the STREAMINFO block body: skip `fLaC` + the 4-byte block header.
|
||||
Codec::Flac { private } => PacketDecoder::new(CODEC_TYPE_FLAC, rate, private.get(8..42).map(<[u8]>::to_vec)),
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! MPEG audio Layer I (MP1) in Matroska (`A_MPEG/L1`, #1402) against ffmpeg's decode. No
|
||||
//! installed encoder writes Layer I, so the frames are synthetic (valid random Layer I frames,
|
||||
//! as in `crates/mpa/tests/oracle.rs`) and ffmpeg only remuxes them into Matroska and decodes.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
|
||||
struct Bits(Vec<u8>, usize);
|
||||
impl Bits {
|
||||
fn put(&mut self, v: u32, n: u32) {
|
||||
for i in (0..n).rev() {
|
||||
if self.1.is_multiple_of(8) {
|
||||
self.0.push(0);
|
||||
}
|
||||
if (v >> i) & 1 == 1 {
|
||||
let last = self.0.len() - 1;
|
||||
self.0[last] |= 0x80 >> (self.1 % 8);
|
||||
}
|
||||
self.1 += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mono Layer I frames, MPEG-1, 48 kHz, 192 kbit/s (192-byte frames): allocations on the first 8
|
||||
/// subbands only, so every frame fits.
|
||||
fn layer1_stream(frames: usize) -> Vec<u8> {
|
||||
let mut seed = 0x2545_F491u32;
|
||||
let mut rnd = |n: u32| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
seed % n
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for _ in 0..frames {
|
||||
let mut b = Bits(Vec::new(), 0);
|
||||
for byte in [0xFFu32, 0xFF, 6 << 4 | 1 << 2, 3 << 6] {
|
||||
b.put(byte, 8);
|
||||
}
|
||||
let alloc: Vec<u32> = (0..32).map(|sb| if sb < 8 { 1 + rnd(8) } else { 0 }).collect();
|
||||
for &a in &alloc {
|
||||
b.put(a, 4);
|
||||
}
|
||||
for &a in &alloc {
|
||||
if a != 0 {
|
||||
b.put(10 + rnd(30), 6);
|
||||
}
|
||||
}
|
||||
for _ in 0..12 {
|
||||
for &a in &alloc {
|
||||
if a != 0 {
|
||||
b.put(rnd((1 << (a + 1)) - 1), a + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut f = b.0;
|
||||
f.resize(192, 0);
|
||||
out.extend(f);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mp1_in_matroska_matches_ffmpeg() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
let raw = dir().join("synthetic_layer1.mp1");
|
||||
std::fs::create_dir_all(dir()).unwrap();
|
||||
std::fs::write(&raw, layer1_stream(250)).unwrap();
|
||||
let Some(mkv) = fixture(&ff, "synthetic_layer1.mkv", &["-f", "mp3", "-i", raw.to_str().unwrap(), "-c:a", "copy"]) else {
|
||||
eprintln!("SKIPPED: this ffmpeg can't remux Layer I into Matroska");
|
||||
return;
|
||||
};
|
||||
let m = filmcraft_matroska::open(bytes(&mkv)).unwrap();
|
||||
assert!(matches!(m.tracks.first().map(|t| &t.codec), Some(filmcraft_matroska::Codec::Mp1)), "A_MPEG/L1 maps to Codec::Mp1");
|
||||
let src = filmcraft_codecs::open_bytes("synthetic_layer1.mkv", bytes(&mkv)).unwrap();
|
||||
let a = src.info().audio().cloned().unwrap();
|
||||
assert_eq!((a.sample_rate, a.channels), (48_000, 1));
|
||||
let want = ffmpeg_audio_f32(&ff, &mkv, &[]);
|
||||
let n = want.len().min(250 * 384) - 800;
|
||||
let got = src.audio(0, n, 48_000).unwrap();
|
||||
let peak = want.iter().fold(0f32, |m, x| m.max(x.abs()));
|
||||
// ffmpeg's CLI may or may not drop the decoder delay of a stream copy; search a few lags
|
||||
let (worst, lag) = (-2i64..=2)
|
||||
.map(|lag| ((2..n - 2).map(|i| (want[(i as i64 + lag) as usize] - got.channels[0][i]).abs()).fold(0f32, f32::max), lag))
|
||||
.fold((f32::INFINITY, 0), |m, x| if x.0 < m.0 { x } else { m });
|
||||
println!("mkv Layer I: max error {worst} at lag {lag}, peak {peak}");
|
||||
assert!(peak > 0.01 && worst <= 2e-4, "peak {peak}, max error {worst} at lag {lag}");
|
||||
}
|
||||
@@ -73,7 +73,7 @@ while let Some(p) = d.next_packet()? { // file order, all tracks
|
||||
| `A_VORBIS` | `Vorbis` | three headers split from Xiph-laced CodecPrivate |
|
||||
| `A_FLAC` | `Flac` | `fLaC` + metadata blocks |
|
||||
| `A_PCM/INT/LIT`, `A_PCM/INT/BIG`, `A_PCM/FLOAT/IEEE` | `Pcm` | bit depth from Audio |
|
||||
| `A_AC3`, `A_EAC3`, `A_MPEG/L3`, `A_MPEG/L2` | `Ac3`, `Eac3`, `Mp3`, `Mp2` | |
|
||||
| `A_AC3`, `A_EAC3`, `A_MPEG/L3`, `A_MPEG/L2`, `A_MPEG/L1` | `Ac3`, `Eac3`, `Mp3`, `Mp2`, `Mp1` | |
|
||||
| `S_TEXT/UTF8` | `SubRip` | |
|
||||
| `S_TEXT/WEBVTT`, `D_WEBVTT/*` (WebM) | `WebVtt` | WebM blocks keep `id\nsettings\npayload` |
|
||||
| `S_TEXT/ASS`, `S_TEXT/SSA` | `Ass` | script header |
|
||||
|
||||
@@ -45,6 +45,8 @@ pub enum Codec {
|
||||
Mp3,
|
||||
/// `A_MPEG/L2`.
|
||||
Mp2,
|
||||
/// `A_MPEG/L1`.
|
||||
Mp1,
|
||||
/// `S_TEXT/UTF8` (SubRip-style plain text).
|
||||
SubRip,
|
||||
/// `S_TEXT/WEBVTT`, or WebM's `D_WEBVTT/{SUBTITLES,CAPTIONS,DESCRIPTIONS,METADATA}`.
|
||||
@@ -86,6 +88,7 @@ impl Codec {
|
||||
Codec::Eac3 => "eac3".into(),
|
||||
Codec::Mp3 => "mp3".into(),
|
||||
Codec::Mp2 => "mp2".into(),
|
||||
Codec::Mp1 => "mp1".into(),
|
||||
Codec::SubRip => "subrip".into(),
|
||||
Codec::WebVtt => "webvtt".into(),
|
||||
Codec::Ass { ssa, .. } => if *ssa { "ssa" } else { "ass" }.into(),
|
||||
@@ -173,6 +176,7 @@ pub(crate) fn map_codec(id: &str, private: &[u8], audio: Option<&crate::AudioInf
|
||||
"A_EAC3" => Codec::Eac3,
|
||||
"A_MPEG/L3" => Codec::Mp3,
|
||||
"A_MPEG/L2" => Codec::Mp2,
|
||||
"A_MPEG/L1" => Codec::Mp1,
|
||||
"S_TEXT/UTF8" => Codec::SubRip,
|
||||
"S_TEXT/WEBVTT" | "D_WEBVTT/SUBTITLES" | "D_WEBVTT/CAPTIONS" | "D_WEBVTT/DESCRIPTIONS" | "D_WEBVTT/METADATA" => Codec::WebVtt,
|
||||
"S_TEXT/ASS" | "S_ASS" => Codec::Ass { header: p, ssa: false },
|
||||
|
||||
+3
-2
@@ -178,12 +178,12 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
|
||||
- **Have:** E-AC-3 (Dolby Digital Plus) in MP4, MOV, Matroska and MPEG-TS (#647): independent
|
||||
substream, 1–6 blocks per syncframe, spectral extension. MP1 / MP2 (MPEG audio Layer I / II):
|
||||
own decoder (#809) in raw files, MP4 and MOV (#800), MPEG-TS / PS and Matroska, including the
|
||||
own decoder (#809) in raw files, MP4 and MOV (#800), MPEG-TS / PS and Matroska (`A_MPEG/L1` and `L2`, #1402), including the
|
||||
bit rate / mode combinations ISO/IEC 11172-3 disallows but ffmpeg writes (its mono default is
|
||||
384 kbit/s). OpenEXR stills and image
|
||||
sequences (#828): scan-line, uncompressed / RLE / ZIPS / ZIP, half / float / uint, RGB(A) or
|
||||
luminance, decoded to linear float frames and bit-exact against ffmpeg.
|
||||
- **Missing:** free-format MPEG audio, MP1 in Matroska (`A_MPEG/L1`), E-AC-3 adaptive hybrid
|
||||
- **Missing:** free-format MPEG audio, E-AC-3 adaptive hybrid
|
||||
transform, enhanced coupling and the extra channels of 7.1 dependent substreams (played as the
|
||||
5.1 core), AVI (#598; listed as an extension but no
|
||||
demuxer), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, FLAC in MP4 (#603), camera RAW (R3D
|
||||
@@ -306,3 +306,4 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | major | Created from ROADMAP.md's "Where we are lacking" (2026-10-05); re-ranked against Premiere 26.5.2 and the 188 open issues; added `.prproj`, 10-bit / 4:2:2 camera decode, launch crashes and missing PR CI, localization, delivery formats |
|
||||
| 2026-10-12 | minor | G10: MP1 (`A_MPEG/L1`) in Matroska imports (#1402) |
|
||||
Reference in new issue
Block a user