Fix JAR label matching: by language+codec for label format, by index for TextField

Labels like eng_MLP_ now match to the correct stream by checking language
and codec hint against the stream's properties. Fixes Barbie where labels
were swapped (TrueHD label was on DD stream). TextField format (Civil War)
still uses index matching.

TrackLabel now carries language + codec_hint for structured matching.
This commit is contained in:
MattJackson
2026-04-07 16:27:47 -07:00
parent 791b832f25
commit aebe6a256b
2 changed files with 91 additions and 32 deletions
+59 -19
View File
@@ -580,35 +580,63 @@ impl Disc {
} }
/// Merge JAR labels into title streams. /// Merge JAR labels into title streams.
/// Matches by position — JAR audio labels correspond to audio streams in order, ///
/// JAR subtitle labels correspond to subtitle streams in order. /// Two matching strategies:
/// 1. By language+codec (label format like eng_MLP_) — matches stream by content
/// 2. By index (TextField format) — Nth label → Nth stream
fn apply_jar_labels(titles: &mut [Title], jar: &crate::jar::JarLabels) { fn apply_jar_labels(titles: &mut [Title], jar: &crate::jar::JarLabels) {
if jar.audio.is_empty() && jar.subtitle.is_empty() { if jar.audio.is_empty() && jar.subtitle.is_empty() {
return; return;
} }
for title in titles.iter_mut() { // Check if labels have language+codec info (label format)
let mut audio_idx = 0; let has_content_match = jar.audio.iter().any(|l| !l.language.is_empty() && !l.codec_hint.is_empty());
let mut sub_idx = 0;
for stream in &mut title.streams { for title in titles.iter_mut() {
match stream { if has_content_match {
Stream::Audio(a) => { // Match by language + codec
if let Some(label) = jar.audio.get(audio_idx) { Self::apply_labels_by_content(title, jar);
if !label.description.is_empty() { } else {
a.label = label.description.clone(); // Match by index
} Self::apply_labels_by_index(title, jar);
}
}
}
/// Match labels to streams by language + codec hint.
fn apply_labels_by_content(title: &mut Title, jar: &crate::jar::JarLabels) {
// For each JAR audio label, find the matching stream
let mut used = vec![false; jar.audio.len()];
for stream in &mut title.streams {
if let Stream::Audio(a) = stream {
// Find a JAR label matching this stream's language + codec
for (i, label) in jar.audio.iter().enumerate() {
if used[i] { continue; }
if label.language == a.language && codec_matches(&label.codec_hint, a.codec) {
if !label.description.is_empty() {
a.label = label.description.clone();
} }
audio_idx += 1; used[i] = true;
break;
} }
Stream::Subtitle(_s) => {
// TODO: JAR subtitle labels could set forced flag or description
sub_idx += 1;
}
_ => {}
} }
} }
let _ = sub_idx; // suppress warning }
}
/// Match labels to streams by STN index position.
fn apply_labels_by_index(title: &mut Title, jar: &crate::jar::JarLabels) {
let mut audio_idx = 0;
for stream in &mut title.streams {
if let Stream::Audio(a) = stream {
if let Some(label) = jar.audio.get(audio_idx) {
if !label.description.is_empty() {
a.label = label.description.clone();
}
}
audio_idx += 1;
}
} }
} }
@@ -892,6 +920,18 @@ fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048
// ─── Format helpers ──────────────────────────────────────────────────────── // ─── Format helpers ────────────────────────────────────────────────────────
/// Check if a JAR codec hint matches a stream codec.
fn codec_matches(hint: &str, codec: Codec) -> bool {
match hint {
"MLP" => codec == Codec::TrueHd,
"AC3" => codec == Codec::Ac3 || codec == Codec::Ac3Plus,
"DTS" => codec == Codec::Dts || codec == Codec::DtsHdMa || codec == Codec::DtsHdHr,
"LPCM" => codec == Codec::Lpcm,
"ADES" => codec == Codec::Ac3, // descriptive audio is usually DD
_ => false,
}
}
fn format_resolution(video_format: u8, _video_rate: u8) -> String { fn format_resolution(video_format: u8, _video_rate: u8) -> String {
match video_format { match video_format {
1 => "480i".into(), 1 => "480i".into(),
+32 -13
View File
@@ -30,6 +30,10 @@ pub struct JarLabels {
pub struct TrackLabel { pub struct TrackLabel {
/// Human-readable description (e.g. "English Dolby Atmos", "TrueHD", "Descriptive Audio (US)") /// Human-readable description (e.g. "English Dolby Atmos", "TrueHD", "Descriptive Audio (US)")
pub description: String, pub description: String,
/// ISO 639-2 language code if available (e.g. "eng", "fra")
pub language: String,
/// Codec hint if available (e.g. "MLP"=TrueHD, "AC3"=DD, "DTS")
pub codec_hint: String,
/// The raw string from the class file /// The raw string from the class file
pub raw: String, pub raw: String,
} }
@@ -99,8 +103,12 @@ fn try_textfield_format(strings: &[String]) -> Option<JarLabels> {
subtitle.sort_by_key(|s| s.0); subtitle.sort_by_key(|s| s.0);
Some(JarLabels { Some(JarLabels {
audio: audio.into_iter().map(|(_, desc, raw)| TrackLabel { description: desc, raw }).collect(), audio: audio.into_iter().map(|(_, desc, raw)| TrackLabel {
subtitle: subtitle.into_iter().map(|(_, desc, raw)| TrackLabel { description: desc, raw }).collect(), description: desc, language: String::new(), codec_hint: String::new(), raw,
}).collect(),
subtitle: subtitle.into_iter().map(|(_, desc, raw)| TrackLabel {
description: desc, language: String::new(), codec_hint: String::new(), raw,
}).collect(),
playlists, playlists,
}) })
} }
@@ -118,9 +126,15 @@ fn try_label_format(strings: &[String]) -> Option<JarLabels> {
for s in strings { for s in strings {
if let Some(label) = parse_label_string(s) { if let Some(label) = parse_label_string(s) {
if label.is_audio && !audio.iter().any(|a: &TrackLabel| a.raw == label.raw) { if label.is_audio && !audio.iter().any(|a: &TrackLabel| a.raw == label.raw) {
audio.push(TrackLabel { description: label.description, raw: label.raw }); audio.push(TrackLabel {
description: label.description, language: label.language,
codec_hint: label.codec_hint, raw: label.raw,
});
} else if label.is_subtitle && !subtitle.iter().any(|a: &TrackLabel| a.raw == label.raw) { } else if label.is_subtitle && !subtitle.iter().any(|a: &TrackLabel| a.raw == label.raw) {
subtitle.push(TrackLabel { description: label.description, raw: label.raw }); subtitle.push(TrackLabel {
description: label.description, language: label.language,
codec_hint: label.codec_hint, raw: label.raw,
});
} }
} }
@@ -136,6 +150,8 @@ fn try_label_format(strings: &[String]) -> Option<JarLabels> {
struct ParsedLabel { struct ParsedLabel {
description: String, description: String,
language: String,
codec_hint: String,
raw: String, raw: String,
is_audio: bool, is_audio: bool,
is_subtitle: bool, is_subtitle: bool,
@@ -154,25 +170,28 @@ fn parse_label_string(s: &str) -> Option<ParsedLabel> {
let hint = parts[1]; let hint = parts[1];
let variant = if parts.len() > 2 { parts[2] } else { "" }; let variant = if parts.len() > 2 { parts[2] } else { "" };
let (description, is_audio, is_subtitle) = match hint { let (description, codec_hint, is_audio, is_subtitle) = match hint {
"MLP" => ("TrueHD".to_string(), true, false), "MLP" => ("TrueHD".to_string(), "MLP".to_string(), true, false),
"AC3" => { "AC3" => {
let d = if variant.is_empty() { "compatibility".to_string() } else { variant.to_string() }; let d = if variant.is_empty() { "compatibility".to_string() } else { variant.to_string() };
(d, true, false) (d, "AC3".to_string(), true, false)
} }
"DTS" => ("DTS".to_string(), true, false), "DTS" => ("DTS".to_string(), "DTS".to_string(), true, false),
"LPCM" => ("LPCM".to_string(), true, false), "LPCM" => ("LPCM".to_string(), "LPCM".to_string(), true, false),
"ADES" => { "ADES" => {
let d = if variant.is_empty() { "Descriptive Audio".to_string() } let d = if variant.is_empty() { "Descriptive Audio".to_string() }
else { format!("Descriptive Audio ({})", variant) }; else { format!("Descriptive Audio ({})", variant) };
(d, true, false) (d, "ADES".to_string(), true, false)
} }
h if h.starts_with("AudioStream") => (String::new(), true, false), h if h.starts_with("AudioStream") => (String::new(), String::new(), true, false),
h if h.starts_with("PGStream") => (String::new(), false, true), h if h.starts_with("PGStream") => (String::new(), String::new(), false, true),
_ => return None, _ => return None,
}; };
Some(ParsedLabel { description, raw: s.to_string(), is_audio, is_subtitle }) Some(ParsedLabel {
description, language: language.to_string(), codec_hint,
raw: s.to_string(), is_audio, is_subtitle,
})
} }
// ── Format 3: Playlist-only ───────────────────────────────────────────────── // ── Format 3: Playlist-only ─────────────────────────────────────────────────