labels/deluxe: Phase D rewrite against ground-truth binding pattern
Replaces the speculative arg-position heuristic with type-presence
detection driven by real disc bytecode. Ground truth captured in
(internal)/research/deluxe-poc/data/ via POC v0.3 binding-
bytecode dumps against disc-01 (Disney) and disc-09 (Warner).
What changed:
1. StackVal::CodingType(String) — new variant. getstatic against
org/bluray/ti/CodingType (the BD-J spec codec enum) now pushes
this, carrying the field name (e.g. DOLBY_LOSSLESS_AUDIO). The
pre-fix code was treating codecs as a Deluxe-internal enum
subclass walk (Phase B), which is the wrong model — codecs are
standard BD-J API references.
2. coding_type_to_codec_hint(field) — new function. Maps
org.bluray.ti.CodingType field names to human-readable codec
strings (DOLBY_LOSSLESS_AUDIO -> "Dolby TrueHD", DOLBY_AC3_AUDIO
-> "Dolby Digital", etc.). Unknown field names pass through
verbatim so future codec values still surface something.
3. find_binding_classes — multi-class variant. Some Deluxe discs
split per-stream tables across two binding classes (audio +
subtitle). Returns top-K candidates by getstatic count, filtered
to >=40% of the top count and capped at 4. Replaces the old
single-class find_binding_class (which was unused after this
change).
4. interpret_streams — rewritten. Args identified by TYPE not
position:
- First EnumRef{kind:"Language"} -> language
- First EnumRef{kind:"Purpose"} -> purpose
- First CodingType(name) -> codec_hint
- First Int(n) -> stream index hint (traced
only; per-type sequential stream_number still wins because BD
spec stream-numbering is anchored on MPLS)
- Construction has CodingType -> Audio stream; otherwise Subtitle
- No Language -> skip (not a stream construction)
This handles BOTH the Disney 5-arg pattern (I, Lbe, Llp, I,
LCodingType) and the Warner 4-arg pattern (I, Law, Lgp,
LCodingType) automatically — same code path because args are
identified by type rather than constructor-signature shape.
5. parse() now walks all binding-class candidates and unions
their constructions before calling interpret_streams. Logs each
candidate at INFO with getstatic_count for diagnosis.
Tests:
- 2 new tests verify the CodingType -> codec_hint mapping for
known + unknown field names.
- Existing interpret_streams tests updated to use the new
signature (dropped CodecTable arg).
- Audio-emission test rewritten to use CodingType arg instead of
the old binding_type substring-match approach.
Confidence is still Medium for now (single-corpus verification);
ready to promote to High once tested against a third Deluxe disc.
Precommit (cargo +1.86 fmt + clippy + test) green.
This commit is contained in:
+206
-109
@@ -52,7 +52,7 @@
|
|||||||
//! (each enum value's `<init>` is called with its name string as
|
//! (each enum value's `<init>` is called with its name string as
|
||||||
//! the first arg).
|
//! the first arg).
|
||||||
//!
|
//!
|
||||||
//! - **Phase C** — binding-class identification (`find_binding_class`).
|
//! - **Phase C** — binding-class identification (`find_binding_classes`).
|
||||||
//! The per-stream table is built by some class via repeated
|
//! The per-stream table is built by some class via repeated
|
||||||
//! `getstatic` references to the master enums identified in A.
|
//! `getstatic` references to the master enums identified in A.
|
||||||
//! That class has the highest such `getstatic` count in the jar.
|
//! That class has the highest such `getstatic` count in the jar.
|
||||||
@@ -138,34 +138,41 @@ pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<ParseResult>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase C — find the binding class via getstatic count to
|
// Phase C — find ALL binding-class candidates (audio + subtitle
|
||||||
// the master enums.
|
// are often split across two classes on Deluxe). Each gets its
|
||||||
let binding_class_name = find_binding_class(archive, &master_table.class_name_set());
|
// own `<clinit>` walk; constructions union into a single
|
||||||
let Some(binding_class_name) = binding_class_name else {
|
// stream list for interpret_streams.
|
||||||
|
let binding_classes = find_binding_classes(archive, &master_table.class_name_set());
|
||||||
|
if binding_classes.is_empty() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
jar = %entry_name,
|
jar = %entry_name,
|
||||||
"deluxe: no binding class found (no class has enough getstatic refs to master enums)"
|
"deluxe: no binding class found (no class has enough getstatic refs to master enums)"
|
||||||
);
|
);
|
||||||
return None;
|
return None;
|
||||||
};
|
}
|
||||||
tracing::info!(
|
for (name, count) in &binding_classes {
|
||||||
jar = %entry_name,
|
tracing::info!(
|
||||||
binding_class = %binding_class_name,
|
jar = %entry_name,
|
||||||
"deluxe binding class identified",
|
binding_class = %name,
|
||||||
);
|
getstatic_count = count,
|
||||||
|
"deluxe binding class candidate",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase D — decode the binding class's <clinit>.
|
// Phase D — decode each binding class's <clinit>.
|
||||||
let streams = decode_binding(archive, &binding_class_name, &master_table);
|
let mut streams: Vec<Construction> = Vec::new();
|
||||||
|
for (name, _) in &binding_classes {
|
||||||
|
streams.extend(decode_binding(archive, name, &master_table));
|
||||||
|
}
|
||||||
if streams.is_empty() {
|
if streams.is_empty() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
jar = %entry_name,
|
jar = %entry_name,
|
||||||
binding_class = %binding_class_name,
|
"deluxe: binding classes found but produced 0 decoded streams"
|
||||||
"deluxe: binding class found but produced 0 decoded streams"
|
|
||||||
);
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let labels = interpret_streams(&streams, &codec_table, &master_table);
|
let labels = interpret_streams(&streams, &master_table);
|
||||||
if labels.is_empty() {
|
if labels.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -545,26 +552,39 @@ fn clinit_news_and_ldcs(
|
|||||||
/// arity); we use a low floor (4) so a small disc with few streams
|
/// arity); we use a low floor (4) so a small disc with few streams
|
||||||
/// still qualifies, but high enough to filter out classes that just
|
/// still qualifies, but high enough to filter out classes that just
|
||||||
/// reference the language enum once for a config string.
|
/// reference the language enum once for a config string.
|
||||||
pub(crate) fn find_binding_class(
|
/// Identify all binding-class candidates by getstatic-count to the
|
||||||
|
/// master enums. Some Deluxe discs split the per-stream table across
|
||||||
|
/// two binding classes (one for audio, one for subtitle), so the
|
||||||
|
/// per-stream decoder needs to walk all of them. Returns top-K
|
||||||
|
/// candidates ordered by descending getstatic count, filtered to a
|
||||||
|
/// minimum concentration of master-enum references.
|
||||||
|
///
|
||||||
|
/// Empirically (POC v0.3 dumps): on disc-01 the audio binding class
|
||||||
|
/// (`ma.class`) has ~82 getstatic refs, the subtitle binding
|
||||||
|
/// (`ko.class`) has ~63. Both share the master Language + Purpose
|
||||||
|
/// enums.
|
||||||
|
pub(crate) fn find_binding_classes(
|
||||||
archive: &mut jar::Jar,
|
archive: &mut jar::Jar,
|
||||||
master_enum_classes: &HashSet<&str>,
|
master_enum_classes: &HashSet<&str>,
|
||||||
) -> Option<String> {
|
) -> Vec<(String, usize)> {
|
||||||
const MIN_GETSTATIC: usize = 4;
|
const MIN_GETSTATIC: usize = 4;
|
||||||
let mut best: Option<(String, usize)> = None;
|
let mut candidates: Vec<(String, usize)> = Vec::new();
|
||||||
jar::for_each_class(archive, |class_name, class| {
|
jar::for_each_class(archive, |class_name, class| {
|
||||||
let count = count_master_enum_getstatic(class, master_enum_classes);
|
let count = count_master_enum_getstatic(class, master_enum_classes);
|
||||||
if count < MIN_GETSTATIC {
|
if count >= MIN_GETSTATIC {
|
||||||
return;
|
candidates.push((class_name.to_string(), count));
|
||||||
}
|
|
||||||
match &best {
|
|
||||||
None => best = Some((class_name.to_string(), count)),
|
|
||||||
Some((_, c)) if count > *c => {
|
|
||||||
best = Some((class_name.to_string(), count));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
best.map(|(name, _)| name)
|
candidates.sort_by_key(|(_, c)| std::cmp::Reverse(*c));
|
||||||
|
// Top candidates only — anything significantly below the top one
|
||||||
|
// is noise. We keep candidates whose count is at least 40% of the
|
||||||
|
// top, capped at 4 total (audio + subtitle + future use).
|
||||||
|
if let Some(top_count) = candidates.first().map(|(_, c)| *c) {
|
||||||
|
let threshold = (top_count * 2) / 5; // 40%
|
||||||
|
candidates.retain(|(_, c)| *c >= threshold);
|
||||||
|
candidates.truncate(4);
|
||||||
|
}
|
||||||
|
candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count `getstatic` instructions in this class's `<clinit>` whose
|
/// Count `getstatic` instructions in this class's `<clinit>` whose
|
||||||
@@ -617,6 +637,15 @@ pub(crate) enum StackVal {
|
|||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
ordinal: u16,
|
ordinal: u16,
|
||||||
},
|
},
|
||||||
|
/// Reference to a `org.bluray.ti.CodingType` enum value. Field
|
||||||
|
/// name (e.g. `DOLBY_AC3_AUDIO`, `DOLBY_LOSSLESS_AUDIO`) is the
|
||||||
|
/// codec identifier. Deluxe binding constructors take a
|
||||||
|
/// `LCodingType;` arg directly — codecs are NOT a Deluxe-internal
|
||||||
|
/// enum (Phase B's codec-subclass walk was based on a wrong
|
||||||
|
/// assumption; the actual codec source is the standard BD-J API
|
||||||
|
/// enum). Discovered via deluxe-poc v0.3 binding-bytecode dump
|
||||||
|
/// against disc-01 (Disney) + disc-09 (Warner) on 2026-05-10.
|
||||||
|
CodingType(String),
|
||||||
/// An uninitialized `new` object — popped by the matching
|
/// An uninitialized `new` object — popped by the matching
|
||||||
/// invokespecial.
|
/// invokespecial.
|
||||||
NewObj(String),
|
NewObj(String),
|
||||||
@@ -626,6 +655,10 @@ pub(crate) enum StackVal {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fully-qualified class name of the BD-J spec codec enum that
|
||||||
|
/// Deluxe constructors reference directly.
|
||||||
|
const BD_CODING_TYPE_CLASS: &str = "org/bluray/ti/CodingType";
|
||||||
|
|
||||||
/// Phase D entry point: find the binding class in `archive`, run the
|
/// Phase D entry point: find the binding class in `archive`, run the
|
||||||
/// bytecode walker against its `<clinit>`, return one `Construction`
|
/// bytecode walker against its `<clinit>`, return one `Construction`
|
||||||
/// per `new X / invokespecial X.<init>` sequence.
|
/// per `new X / invokespecial X.<init>` sequence.
|
||||||
@@ -755,10 +788,19 @@ impl<'a> BindingDecoder<'a> {
|
|||||||
let val = insn
|
let val = insn
|
||||||
.cp_index()
|
.cp_index()
|
||||||
.and_then(|i| self.pool.member_ref(i))
|
.and_then(|i| self.pool.member_ref(i))
|
||||||
.and_then(|m| {
|
.map(|m| {
|
||||||
self.master
|
// Three-way resolution:
|
||||||
.resolve(m.class_name, m.name)
|
// 1. org.bluray.ti.CodingType.X → CodingType(X)
|
||||||
.map(|(kind, ord)| StackVal::EnumRef { kind, ordinal: ord })
|
// 2. master-enum classname.X → EnumRef(kind, ord)
|
||||||
|
// 3. anything else → Unknown
|
||||||
|
if m.class_name == BD_CODING_TYPE_CLASS {
|
||||||
|
StackVal::CodingType(m.name.to_string())
|
||||||
|
} else if let Some((kind, ord)) = self.master.resolve(m.class_name, m.name)
|
||||||
|
{
|
||||||
|
StackVal::EnumRef { kind, ordinal: ord }
|
||||||
|
} else {
|
||||||
|
StackVal::Unknown
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or(StackVal::Unknown);
|
.unwrap_or(StackVal::Unknown);
|
||||||
self.stack.push(val);
|
self.stack.push(val);
|
||||||
@@ -946,48 +988,37 @@ impl MasterEnumTable {
|
|||||||
// ── interpret_streams: Constructions → StreamLabels ─────────────────────────
|
// ── interpret_streams: Constructions → StreamLabels ─────────────────────────
|
||||||
|
|
||||||
/// Convert the per-construction tuples from Phase D into
|
/// Convert the per-construction tuples from Phase D into
|
||||||
/// [`StreamLabel`]s using a documented heuristic.
|
/// [`StreamLabel`]s. Pattern verified against corpus discs via
|
||||||
|
/// deluxe-poc v0.3 binding-bytecode dump (2026-05-10):
|
||||||
///
|
///
|
||||||
/// Heuristic:
|
/// Disney binding (5-arg): `BindingType.<init>(I, Lbe;, Llp;, I, LCodingType;)V`
|
||||||
/// - For each Construction, classify the args into language / codec /
|
/// Warner binding (4-arg): `BindingType.<init>(I, Law;, Lgp;, LCodingType;)V`
|
||||||
/// purpose / region / int slots using the EnumRef kinds.
|
|
||||||
/// - A construction with a Language ref AND a Codec ref → Audio stream.
|
|
||||||
/// - A construction with a Language ref AND no Codec ref → Subtitle.
|
|
||||||
/// - Any construction with no Language ref → ignored (not a stream).
|
|
||||||
/// - stream_number = sequential per type (1, 2, 3 ...).
|
|
||||||
/// - language = vocab::lang fallback applied to the enum value
|
|
||||||
/// string (so "Brazilian Portuguese" → "por" + variant="Brazilian").
|
|
||||||
/// When the enum value isn't a recognized phrase, the raw value is
|
|
||||||
/// used as language verbatim (ISO codes pass through).
|
|
||||||
/// - codec_hint = the CodecTable lookup of the codec ordinal.
|
|
||||||
/// - purpose = LabelPurpose decoded from the Purpose-enum value
|
|
||||||
/// string via the documented Deluxe Purpose enum (Normal,
|
|
||||||
/// Commentary, PiP, Trivia, Descriptive, Score, NoForced,
|
|
||||||
/// NoForcedDescriptive).
|
|
||||||
/// - qualifier = LabelQualifier::Forced when the Purpose-enum value
|
|
||||||
/// is one of the *Forced* variants (Normal+Forced bit etc.); else
|
|
||||||
/// None. (Deluxe's framework doesn't appear to expose SDH at this
|
|
||||||
/// layer — that comes from the subtitle codec.)
|
|
||||||
///
|
///
|
||||||
/// This heuristic is the part of Phase D that needs corpus-disc
|
/// Args are identified by **TYPE**, not position:
|
||||||
/// verification. The DECODING (Constructions from bytecode) is
|
/// - First `EnumRef{kind: "Language"}` → audio/subtitle language
|
||||||
/// mechanically correct; the SEMANTIC INTERPRETATION here may need
|
/// - First `EnumRef{kind: "Purpose"}` → Deluxe purpose ordinal
|
||||||
/// adjustment once we see real binding-class output.
|
/// - First `CodingType(name)` → codec field name (translated via
|
||||||
fn interpret_streams(
|
/// [`coding_type_to_codec_hint`])
|
||||||
constructions: &[Construction],
|
/// - First `Int(n)` → stream index (preserved as ordering hint;
|
||||||
codec_table: &CodecTable,
|
/// per-type sequential stream_number is what actually goes into
|
||||||
master: &MasterEnumTable,
|
/// the StreamLabel, since BD spec stream-numbering is anchored on
|
||||||
) -> Vec<StreamLabel> {
|
/// MPLS data, not the binding code)
|
||||||
|
///
|
||||||
|
/// Stream type inference:
|
||||||
|
/// - Construction has a `CodingType` arg → audio stream (subtitles
|
||||||
|
/// on Deluxe don't carry a CodingType; their codec is implicit
|
||||||
|
/// PGS via the BD spec).
|
||||||
|
/// - Construction has Language but no CodingType → subtitle stream.
|
||||||
|
/// - No Language → not a stream (skip).
|
||||||
|
fn interpret_streams(constructions: &[Construction], master: &MasterEnumTable) -> Vec<StreamLabel> {
|
||||||
let mut audio_idx: u16 = 0;
|
let mut audio_idx: u16 = 0;
|
||||||
let mut sub_idx: u16 = 0;
|
let mut sub_idx: u16 = 0;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
for c in constructions {
|
for c in constructions {
|
||||||
// Collect typed args: language / purpose ordinals + any int
|
|
||||||
// (preserved for future stream-index resolution; logged only
|
|
||||||
// for now).
|
|
||||||
let mut lang_ord: Option<u16> = None;
|
let mut lang_ord: Option<u16> = None;
|
||||||
let mut purpose_ord: Option<u16> = None;
|
let mut purpose_ord: Option<u16> = None;
|
||||||
|
let mut coding_type: Option<String> = None;
|
||||||
let mut stream_idx_hint: Option<i32> = None;
|
let mut stream_idx_hint: Option<i32> = None;
|
||||||
for arg in &c.args {
|
for arg in &c.args {
|
||||||
match arg {
|
match arg {
|
||||||
@@ -996,31 +1027,28 @@ fn interpret_streams(
|
|||||||
"Purpose" => purpose_ord = purpose_ord.or(Some(*ordinal)),
|
"Purpose" => purpose_ord = purpose_ord.or(Some(*ordinal)),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
StackVal::Int(n) => stream_idx_hint = stream_idx_hint.or(Some(*n)),
|
StackVal::CodingType(name) => {
|
||||||
|
coding_type = coding_type.or_else(|| Some(name.clone()));
|
||||||
|
}
|
||||||
|
StackVal::Int(n) => {
|
||||||
|
stream_idx_hint = stream_idx_hint.or(Some(*n));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Codec enum isn't fingerprinted by ldc prefix (0 ldcs
|
|
||||||
// in its <clinit>), so it isn't in MasterEnumTable.
|
|
||||||
// EnumRefs resolved through master_table never carry the
|
|
||||||
// "Codec" kind. Codec association at this layer is heuristic:
|
|
||||||
// if the construction's binding_type matches one of the
|
|
||||||
// CodecTable entries (substring match), use that codec.
|
|
||||||
// Stronger codec→ordinal resolution is deferred until corpus
|
|
||||||
// bytecode confirms the codec field's location in the
|
|
||||||
// binding constructor's args.
|
|
||||||
let codec_hint = codec_table
|
|
||||||
.codecs
|
|
||||||
.iter()
|
|
||||||
.find(|name| !name.is_empty() && c.binding_type.contains(name.as_str()))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let Some(lang_ord) = lang_ord else { continue };
|
let Some(lang_ord) = lang_ord else { continue };
|
||||||
|
|
||||||
// Audio when codec is known, subtitle otherwise.
|
// Audio when a CodingType is present (audio binding type
|
||||||
let (stream_type, stream_number) = if !codec_hint.is_empty() {
|
// always references org.bluray.ti.CodingType); subtitle
|
||||||
|
// otherwise.
|
||||||
|
let codec_hint = coding_type
|
||||||
|
.as_deref()
|
||||||
|
.map(coding_type_to_codec_hint)
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let (stream_type, stream_number) = if coding_type.is_some() {
|
||||||
audio_idx += 1;
|
audio_idx += 1;
|
||||||
(StreamLabelType::Audio, audio_idx)
|
(StreamLabelType::Audio, audio_idx)
|
||||||
} else {
|
} else {
|
||||||
@@ -1044,10 +1072,10 @@ fn interpret_streams(
|
|||||||
|
|
||||||
if let Some(hint) = stream_idx_hint {
|
if let Some(hint) = stream_idx_hint {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
stream_idx_hint = hint,
|
disc_stream_idx = hint,
|
||||||
lang = %language,
|
lang = %language,
|
||||||
binding = %c.binding_type,
|
binding = %c.binding_type,
|
||||||
"deluxe interpret_streams: captured int arg for future stream-index resolution"
|
"deluxe interpret_streams: disc-authored stream index (not used for stream_number; preserved for diagnostic)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,6 +1094,39 @@ fn interpret_streams(
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map a `org.bluray.ti.CodingType` field name (as observed in
|
||||||
|
/// getstatic operands on Deluxe binding classes) to a human-readable
|
||||||
|
/// codec hint string.
|
||||||
|
///
|
||||||
|
/// CodingType is the standard BD-J API enum; values are documented
|
||||||
|
/// in the BD-J specification and verified empirically against the
|
||||||
|
/// binding-bytecode dumps in `(internal)/research/deluxe-poc/data/`.
|
||||||
|
/// Unknown field names pass through unchanged so unfamiliar codecs
|
||||||
|
/// still surface something rather than going silent.
|
||||||
|
fn coding_type_to_codec_hint(field: &str) -> &str {
|
||||||
|
match field {
|
||||||
|
// Lossless / hi-res.
|
||||||
|
"DOLBY_LOSSLESS_AUDIO" => "Dolby TrueHD",
|
||||||
|
"DTS_HD_LOSSLESS_AUDIO" | "DTS_HD_MA_AUDIO" => "DTS-HD Master Audio",
|
||||||
|
"LPCM_AUDIO" => "LPCM",
|
||||||
|
// Dolby family.
|
||||||
|
"DOLBY_AC3_AUDIO" => "Dolby Digital",
|
||||||
|
"DOLBY_DIGITAL_PLUS_AUDIO" => "Dolby Digital Plus",
|
||||||
|
"DOLBY_ATMOS_AUDIO" => "Dolby Atmos",
|
||||||
|
// DTS family.
|
||||||
|
"DTS_AUDIO" => "DTS",
|
||||||
|
"DTS_HD_AUDIO" | "DTS_HD_HR_AUDIO" => "DTS-HD HR",
|
||||||
|
// MPEG family.
|
||||||
|
"MPEG1_AUDIO_LAYER2" | "MPEG2_AUDIO_LAYER2" => "MPEG Audio",
|
||||||
|
// PG-style subtitle codecs (rare to see in Deluxe bindings;
|
||||||
|
// subtitles usually have NO CodingType arg).
|
||||||
|
"PG_STREAM" | "PRESENTATION_GRAPHICS_STREAM" => "PGS",
|
||||||
|
// Unknown / future — pass through verbatim so the operator
|
||||||
|
// can see what the disc actually authored.
|
||||||
|
_ => field,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Deluxe Purpose enum ordinal → (LabelPurpose, LabelQualifier). The
|
/// Deluxe Purpose enum ordinal → (LabelPurpose, LabelQualifier). The
|
||||||
/// enum order is fixed per Phase A's verified output:
|
/// enum order is fixed per Phase A's verified output:
|
||||||
/// 0=Normal, 1=Commentary, 2=PiP, 3=Trivia, 4=Descriptive, 5=Score,
|
/// 0=Normal, 1=Commentary, 2=PiP, 3=Trivia, 4=Descriptive, 5=Score,
|
||||||
@@ -1419,9 +1480,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interpret_streams_emits_subtitle_when_no_codec() {
|
fn interpret_streams_emits_subtitle_when_no_codingtype() {
|
||||||
// A Construction with just a language enum ref + no codec
|
// A Construction with just a language enum ref (no CodingType)
|
||||||
// → subtitle stream (codec_hint stays empty).
|
// -> subtitle stream (codec_hint stays empty). Subtitles on
|
||||||
|
// Deluxe don't carry a CodingType arg.
|
||||||
let constructions = vec![Construction {
|
let constructions = vec![Construction {
|
||||||
binding_type: "SubtitleSlot".into(),
|
binding_type: "SubtitleSlot".into(),
|
||||||
args: vec![StackVal::EnumRef {
|
args: vec![StackVal::EnumRef {
|
||||||
@@ -1429,9 +1491,7 @@ mod tests {
|
|||||||
ordinal: 0,
|
ordinal: 0,
|
||||||
}],
|
}],
|
||||||
}];
|
}];
|
||||||
let codec_table = CodecTable::default();
|
let out = interpret_streams(&constructions, &lang_enum_master());
|
||||||
let master = lang_enum_master();
|
|
||||||
let out = interpret_streams(&constructions, &codec_table, &master);
|
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 1);
|
||||||
assert_eq!(out[0].stream_type, StreamLabelType::Subtitle);
|
assert_eq!(out[0].stream_type, StreamLabelType::Subtitle);
|
||||||
assert_eq!(out[0].language, "eng");
|
assert_eq!(out[0].language, "eng");
|
||||||
@@ -1439,24 +1499,28 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interpret_streams_emits_audio_when_binding_matches_codec_name() {
|
fn interpret_streams_emits_audio_when_codingtype_present() {
|
||||||
// A Construction whose binding_type contains a codec name
|
// A Construction with a CodingType arg -> audio stream with
|
||||||
// from CodecTable → audio stream with codec_hint populated.
|
// codec_hint populated by coding_type_to_codec_hint.
|
||||||
let constructions = vec![Construction {
|
let constructions = vec![Construction {
|
||||||
binding_type: "AudioSlot_ATMOS".into(),
|
binding_type: "ng".into(),
|
||||||
args: vec![StackVal::EnumRef {
|
args: vec![
|
||||||
kind: "Language",
|
StackVal::Int(1),
|
||||||
ordinal: 0,
|
StackVal::EnumRef {
|
||||||
}],
|
kind: "Language",
|
||||||
|
ordinal: 0,
|
||||||
|
},
|
||||||
|
StackVal::EnumRef {
|
||||||
|
kind: "Purpose",
|
||||||
|
ordinal: 0,
|
||||||
|
},
|
||||||
|
StackVal::CodingType("DOLBY_LOSSLESS_AUDIO".into()),
|
||||||
|
],
|
||||||
}];
|
}];
|
||||||
let codec_table = CodecTable {
|
let out = interpret_streams(&constructions, &lang_enum_master());
|
||||||
codecs: vec!["ATMOS".into(), "DTS".into()],
|
|
||||||
};
|
|
||||||
let master = lang_enum_master();
|
|
||||||
let out = interpret_streams(&constructions, &codec_table, &master);
|
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 1);
|
||||||
assert_eq!(out[0].stream_type, StreamLabelType::Audio);
|
assert_eq!(out[0].stream_type, StreamLabelType::Audio);
|
||||||
assert_eq!(out[0].codec_hint, "ATMOS");
|
assert_eq!(out[0].codec_hint, "Dolby TrueHD");
|
||||||
assert_eq!(out[0].language, "eng");
|
assert_eq!(out[0].language, "eng");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1475,7 +1539,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
let out = interpret_streams(&constructions, &CodecTable::default(), &lang_enum_master());
|
let out = interpret_streams(&constructions, &lang_enum_master());
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 1);
|
||||||
assert_eq!(out[0].purpose, LabelPurpose::Commentary);
|
assert_eq!(out[0].purpose, LabelPurpose::Commentary);
|
||||||
}
|
}
|
||||||
@@ -1486,10 +1550,43 @@ mod tests {
|
|||||||
binding_type: "SomeOtherType".into(),
|
binding_type: "SomeOtherType".into(),
|
||||||
args: vec![StackVal::Int(1)],
|
args: vec![StackVal::Int(1)],
|
||||||
}];
|
}];
|
||||||
let out = interpret_streams(&constructions, &CodecTable::default(), &lang_enum_master());
|
let out = interpret_streams(&constructions, &lang_enum_master());
|
||||||
assert!(out.is_empty());
|
assert!(out.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coding_type_maps_known_codecs() {
|
||||||
|
// BD-J spec CodingType field names -> display strings.
|
||||||
|
assert_eq!(
|
||||||
|
coding_type_to_codec_hint("DOLBY_LOSSLESS_AUDIO"),
|
||||||
|
"Dolby TrueHD"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
coding_type_to_codec_hint("DOLBY_AC3_AUDIO"),
|
||||||
|
"Dolby Digital"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
coding_type_to_codec_hint("DOLBY_DIGITAL_PLUS_AUDIO"),
|
||||||
|
"Dolby Digital Plus"
|
||||||
|
);
|
||||||
|
assert_eq!(coding_type_to_codec_hint("DTS_AUDIO"), "DTS");
|
||||||
|
assert_eq!(
|
||||||
|
coding_type_to_codec_hint("DTS_HD_MA_AUDIO"),
|
||||||
|
"DTS-HD Master Audio"
|
||||||
|
);
|
||||||
|
assert_eq!(coding_type_to_codec_hint("LPCM_AUDIO"), "LPCM");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coding_type_passes_through_unknown() {
|
||||||
|
// Unknown field names pass through verbatim so the operator
|
||||||
|
// sees what the disc authored.
|
||||||
|
assert_eq!(
|
||||||
|
coding_type_to_codec_hint("FUTURE_CODEC_X"),
|
||||||
|
"FUTURE_CODEC_X"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extract_codec_name_picks_uppercase_with_underscore() {
|
fn extract_codec_name_picks_uppercase_with_underscore() {
|
||||||
// Synthetic class file built via ClassFile::parse would be
|
// Synthetic class file built via ClassFile::parse would be
|
||||||
|
|||||||
Reference in New Issue
Block a user