dvd: route private_stream_1 audio sub-ids by positional stream number
The private_stream_1 sub-stream id low nibble is the DVD audio-stream NUMBER (0-7), shared across codecs, not a per-codec ordinal. A DTS (or LPCM) track that isn't the disc's first audio stream got a sub-id one too low, so the demux routing key (0xBD00 | sub_id) never matched and every packet was dropped -> present-but-silent track. AC-3 at position 0 coincidentally worked (ordinal==position). Route by positional index. Fixes silent DTS 5.0 track on The Punisher (AC-3 5.1 @ pos0 plays, DTS 5.0 @ pos1 was 0xBD88, real wire id 0xBD89).
This commit is contained in:
+12
-11
@@ -922,18 +922,19 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
"DTS 2.0 nibble must decode to 2 channels"
|
"DTS 2.0 nibble must decode to 2 channels"
|
||||||
);
|
);
|
||||||
// PIDs route via the per-codec sub-id table: AC-3 #0 → 0x80 → 0xBD80,
|
// PIDs route via the positional sub-id table: AC-3 @ pos 0 → 0x80 →
|
||||||
// DTS #0 → 0x88 → 0xBD88. Distinct (no 0xBD00 collision) AND the exact
|
// 0xBD80, DTS @ pos 1 → 0x89 → 0xBD89 (the shared audio-stream number
|
||||||
// canonical PIDs.
|
// in the low nibble, NOT a per-codec ordinal). Distinct AND the exact
|
||||||
assert_eq!(audios[0].pid, 0xBD80, "AC-3 #0 → 0xBD80");
|
// canonical wire PIDs the demux routes on.
|
||||||
assert_eq!(audios[1].pid, 0xBD88, "DTS #0 → 0xBD88");
|
assert_eq!(audios[0].pid, 0xBD80, "AC-3 @ pos 0 → 0xBD80");
|
||||||
|
assert_eq!(audios[1].pid, 0xBD89, "DTS @ pos 1 → 0xBD89");
|
||||||
assert_ne!(audios[0].pid, audios[1].pid);
|
assert_ne!(audios[0].pid, audios[1].pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// LPCM SCAN ROUTING (audit §2 / §5 #6): the 0xA0..=0xA7 PID range was never
|
/// LPCM SCAN ROUTING (audit §2 / §5 #6): the 0xA0..=0xA7 PID range was never
|
||||||
/// exercised in the dvd.rs scan. An LPCM stream (coding_mode 4) must get
|
/// exercised in the dvd.rs scan. An LPCM stream (coding_mode 4) at audio
|
||||||
/// sub_stream_id 0xA0 → PID 0xBDA0 via `dvd_audio_pid`, distinct from the
|
/// position 1 must get sub_stream_id 0xA1 → PID 0xBDA1 via `dvd_audio_pid`,
|
||||||
/// AC-3 0xBD80 space, with its real channel count preserved.
|
/// distinct from the AC-3 0xBD80 space, with its real channel count preserved.
|
||||||
#[test]
|
#[test]
|
||||||
fn scan_dvd_titles_lpcm_routes_to_a0_pid_range() {
|
fn scan_dvd_titles_lpcm_routes_to_a0_pid_range() {
|
||||||
let mut disc = MemDisc::new();
|
let mut disc = MemDisc::new();
|
||||||
@@ -978,10 +979,10 @@ mod tests {
|
|||||||
assert_eq!(audios.len(), 2);
|
assert_eq!(audios.len(), 2);
|
||||||
assert_eq!(audios[0].codec, Codec::Ac3);
|
assert_eq!(audios[0].codec, Codec::Ac3);
|
||||||
assert_eq!(audios[1].codec, Codec::Lpcm, "coding_mode 4 → LPCM");
|
assert_eq!(audios[1].codec, Codec::Lpcm, "coding_mode 4 → LPCM");
|
||||||
assert_eq!(audios[0].pid, 0xBD80, "AC-3 #0 → 0xBD80");
|
assert_eq!(audios[0].pid, 0xBD80, "AC-3 @ pos 0 → 0xBD80");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
audios[1].pid, 0xBDA0,
|
audios[1].pid, 0xBDA1,
|
||||||
"LPCM #0 → 0xBDA0 (the 0xA0 sub-id range), NOT the AC-3 space"
|
"LPCM @ pos 1 → 0xBDA1 (the 0xA0 sub-id range | position), NOT the AC-3 space"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
audios[1].channels.count(),
|
audios[1].channels.count(),
|
||||||
|
|||||||
+69
-35
@@ -621,37 +621,34 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Assign the on-wire `private_stream_1` sub-stream id to each audio
|
/// Assign the on-wire `private_stream_1` sub-stream id to each audio
|
||||||
/// stream by per-codec ordinal, matching DVD authoring convention and the
|
/// stream. On DVD-Video the sub-id's **low nibble is the audio-stream
|
||||||
/// muxer's `dvd_pid()` routing:
|
/// *number* (0-7), shared across all codecs** — the single stream index the
|
||||||
/// - AC-3 → `0x80 + n` (n = 0-based index among AC-3 streams)
|
/// PGC `audio_control` table / navigation registers select — and the high
|
||||||
/// - DTS → `0x88 + n`
|
/// nibble is the codec base. So the sub-id is `codec_base | position`, where
|
||||||
/// - LPCM → `0xA0 + n`
|
/// `position` is the stream's index in the IFO audio-attribute table (NOT a
|
||||||
|
/// per-codec running count):
|
||||||
|
/// - AC-3 → `0x80 | i`
|
||||||
|
/// - DTS → `0x88 | i`
|
||||||
|
/// - LPCM → `0xA0 | i`
|
||||||
/// - MP1/MP2 and anything else → `None` (regular MPEG-audio PES, not a
|
/// - MP1/MP2 and anything else → `None` (regular MPEG-audio PES, not a
|
||||||
/// private-stream-1 sub-id).
|
/// private-stream-1 sub-id).
|
||||||
///
|
///
|
||||||
/// Indices saturate at the codec range ceiling (8 AC-3/DTS, 8 LPCM) so a
|
/// A per-codec ordinal was wrong: it only coincides with the wire id when a
|
||||||
/// malformed over-count never produces an out-of-range sub-id.
|
/// codec's first stream is also the disc's audio stream #0. Any codec that is
|
||||||
|
/// not the first audio stream (e.g. a DTS track after an AC-3 track) then got
|
||||||
|
/// a sub-id one-too-low, so the demux routing key (`0xBD00 | sub_id`) never
|
||||||
|
/// matched and the track muxed silent. The positional index is the real wire
|
||||||
|
/// number, so distinct positions still give distinct sub-ids (no collision).
|
||||||
|
///
|
||||||
|
/// Position saturates at 7 so a malformed over-count never produces an
|
||||||
|
/// out-of-range sub-id.
|
||||||
fn assign_audio_sub_stream_ids(streams: &mut [DvdAudioAttr]) {
|
fn assign_audio_sub_stream_ids(streams: &mut [DvdAudioAttr]) {
|
||||||
let mut n_ac3 = 0u8;
|
for (i, s) in streams.iter_mut().enumerate() {
|
||||||
let mut n_dts = 0u8;
|
let n = (i as u8).min(7);
|
||||||
let mut n_lpcm = 0u8;
|
|
||||||
for s in streams.iter_mut() {
|
|
||||||
s.sub_stream_id = match s.codec {
|
s.sub_stream_id = match s.codec {
|
||||||
Codec::Ac3 => {
|
Codec::Ac3 => Some(0x80 | n),
|
||||||
let id = 0x80 + n_ac3.min(7);
|
Codec::Dts => Some(0x88 | n),
|
||||||
n_ac3 = n_ac3.saturating_add(1);
|
Codec::Lpcm => Some(0xA0 | n),
|
||||||
Some(id)
|
|
||||||
}
|
|
||||||
Codec::Dts => {
|
|
||||||
let id = 0x88 + n_dts.min(7);
|
|
||||||
n_dts = n_dts.saturating_add(1);
|
|
||||||
Some(id)
|
|
||||||
}
|
|
||||||
Codec::Lpcm => {
|
|
||||||
let id = 0xA0 + n_lpcm.min(7);
|
|
||||||
n_lpcm = n_lpcm.saturating_add(1);
|
|
||||||
Some(id)
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1138,9 +1135,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_codec_sub_stream_ids_are_distinct() {
|
fn mixed_codec_sub_stream_ids_are_distinct() {
|
||||||
// A title mixing AC-3, DTS and LPCM must get per-codec ordinal
|
// A title mixing AC-3, DTS and LPCM: the sub-id low nibble is the
|
||||||
// sub-ids (0x80, 0x88, 0xA0...), all distinct — this is the
|
// POSITIONAL audio-stream number (shared across codecs), OR'd with the
|
||||||
// routing key that keeps mixed-codec audio from colliding.
|
// codec base. So idx 1 (DTS) → 0x89, idx 3 (AC-3) → 0x83 — the real
|
||||||
|
// wire ids the demux routes on. All distinct (positions are unique).
|
||||||
let mut streams = vec![
|
let mut streams = vec![
|
||||||
DvdAudioAttr {
|
DvdAudioAttr {
|
||||||
codec: Codec::Ac3,
|
codec: Codec::Ac3,
|
||||||
@@ -1172,10 +1170,10 @@ mod tests {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
assign_audio_sub_stream_ids(&mut streams);
|
assign_audio_sub_stream_ids(&mut streams);
|
||||||
assert_eq!(streams[0].sub_stream_id, Some(0x80)); // AC-3 #0
|
assert_eq!(streams[0].sub_stream_id, Some(0x80)); // AC-3 @ pos 0
|
||||||
assert_eq!(streams[1].sub_stream_id, Some(0x88)); // DTS #0
|
assert_eq!(streams[1].sub_stream_id, Some(0x89)); // DTS @ pos 1
|
||||||
assert_eq!(streams[2].sub_stream_id, Some(0xA0)); // LPCM #0
|
assert_eq!(streams[2].sub_stream_id, Some(0xA2)); // LPCM @ pos 2
|
||||||
assert_eq!(streams[3].sub_stream_id, Some(0x81)); // AC-3 #1
|
assert_eq!(streams[3].sub_stream_id, Some(0x83)); // AC-3 @ pos 3
|
||||||
// All sub-ids unique.
|
// All sub-ids unique.
|
||||||
let ids: Vec<u8> = streams.iter().filter_map(|s| s.sub_stream_id).collect();
|
let ids: Vec<u8> = streams.iter().filter_map(|s| s.sub_stream_id).collect();
|
||||||
let mut sorted = ids.clone();
|
let mut sorted = ids.clone();
|
||||||
@@ -1184,6 +1182,42 @@ mod tests {
|
|||||||
assert_eq!(ids.len(), sorted.len(), "sub-stream ids must be unique");
|
assert_eq!(ids.len(), sorted.len(), "sub-stream ids must be unique");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Regression (The Punisher 2004): audio[0]=AC-3 5.1, audio[1]=DTS 5.0.
|
||||||
|
/// The DTS track sits at audio position 1, so its wire sub-id is 0x89
|
||||||
|
/// (0x88 | 1), NOT the per-codec 0x88. With the old per-codec ordinal it
|
||||||
|
/// got 0x88 → demux routing key 0xBD88 had no match → every DTS packet
|
||||||
|
/// (which carries 0x89) was dropped → the track muxed present-but-silent
|
||||||
|
/// while the AC-3 (at position 0, where ordinal and position coincide)
|
||||||
|
/// played fine. Positional numbering fixes it end-to-end.
|
||||||
|
#[test]
|
||||||
|
fn dts_after_ac3_uses_positional_substream_id() {
|
||||||
|
let mut streams = vec![
|
||||||
|
DvdAudioAttr {
|
||||||
|
codec: Codec::Ac3,
|
||||||
|
channels: 6,
|
||||||
|
sample_rate: 48000,
|
||||||
|
language: "en".into(),
|
||||||
|
sub_stream_id: None,
|
||||||
|
},
|
||||||
|
DvdAudioAttr {
|
||||||
|
codec: Codec::Dts,
|
||||||
|
channels: 5,
|
||||||
|
sample_rate: 48000,
|
||||||
|
language: "en".into(),
|
||||||
|
sub_stream_id: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assign_audio_sub_stream_ids(&mut streams);
|
||||||
|
assert_eq!(streams[0].sub_stream_id, Some(0x80));
|
||||||
|
assert_eq!(
|
||||||
|
streams[1].sub_stream_id,
|
||||||
|
Some(0x89),
|
||||||
|
"DTS at audio position 1 routes to 0x89 on the wire, not 0x88"
|
||||||
|
);
|
||||||
|
// The routing key the muxer actually uses must resolve for 0x89.
|
||||||
|
assert_eq!(crate::mux::ps::dvd_audio_pid(0x89), Some(0xBD89));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn audio_attr_dts() {
|
fn audio_attr_dts() {
|
||||||
let mut data = vec![0u8; 16];
|
let mut data = vec![0u8; 16];
|
||||||
@@ -1371,10 +1405,10 @@ mod tests {
|
|||||||
];
|
];
|
||||||
assign_audio_sub_stream_ids(&mut streams);
|
assign_audio_sub_stream_ids(&mut streams);
|
||||||
assert_eq!(streams[0].sub_stream_id, None); // MP2 → no sub-id
|
assert_eq!(streams[0].sub_stream_id, None); // MP2 → no sub-id
|
||||||
assert_eq!(streams[1].sub_stream_id, Some(0x80)); // AC3 #0
|
assert_eq!(streams[1].sub_stream_id, Some(0x81)); // AC3 @ pos 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// assign_audio_sub_stream_ids saturates the per-codec ordinal at the
|
/// assign_audio_sub_stream_ids saturates the positional index at the
|
||||||
/// range ceiling (min(7)) so a malformed over-count never produces an
|
/// range ceiling (min(7)) so a malformed over-count never produces an
|
||||||
/// out-of-range sub-id. 9 AC-3 streams: the 9th still ≤ 0x87.
|
/// out-of-range sub-id. 9 AC-3 streams: the 9th still ≤ 0x87.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user