libfreemkv: rc.5.1 DVD correctness fixes

- CSS: unlock scrambled-sector reads on enforcing drives via bus-auth
  only; classify sense 6F/03 as CSS-locked; early-bail on a fully locked
  scan; gate the AACS handshake off DVD discs.
- DVD first-play menu no longer prepended to the feature: read the title
  VOBS base from vtstt_vobs (0xC4), not the menu VOBS vtsm_vobs (0xC0).
- Interlaced field-duration (DefaultDecodedFieldDuration) written as a
  direct TrackEntry child rather than inside Video, so Windows reports
  the correct frame rate.
- Audio channel count read from the AC-3 bitstream; FieldOrder set to
  TFF; per-track BPS tags.
- Structured disc diagnostics at --log-level 3; reduced per-operation
  log spam.
This commit is contained in:
Matthew Jackson
2026-06-24 14:34:55 -07:00
parent 315276dd13
commit 6592f2a590
18 changed files with 1938 additions and 120 deletions
+181
View File
@@ -261,6 +261,81 @@ fn frame_duration_ns(data: &[u8], bsid: u8) -> u64 {
(samples * 1_000_000_000 + rate / 2) / rate
}
/// Base channel count per AC-3 `acmod` (A/52 Table 5.8), BEFORE the LFE.
/// Index is the 3-bit acmod value; add 1 when `lfeon` is set.
///
/// ```text
/// 0 = 1+1 (Ch1, Ch2) -> 2 4 = 3/0 (L,C,R) -> 3
/// 1 = 1/0 (C, mono) -> 1 5 = 2/1 (L,R,S) -> 3
/// 2 = 2/0 (L, R) -> 2 6 = 3/1 (L,C,R,S) -> 4
/// 3 = 3/0 (L,C,R) -> 3 7 = 3/2 (L,C,R,SL,SR) -> 5
/// ```
const ACMOD_CHANNELS: [u8; 8] = [2, 1, 2, 3, 3, 3, 4, 5];
/// Decode the channel count of an (E-)AC-3 frame from its bitstream `acmod` and
/// `lfeon`, starting at the 0x0B77 syncword. Returns `None` when the frame is
/// too short to carry the BSI bits.
///
/// This is the AUTHORITATIVE channel count for the track header: the DVD IFO
/// `audio_attr_t.channels` nibble is a well-known unreliable/stale field, so
/// the muxer prefers this over the IFO-claimed count (mirrors MakeMKV /
/// HandBrake, which never trust the IFO audio nibble). LFE adds one channel
/// (e.g. acmod=7 + lfeon → 6 = 5.1).
///
/// Bit layout from the syncword (A/52 §5.3.2 BSI):
///
/// ```text
/// byte 5: bsid(5) | bsmod(3)
/// byte 6: acmod(3) | [cmixlev(2) if acmod has a centre and acmod!=1]
/// | [surmixlev(2) if acmod has surround]
/// | [dsurmod(2) if acmod==2] | lfeon(1) | ...
/// ```
///
/// `acmod` therefore always occupies byte-6 bits 7-5; `lfeon` follows a
/// variable number of optional 2-bit fields, so we track the bit cursor.
pub(crate) fn acmod_channels(data: &[u8]) -> Option<u8> {
// Need at least bytes 0..=6 to read acmod (byte 6) and its trailing
// optional fields + lfeon (which never spills past byte 7 for any acmod).
if data.len() < 8 {
return None;
}
let bsid = get_bsid(data);
// E-AC-3 (bsid >= 11, Annex E) uses a different BSI layout. DVD audio is
// always legacy AC-3 (bsid <= 8); for E-AC-3 we don't decode acmod here
// and let the caller fall back to the passed channel count.
if bsid >= 11 {
return None;
}
// Bit cursor over `data`, MSB-first, starting at byte 6 bit 7 (= bit 48).
let mut bit = 6 * 8;
let read = |n: usize, bit: &mut usize| -> u32 {
let mut v = 0u32;
for _ in 0..n {
let byte = data[*bit / 8];
let shift = 7 - (*bit % 8);
v = (v << 1) | ((byte >> shift) & 1) as u32;
*bit += 1;
}
v
};
let acmod = read(3, &mut bit) as usize;
// cmixlev: present when acmod has a centre channel AND is not the 1/0
// (centre-only) mode — i.e. acmod & 0x1 != 0 && acmod != 0x1.
if (acmod & 0x1) != 0 && acmod != 0x1 {
let _cmixlev = read(2, &mut bit);
}
// surmixlev: present when acmod has a surround channel (acmod & 0x4).
if (acmod & 0x4) != 0 {
let _surmixlev = read(2, &mut bit);
}
// dsurmod: present only for the 2/0 (stereo) mode.
if acmod == 0x2 {
let _dsurmod = read(2, &mut bit);
}
let lfeon = read(1, &mut bit);
Some(ACMOD_CHANNELS[acmod] + lfeon as u8)
}
/// Find AC3/E-AC-3 syncword (0x0B77) in data.
fn find_ac3_sync(data: &[u8]) -> Option<usize> {
(0..data.len().saturating_sub(1)).find(|&i| data[i] == 0x0B && data[i + 1] == 0x77)
@@ -968,6 +1043,112 @@ mod tests {
assert!(parser.flush().is_empty());
}
// --- acmod_channels: channel count from the AC-3 BSI bitstream ---
/// Build a minimal AC-3 BSI header (8 bytes) with a given acmod + lfeon.
/// byte5 = bsid<<3 (bsmod=0); byte6 carries acmod in bits 7-5 followed by
/// the optional mix-level fields and lfeon. We construct byte6/7 by writing
/// bits MSB-first in the exact order acmod_channels reads them.
fn make_bsi(acmod: u8, lfeon: bool) -> Vec<u8> {
// Collect the bit sequence after byte 6 bit 7: acmod(3), [cmixlev(2)],
// [surmixlev(2)], [dsurmod(2)], lfeon(1). Mix-level/dsurmod bits are
// arbitrary (0 here) — only their PRESENCE shifts lfeon's position.
let mut bits: Vec<u8> = Vec::new();
for i in (0..3).rev() {
bits.push((acmod >> i) & 1);
}
if (acmod & 0x1) != 0 && acmod != 0x1 {
bits.push(0);
bits.push(0); // cmixlev
}
if (acmod & 0x4) != 0 {
bits.push(0);
bits.push(0); // surmixlev
}
if acmod == 0x2 {
bits.push(0);
bits.push(0); // dsurmod
}
bits.push(lfeon as u8); // lfeon
// Pack bits MSB-first starting at byte 6.
let mut frame = vec![0u8; 8];
frame[0] = 0x0B;
frame[1] = 0x77;
frame[5] = 8 << 3; // bsid = 8 (legacy AC-3), bsmod = 0
for (idx, &b) in bits.iter().enumerate() {
let bitpos = 6 * 8 + idx;
if b != 0 {
frame[bitpos / 8] |= 1 << (7 - (bitpos % 8));
}
}
frame
}
#[test]
fn acmod_channels_stereo_2_0_no_lfe() {
// acmod=2 (2/0 L,R), no LFE → 2 channels. Verifies the channel count is
// read from the AC-3 bitstream's acmod, independent of any IFO claim.
// (A disc whose IFO lists 5.1 but where the wrong physical substream is
// selected is a separate stream-SELECTION bug, not this label path —
// tracked for rc.5.2.)
assert_eq!(acmod_channels(&make_bsi(2, false)), Some(2));
}
#[test]
fn acmod_channels_5_1() {
// acmod=7 (3/2 L,C,R,SL,SR) + LFE → 6 channels (5.1).
assert_eq!(acmod_channels(&make_bsi(7, true)), Some(6));
// 3/2 without LFE → 5 channels.
assert_eq!(acmod_channels(&make_bsi(7, false)), Some(5));
}
#[test]
fn acmod_channels_mono_and_dual_mono() {
// acmod=1 (1/0 centre/mono) → 1; with LFE → 2.
assert_eq!(acmod_channels(&make_bsi(1, false)), Some(1));
assert_eq!(acmod_channels(&make_bsi(1, true)), Some(2));
// acmod=0 (1+1 dual mono) → 2 base channels.
assert_eq!(acmod_channels(&make_bsi(0, false)), Some(2));
}
#[test]
fn acmod_channels_3_0_and_2_1() {
// acmod=4 (3/0 L,C,R) → 3 (exercises cmixlev present, surmixlev absent).
assert_eq!(acmod_channels(&make_bsi(4, false)), Some(3));
// acmod=5 (2/1 L,R,S) → 3 (surmixlev present, no centre).
assert_eq!(acmod_channels(&make_bsi(5, false)), Some(3));
// acmod=6 (3/1) + LFE → 5; lfeon position shifts after both
// cmixlev (centre) and surmixlev (surround) 2-bit fields.
assert_eq!(acmod_channels(&make_bsi(6, true)), Some(5));
}
#[test]
fn acmod_channels_short_frame_is_none() {
// Fewer than 8 bytes cannot carry the BSI bits → None (caller falls
// back to the IFO-claimed channel count).
assert_eq!(acmod_channels(&[0x0B, 0x77, 0, 0, 0, 8 << 3]), None);
assert_eq!(acmod_channels(&[]), None);
}
#[test]
fn acmod_channels_eac3_is_none() {
// E-AC-3 (bsid >= 11) uses a different BSI layout; acmod_channels
// declines so the caller keeps the passed count.
let mut data = make_bsi(2, false);
data[5] = 16 << 3; // bsid = 16 (E-AC-3)
assert_eq!(acmod_channels(&data), None);
}
#[test]
fn acmod_channels_parses_real_built_frame() {
// A frame built by make_ac3_frame (fscod/frmsizecod set, acmod bits 0)
// decodes acmod=0 → 2 channels (dual mono), confirming the cursor lands
// on the right bytes for a fully-formed frame, not just a stub header.
let frame = make_ac3_frame(0, 2);
// make_ac3_frame leaves byte 6 = 0 → acmod=0, lfeon=0 → 2 channels.
assert_eq!(acmod_channels(&frame), Some(2));
}
// helper: PES with a generic pts for E-AC-3 tests
fn make_eac3_pes(data: Vec<u8>) -> PesPacket {
PesPacket {