dvd: route AC-3 audio to the physically-correct sub-stream by probed channel count

Fixes the "Silence of the Lambs" R2 PAL wrong-substream rip: the feature's
IFO declares one 5.1 AC-3 stream, but the scan assigned it the on-wire
sub-stream id 0x80 purely by per-codec ordinal (ifo::assign_audio_sub_stream_ids).
On this disc the physical 0x80 carries the 2.0 down-mix and the 5.1 main mix
lives at a different 0x8x sub-stream, so the rip muxed 2.0 while labelling it
"Dolby Digital 5.1" (the acmod fixup in mkv.rs then corrected only the Channels
element, surfacing the mismatch as the "IFO claimed 6 but acmod says 2" warning
— too late to re-route).

New src/disc/dvd_audio_probe.rs probes each physical AC-3 sub-stream's real
channel count from the head of the feature (the acmod/lfeon of its first frame
after the 0x0B77 sync) and re-routes each IFO-declared AC-3 stream onto the
physical sub-stream whose actual channel count matches the declared count,
instead of trusting the ordinal. Wired into both mux demux paths
(DiscStream::new and resolve::build_iso_pipeline) over the decrypting reader,
so it works on CSS discs and the autorip ISO-remux path alike. Bounded
512-sector best-effort read; an empty/unreadable probe degrades to the original
ordinal mapping (no regression on normal discs).

The cell selection is left unchanged: the feature's cell 0 (cat=0x02, 302.4s)
is chapter 1 of the movie (matches MakeMKV's chapter map and 1h53 duration
exactly), so it must NOT be dropped — the perceived "wrong video at the start"
was the wrong 2.0 audio over the opening, the same root cause.

Diagnostics (--log-level 3): new tag=dvd.substream rows dump the ACTUAL acmod
channel count of each physical 0x8x sub-stream read from the VOB, and the
per-cell tag=dvd.cell verdict now spells out the keep/skip reason. With the
existing tag=dvd.aattr (IFO declared sub_id + channels) a bug log alone now
shows whether the ordinal 0x80 really carries the declared layout — no disc
needed to diagnose this class.

expose ac3::find_ac3_sync as pub(crate) for the probe.
This commit is contained in:
Matthew Jackson
2026-06-24 16:28:21 -07:00
parent 1cec2aaaf3
commit 674a7dd867
6 changed files with 446 additions and 9 deletions
+46 -3
View File
@@ -134,6 +134,19 @@ pub fn sample_rate_hz(s: SampleRate) -> u32 {
/// filter.
pub fn dvd_cell_row(idx: usize, cell: &crate::ifo::DvdCell, dropped: bool) -> String {
let c = CellCategory::decode(cell.category);
// Per-cell keep/skip REASON (self-sufficient bug log): a dropped cell is a
// leading secondary angle/interleave block piece; a kept cell is either the
// first feature cell or genuine feature content. This makes the
// leading-cell-filter decision auditable from the log without the disc.
let verdict = if dropped {
"DROP(leading-secondary-block-piece)"
} else if c.is_secondary_block_piece() {
// Kept despite being a secondary piece — only happens past the leading
// run (the filter stops at the first plain feature cell).
"keep(feature-body)"
} else {
"keep(plain-feature)"
};
format!(
"tag=dvd.cell idx={idx} cat=0x{:02X} type={} block_mode={} block_type={} \
seamless={} ilv={} plain={} first={} last={} dur={:.1}s {}",
@@ -147,7 +160,7 @@ seamless={} ilv={} plain={} first={} last={} dur={:.1}s {}",
cell.first_sector,
cell.last_sector,
cell.duration_secs,
if dropped { "DROP(non-feature)" } else { "keep" },
verdict,
)
}
@@ -229,6 +242,36 @@ pub fn dump_dvd_attrs(ts: &crate::ifo::DvdTitleSet) {
}
}
/// Emit the ACTUAL per-physical-sub-stream AC-3 channel counts read off the VOB
/// during the mux-time sub-stream probe (the Silence-of-the-Lambs wrong-stream
/// fix). This is the ground truth the IFO nibble is compared against: each row
/// is `sub_id=0x8x channels=N` for a physical `private_stream_1` AC-3 sub-stream
/// whose first frame was decoded. An empty probe (scrambled / unreadable / short
/// VOB) logs a single `probed=0` line so the absence is explicit in a bug log.
///
/// Self-sufficiency: with `tag=dvd.aattr` (the IFO's declared sub_id + claimed
/// channels) and these `tag=dvd.substream` rows (the physical reality), a bug
/// log alone shows whether the ordinal `0x80` actually carries the declared
/// channel layout — no disc needed to diagnose a wrong-substream rip.
pub fn dump_dvd_substream_probe(title_id: u16, probed: &std::collections::BTreeMap<u8, u8>) {
if !tracing::enabled!(target: DIAG, tracing::Level::DEBUG) {
return;
}
if probed.is_empty() {
tracing::debug!(
target: DIAG,
"tag=dvd.substream title={title_id} probed=0 (no AC-3 sync in feature head — scrambled/unreadable/none)",
);
return;
}
for (sub, ch) in probed {
tracing::debug!(
target: DIAG,
"tag=dvd.substream title={title_id} sub_id=0x{sub:02X} channels={ch} (physical acmod read from VOB)",
);
}
}
// ── Disc-level dump (post-lowering: titles, streams, decisions, AACS) ────────
/// Emit the full scan diagnostic block for a built [`Disc`]. Terse, one line
@@ -450,7 +493,7 @@ mod tests {
assert!(row.contains("first=100"), "{row}");
assert!(row.contains("last=199"), "{row}");
assert!(row.contains("dur=12.5s"), "{row}");
assert!(row.contains("keep"), "{row}");
assert!(row.contains("keep(plain-feature)"), "{row}");
assert!(!row.contains("DROP"), "{row}");
// 0x80 = middle-of-angle-block (cell_type=2), shown dropped.
@@ -463,6 +506,6 @@ mod tests {
let row = dvd_cell_row(0, &sec, true);
assert!(row.contains("cat=0x80"), "{row}");
assert!(row.contains("type=2"), "{row}");
assert!(row.contains("DROP(non-feature)"), "{row}");
assert!(row.contains("DROP(leading-secondary-block-piece)"), "{row}");
}
}