v0.11.7: TrueHD parser rewrite — 12-bit length, AC-3 skip, cross-PES buffering

This commit is contained in:
Matt Jackson
2026-04-19 01:36:56 +00:00
parent 0165f18fad
commit 1bea8eb650
3 changed files with 142 additions and 45 deletions
+12 -2
View File
@@ -1,9 +1,19 @@
# Changelog # Changelog
## 0.11.7 (2026-04-19)
### TrueHD parser rewrite
- **12-bit length mask** — access unit length is lower 12 bits of first 2 bytes, not full 16. Upper 4 bits are parity nibble. Wrong mask caused misaligned frame splits.
- **AC-3 frame skipping** — BD-TS TrueHD PES contains interleaved AC-3 frames (same PID). Parser now detects AC-3 sync word (0x0B77) and skips those frames.
- **Cross-PES buffering** — access units that span PES packet boundaries are correctly reassembled.
- **Per-unit timestamps** — each access unit gets incrementing PTS (1/1200th second apart) instead of all units in one PES sharing the same timestamp.
- **Major sync detection** — keyframe flag set when access unit contains MLP major sync (0xF8726FBA).
- Result: zero TrueHD decode errors on UHD and BD (was ~19 per 30 seconds).
## 0.11.6 (2026-04-18) ## 0.11.6 (2026-04-18)
### TrueHD fix ### TrueHD fix (incomplete)
- **Strip BD-TS access unit header** — TrueHD parser was passing the 4-byte BD-TS access unit header into MKV. MKV expects raw MLP frames. The header bytes corrupted TrueHD sync, causing decode failures in all players. Affects BD and UHD discs with TrueHD/Atmos audio. - Initial attempt at TrueHD header stripping — wrong approach, superseded by 0.11.7.
## 0.11.5 (2026-04-18) ## 0.11.5 (2026-04-18)
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "libfreemkv" name = "libfreemkv"
version = "0.11.6" version = "0.11.7"
edition = "2021" edition = "2021"
rust-version = "1.86" rust-version = "1.86"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
+128 -41
View File
@@ -1,17 +1,24 @@
//! Dolby TrueHD / Atmos elementary stream parser. //! Dolby TrueHD / Atmos elementary stream parser.
//! //!
//! TrueHD access units are 2560-byte fixed-size units (40 per major sync). //! BD-TS TrueHD PES packets contain interleaved AC-3 + TrueHD access units.
//! Each unit starts with a 4-byte header: [length_hi, length_lo, timestamp_hi, timestamp_lo]. //! Access units span PES boundaries — must buffer and reassemble.
//! Major sync: 0xF8726FBA appears within a unit. //!
//! Buffers across PES boundaries for complete unit delivery. //! TrueHD access unit header (4 bytes):
//! [0..1] upper 4 bits = parity, lower 12 bits = length in 2-byte words
//! [2..3] timing value
//! [4..] substream data (major sync 0xF8726FBA may appear at offset 4)
//!
//! AC-3 frames (interleaved, same PID): start with sync word 0x0B77.
//! We skip AC-3 frames and only emit TrueHD access units.
use super::{pts_to_ns, CodecParser, Frame, PesPacket}; use super::{pts_to_ns, CodecParser, Frame, PesPacket};
/// TrueHD access unit size (fixed). /// Duration of one TrueHD access unit in nanoseconds (1/1200 second).
const TRUEHD_UNIT_SIZE: usize = 2560; const AU_DURATION_NS: i64 = 833_333;
pub struct TrueHdParser { pub struct TrueHdParser {
buf: Vec<u8>, buf: Vec<u8>,
next_pts_ns: i64,
} }
impl Default for TrueHdParser { impl Default for TrueHdParser {
@@ -23,9 +30,61 @@ impl Default for TrueHdParser {
impl TrueHdParser { impl TrueHdParser {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
buf: Vec::with_capacity(TRUEHD_UNIT_SIZE * 4), buf: Vec::with_capacity(32768),
next_pts_ns: 0,
} }
} }
/// Skip an AC-3 frame starting at the current buffer position.
/// Returns number of bytes consumed, or 0 if not enough data.
fn skip_ac3_frame(&self) -> usize {
if self.buf.len() < 6 {
return 0;
}
// AC-3 frame size from frmsizcod + fscod
// Byte 4: [fscod:2][frmsizecod:6]
let fscod = (self.buf[4] >> 6) & 0x03;
let frmsizecod = (self.buf[4] & 0x3F) as usize;
// Frame size in 16-bit words per fscod (simplified table for common rates)
let frame_words = match fscod {
0 => {
// 48 kHz
static SIZES: [usize; 38] = [
64, 64, 80, 80, 96, 96, 112, 112, 128, 128,
160, 160, 192, 192, 224, 224, 256, 256, 320, 320,
384, 384, 448, 448, 512, 512, 640, 640, 768, 768,
896, 896, 1024, 1024, 1152, 1152, 1280, 1280,
];
SIZES.get(frmsizecod).copied().unwrap_or(0)
}
1 => {
// 44.1 kHz
static SIZES: [usize; 38] = [
69, 70, 87, 88, 104, 105, 121, 122, 139, 140,
174, 175, 208, 209, 243, 244, 278, 279, 348, 349,
417, 418, 487, 488, 557, 558, 696, 697, 835, 836,
975, 976, 1114, 1115, 1253, 1254, 1393, 1394,
];
SIZES.get(frmsizecod).copied().unwrap_or(0)
}
2 => {
// 32 kHz
static SIZES: [usize; 38] = [
96, 96, 120, 120, 144, 144, 168, 168, 192, 192,
240, 240, 288, 288, 336, 336, 384, 384, 480, 480,
576, 576, 672, 672, 768, 768, 960, 960, 1152, 1152,
1344, 1344, 1536, 1536, 1728, 1728, 1920, 1920,
];
SIZES.get(frmsizecod).copied().unwrap_or(0)
}
_ => 0,
};
let frame_bytes = frame_words * 2;
if frame_bytes == 0 || self.buf.len() < frame_bytes {
return 0;
}
frame_bytes
}
} }
impl CodecParser for TrueHdParser { impl CodecParser for TrueHdParser {
@@ -33,45 +92,59 @@ impl CodecParser for TrueHdParser {
if pes.data.is_empty() { if pes.data.is_empty() {
return Vec::new(); return Vec::new();
} }
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
if let Some(pts) = pes.pts {
self.next_pts_ns = pts_to_ns(pts);
}
self.buf.extend_from_slice(&pes.data); self.buf.extend_from_slice(&pes.data);
let mut frames = Vec::new(); let mut frames = Vec::new();
// BD-TS TrueHD access units: loop {
// [0..1] length in 16-bit words (includes the 4-byte header) if self.buf.len() < 4 {
// [2..3] timestamp (ignored — we use PES PTS) break;
// [4..] MLP payload (raw TrueHD data for MKV) }
//
// MKV stores raw MLP frames without the 4-byte access unit header.
const AU_HEADER: usize = 4;
while self.buf.len() >= AU_HEADER { // AC-3 frame (interleaved): starts with sync word 0x0B77
let unit_words = ((self.buf[0] as usize) << 8) | self.buf[1] as usize; if self.buf[0] == 0x0B && self.buf[1] == 0x77 {
let skip = self.skip_ac3_frame();
if skip == 0 {
break; // incomplete AC-3 frame, wait for more data
}
self.buf.drain(..skip);
continue;
}
// TrueHD access unit: lower 12 bits of first 2 bytes = length in words
let unit_words = (((self.buf[0] as usize) << 8) | self.buf[1] as usize) & 0xFFF;
if unit_words == 0 { if unit_words == 0 {
// Padding — skip 2 bytes
self.buf.drain(..2); self.buf.drain(..2);
continue; continue;
} }
let unit_bytes = unit_words * 2; let unit_bytes = unit_words * 2;
if unit_bytes > 65536 || unit_bytes < AU_HEADER { if unit_bytes > 32768 {
// Invalid — skip 2 bytes to resync // Likely misaligned — try to resync by scanning for AC-3 sync or
// a valid TrueHD length
self.buf.drain(..2); self.buf.drain(..2);
continue; continue;
} }
if self.buf.len() < unit_bytes { if self.buf.len() < unit_bytes {
// Incomplete unit wait for more data break; // incomplete access unit, wait for more data
break;
} }
// Strip the 4-byte access unit header, pass only MLP payload let is_major_sync = unit_bytes >= 8
&& (u32::from_be_bytes([self.buf[4], self.buf[5], self.buf[6], self.buf[7]])
& 0xFFFF_FFFE)
== 0xF872_6FBA;
frames.push(Frame { frames.push(Frame {
pts_ns, pts_ns: self.next_pts_ns,
keyframe: true, keyframe: is_major_sync,
data: self.buf[AU_HEADER..unit_bytes].to_vec(), data: self.buf[..unit_bytes].to_vec(),
}); });
self.buf.drain(..unit_bytes); self.buf.drain(..unit_bytes);
self.next_pts_ns += AU_DURATION_NS;
} }
frames frames
@@ -99,15 +172,17 @@ mod tests {
fn make_truehd_unit(size_bytes: usize) -> Vec<u8> { fn make_truehd_unit(size_bytes: usize) -> Vec<u8> {
let words = size_bytes / 2; let words = size_bytes / 2;
let mut data = vec![0u8; size_bytes]; let mut data = vec![0u8; size_bytes];
// 4-byte header: [length_hi, length_lo, ts_hi, ts_lo] data[0] = ((words >> 8) & 0x0F) as u8;
data[0] = (words >> 8) as u8;
data[1] = (words & 0xFF) as u8; data[1] = (words & 0xFF) as u8;
data[2] = 0; // timestamp data
data[3] = 0;
// Fill payload with non-zero to distinguish from header
for b in data[4..].iter_mut() {
*b = 0xAA;
} }
fn make_ac3_frame() -> Vec<u8> {
// Minimal AC-3 frame: sync 0x0B77, fscod=0 (48kHz), frmsizecod=0 (64 words = 128 bytes)
let mut data = vec![0u8; 128];
data[0] = 0x0B;
data[1] = 0x77;
data[4] = 0x00; // fscod=0, frmsizecod=0
data data
} }
@@ -119,15 +194,13 @@ mod tests {
} }
#[test] #[test]
fn parse_single_unit_strips_header() { fn parse_single_unit() {
let mut parser = TrueHdParser::new(); let mut parser = TrueHdParser::new();
let unit = make_truehd_unit(200); let unit = make_truehd_unit(200);
let pes = make_pes(unit, Some(90000)); let pes = make_pes(unit, Some(90000));
let frames = parser.parse(&pes); let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1); assert_eq!(frames.len(), 1);
// Output should be 200 - 4 = 196 bytes (header stripped) assert_eq!(frames[0].data.len(), 200);
assert_eq!(frames[0].data.len(), 196);
assert_eq!(frames[0].data[0], 0xAA); // payload, not header
} }
#[test] #[test]
@@ -142,19 +215,33 @@ mod tests {
let pes2 = make_pes(unit[mid..].to_vec(), Some(93000)); let pes2 = make_pes(unit[mid..].to_vec(), Some(93000));
let frames = parser.parse(&pes2); let frames = parser.parse(&pes2);
assert_eq!(frames.len(), 1); assert_eq!(frames.len(), 1);
assert_eq!(frames[0].data.len(), 196); // 200 - 4 header assert_eq!(frames[0].data.len(), 200);
} }
#[test] #[test]
fn parse_multiple_units_in_one_pes() { fn parse_multiple_units_incrementing_pts() {
let mut parser = TrueHdParser::new(); let mut parser = TrueHdParser::new();
let mut data = make_truehd_unit(100); let mut data = make_truehd_unit(100);
data.extend_from_slice(&make_truehd_unit(120)); data.extend_from_slice(&make_truehd_unit(120));
let pes = make_pes(data, Some(90000)); let pes = make_pes(data, Some(90000));
let frames = parser.parse(&pes); let frames = parser.parse(&pes);
assert_eq!(frames.len(), 2); assert_eq!(frames.len(), 2);
assert_eq!(frames[0].data.len(), 96); // 100 - 4 assert_eq!(frames[0].data.len(), 100);
assert_eq!(frames[1].data.len(), 116); // 120 - 4 assert_eq!(frames[1].data.len(), 120);
assert_eq!(frames[1].pts_ns - frames[0].pts_ns, AU_DURATION_NS);
}
#[test]
fn skip_interleaved_ac3() {
let mut parser = TrueHdParser::new();
let ac3 = make_ac3_frame();
let truehd = make_truehd_unit(200);
let mut data = ac3;
data.extend_from_slice(&truehd);
let pes = make_pes(data, Some(90000));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].data.len(), 200);
} }
#[test] #[test]