dts: emit clean core alone when the extension boundary is garbage

DTS-HD MA access units are a lossy core frame followed by trailing
extension substreams up to the next core sync. On source-damaged discs
(observed on the Bourne UHDs) the bytes where the XLL extension belongs
are neither a core sync nor an extension sync -- pure garbage -- which
desyncs ffmpeg's XLL decoder and cascades into 'Read past end of XLL
band data' / 'DSYNC check failed' across the whole track.

next_core_boundary now distinguishes three boundary states via a new
ext_clean flag on NextCore::Found:
  - precise/recognized extension sync  -> ext_clean=true  (keep full AU)
  - garbage at the boundary byte       -> ext_clean=false (drop the ext)

When ext_clean is false we emit the DTS core alone (drop the smallest
junk piece, keep the frame and its PTS) and drain past the garbage to
the next core. Recognized-but-unsizeable extensions still ride the
heuristic scan and are kept intact, so lossless tracks are unaffected --
only genuinely corrupt extension bytes are dropped.

Bourne s1.dts: 1606 damaged frames / 691620 (0.232%), 93% isolated
single frames, worst run 3 in a row (~32ms lossy blip).
This commit is contained in:
Matthew Jackson
2026-07-19 09:27:28 -07:00
parent c6e6bb9f4b
commit f255361683
+90 -15
View File
@@ -299,8 +299,8 @@ impl CodecParser for DtsParser {
// flush is an extension-substream PES, carrying its own later // flush is an extension-substream PES, carrying its own later
// timestamp) must NOT become the next unit's PTS base. // timestamp) must NOT become the next unit's PTS base.
let mut forced = false; let mut forced = false;
let au_end = match next_core_boundary(&self.buf, core_size) { let (au_end, ext_clean) = match next_core_boundary(&self.buf, core_size) {
NextCore::Found(end) => end, NextCore::Found { end, ext_clean } => (end, ext_clean),
NextCore::NeedMore => break, // candidate sync needs more header NextCore::NeedMore => break, // candidate sync needs more header
NextCore::None => { NextCore::None => {
// No next core sync buffered yet. The trailing extension // No next core sync buffered yet. The trailing extension
@@ -312,11 +312,20 @@ impl CodecParser for DtsParser {
break; break;
} }
forced = true; forced = true;
self.buf.len() (self.buf.len(), true)
} }
}; };
let au: Vec<u8> = self.buf[..au_end].to_vec(); // Damaged source encoding: when the extension boundary was GARBAGE
// (not any DTS sync — `ext_clean == false`), the extension bytes for
// this AU are corrupt and would make the decoder cascade "DSYNC check
// failed" / "Read past end of XLL band data". Emit the clean DTS core
// ALONE (a decodable, lossy frame) and still drain past the garbage to
// the next core — a perfect mux drops the bad frame's corrupt part
// rather than shipping it. A recognized-but-unsizeable extension
// (`ext_clean == true`) is preserved in full (lossless).
let emit_end = if ext_clean { au_end } else { core_size };
let au: Vec<u8> = self.buf[..emit_end].to_vec();
// The AU's own core PES PTS (the PES covering its first byte, even if // The AU's own core PES PTS (the PES covering its first byte, even if
// that PES preceded the one(s) carrying its extensions or the next // that PES preceded the one(s) carrying its extensions or the next
// core), stamped monotonically: honored when it advances past the // core), stamped monotonically: honored when it advances past the
@@ -407,7 +416,14 @@ fn find_sync(data: &[u8], pattern: &[u8; 4]) -> Option<usize> {
/// Result of scanning for the next valid core sync that closes an access unit. /// Result of scanning for the next valid core sync that closes an access unit.
enum NextCore { enum NextCore {
/// A valid next core sync was found; the access unit ends at this offset. /// A valid next core sync was found; the access unit ends at this offset.
Found(usize), /// `ext_clean` is `false` only when the byte at the extension boundary was
/// GARBAGE — neither a core sync nor a DTS-HD extension sync — meaning the
/// extension region is corrupt (damaged source encoding). The caller then
/// emits the clean DTS core alone and drops the garbage, instead of shipping
/// a corrupt AU that makes the decoder cascade DSYNC / "Read past end of XLL".
/// It stays `true` when the region is a real (if unsizeable) extension sync —
/// that path is load-bearing for valid streams and must NOT be dropped.
Found { end: usize, ext_clean: bool },
/// A candidate core sync was found but its header isn't fully buffered yet, /// A candidate core sync was found but its header isn't fully buffered yet,
/// so its validity can't be decided — wait for more data. /// so its validity can't be decided — wait for more data.
NeedMore, NeedMore,
@@ -492,8 +508,11 @@ fn next_core_boundary(buf: &[u8], core_size: usize) -> NextCore {
} }
pos += sz; // skip the whole extension substream precisely pos += sz; // skip the whole extension substream precisely
} }
// Couldn't size it (truncated/garbage header) — heuristic fallback. // A real extension sync we couldn't size (truncated header /
_ => return scan_for_next_core(buf, pos), // unsupported sub-form) — heuristic fallback, but the region IS a
// recognized extension, so keep it (ext_clean = true). This path
// is load-bearing for valid streams.
_ => return scan_for_next_core(buf, pos, true),
} }
} else if buf[pos..].starts_with(&DTS_CORE_SYNC) { } else if buf[pos..].starts_with(&DTS_CORE_SYNC) {
// The bytes right after the precisely-skipped extensions are the next // The bytes right after the precisely-skipped extensions are the next
@@ -503,13 +522,18 @@ fn next_core_boundary(buf: &[u8], core_size: usize) -> NextCore {
} }
let sz = dts_core_frame_size(&buf[pos..]); let sz = dts_core_frame_size(&buf[pos..]);
if (MIN_CORE_FRAME_BYTES..=MAX_AU_BYTES).contains(&sz) { if (MIN_CORE_FRAME_BYTES..=MAX_AU_BYTES).contains(&sz) {
return NextCore::Found(pos); return NextCore::Found {
end: pos,
ext_clean: true,
};
} }
return scan_for_next_core(buf, pos); // implausible core here — fall back return scan_for_next_core(buf, pos, true); // implausible core — recognized sync, keep
} else { } else {
// Neither a known extension nor a core sync at the precise boundary // GARBAGE at the extension boundary — neither a core sync nor a
// (padding / junk) — fall back to the heuristic scan. // DTS-HD extension sync. This is damaged source encoding: the
return scan_for_next_core(buf, pos); // extension region is corrupt. Mark ext_clean = false so the caller
// emits the clean core alone and drops the garbage.
return scan_for_next_core(buf, pos, false);
} }
} }
} }
@@ -518,7 +542,7 @@ fn next_core_boundary(buf: &[u8], core_size: usize) -> NextCore {
/// syncword whose decoded size is plausible. Used only when precise extension /// syncword whose decoded size is plausible. Used only when precise extension
/// skipping can't proceed; a chance core syncword in extension payload usually /// skipping can't proceed; a chance core syncword in extension payload usually
/// decodes to an implausible size and is skipped. /// decodes to an implausible size and is skipped.
fn scan_for_next_core(buf: &[u8], from: usize) -> NextCore { fn scan_for_next_core(buf: &[u8], from: usize, ext_clean: bool) -> NextCore {
let mut from = from; let mut from = from;
while let Some(rel) = find_sync(&buf[from..], &DTS_CORE_SYNC) { while let Some(rel) = find_sync(&buf[from..], &DTS_CORE_SYNC) {
let pos = from + rel; let pos = from + rel;
@@ -527,7 +551,10 @@ fn scan_for_next_core(buf: &[u8], from: usize) -> NextCore {
} }
let sz = dts_core_frame_size(&buf[pos..]); let sz = dts_core_frame_size(&buf[pos..]);
if (MIN_CORE_FRAME_BYTES..=MAX_AU_BYTES).contains(&sz) { if (MIN_CORE_FRAME_BYTES..=MAX_AU_BYTES).contains(&sz) {
return NextCore::Found(pos); return NextCore::Found {
end: pos,
ext_clean,
};
} }
from = pos + SYNCWORD_BYTES; from = pos + SYNCWORD_BYTES;
} }
@@ -676,12 +703,60 @@ mod tests {
assert!( assert!(
matches!( matches!(
next_core_boundary(&buf, core.len()), next_core_boundary(&buf, core.len()),
NextCore::Found(end) if end == core.len() + exss.len() NextCore::Found { end, .. } if end == core.len() + exss.len()
), ),
"AU must end at the REAL next core (after the full EXSS), not the false sync inside it" "AU must end at the REAL next core (after the full EXSS), not the false sync inside it"
); );
} }
#[test]
fn garbage_extension_emits_core_only_but_valid_ext_is_kept() {
// Damaged source: a valid core, then GARBAGE (no core sync, no extension
// sync) where the extension belongs, then the next core. The framer must
// mark this boundary ext_clean=false and the parser must emit the clean
// 512-byte CORE alone (dropping the garbage), draining to the next core.
let core = make_dts_core(512);
let garbage = vec![0xE4, 0x3F, 0xE3, 0x90, 0xCC, 0x6C]; // real Bourne head bytes
let mut garbage = garbage;
garbage.extend(std::iter::repeat(0xAB).take(300));
let next = make_dts_core(512);
let mut buf = core.clone();
buf.extend_from_slice(&garbage);
buf.extend_from_slice(&next);
assert!(
matches!(
next_core_boundary(&buf, core.len()),
NextCore::Found { end, ext_clean: false } if end == core.len() + garbage.len()
),
"garbage boundary must be flagged unclean"
);
let mut parser = DtsParser::new();
let mut frames = parser.parse(&make_pes(buf, Some(90000)));
frames.extend(parser.flush());
assert!(!frames.is_empty());
for f in &frames {
assert_eq!(f.data.len(), 512, "garbage-extension AU emits core only");
assert_eq!(&f.data[0..4], &DTS_CORE_SYNC);
}
// Contrast: a REAL extension sync (even if unsizeable) must be KEPT in
// full — ext_clean stays true, never downgraded to core-only.
let mut buf2 = make_dts_core(512);
buf2.extend_from_slice(&make_dts_ext(256));
buf2.extend_from_slice(&make_dts_core(512));
assert!(
matches!(
next_core_boundary(&buf2, 512),
NextCore::Found {
ext_clean: true,
..
}
),
"a recognized extension sync is preserved, not dropped"
);
}
#[test] #[test]
fn parse_empty_pes() { fn parse_empty_pes() {
let mut parser = DtsParser::new(); let mut parser = DtsParser::new();