//! Cutscene-voice resolution: movie → sound-id → continuous-stream byte region. //! //! Reverse-engineered 2026-07-22 (Canary file-I/O trace, user-confirmed by ear). //! The movie voices form **one continuous XMA stream**, physically chunked into //! `\Movie\VOICE_*.slb` `sound.pak` TOC entries whose boundaries do **not** //! align with the cutscene cues — a single cue routinely spans two `.slb` chunks, //! so a `.slb` does not necessarily hold the track its name claims. Each cue's //! audio ends at an inline **trailer descriptor** `(sound_id: u32be, 0x11, …)` //! whose id repeats at +0x800. Cue N's audio = the stream bytes //! `[descriptor(N-1) .. descriptor(N)]`, read continuously across chunk //! boundaries. Verified: decoded voice length matches each movie within ~0.3 s, //! including cross-boundary cues (RT01B/RT01C span `VOICE_ADV`→`VOICE_RT01A`→…). //! //! Resolution chain: //! 1. movie → cue name (`ADVERTISE_MOVIE` manifest `VOICETRACK`, e.g. `VOICE_RT01A`) //! 2. cue name → sound-id (master sound registry, [`registry_voice_ids`], → 1601) //! 3. sound-id → `[start, end]` global offsets via [`find_descriptor`] over the //! `sound.pNN` stream (the caller supplies the bytes + the physical anchor). use std::collections::HashMap; /// Parse the master sound registry — the large per-language `tables.pak` IDXD /// entry (schema `0x13cb84ba`) that carries the `\Movie\VOICE_*.slb` paths — /// into `cue-name → sound-id` from its `NAME\0 ID\0` string pool /// (e.g. `VOICE_ADV`→1600, `VOICE_RT01A`→1601). pub fn registry_voice_ids(registry: &[u8]) -> HashMap { let mut toks: Vec<&[u8]> = Vec::new(); let mut start = 0usize; for i in 0..registry.len() { if registry[i] == 0 { if i > start { toks.push(®istry[start..i]); } start = i + 1; } } let mut out = HashMap::new(); for w in toks.windows(2) { let (name, num) = (w[0], w[1]); if name.starts_with(b"VOICE_") && !num.is_empty() && num.iter().all(u8::is_ascii_digit) { if let (Ok(n), Ok(id)) = ( std::str::from_utf8(name), std::str::from_utf8(num).unwrap().parse::(), ) { out.entry(n.to_string()).or_insert(id); } } } out } /// The constant marker following a cue's sound-id in its inline trailer. const DESC_MARK: u32 = 0x11; /// The trailer repeats its id this many bytes later; used to reject a coincidental /// `(id, 0x11)` pair inside XMA audio (probability of a false match is ~2⁻⁶⁴). const DESC_REPEAT: usize = 0x800; /// Byte offset (within `buf`, a slice of the continuous voice stream) of cue /// `id`'s trailer descriptor: the first big-endian `(id, 0x11)` pair whose id /// repeats at `+0x800`. `None` if absent in the slice. pub fn find_descriptor(buf: &[u8], id: u32) -> Option { if buf.len() < DESC_REPEAT + 4 { return None; } let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]); let end = buf.len() - (DESC_REPEAT + 4); let mut o = 0; while o <= end { if be(o) == id && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id { return Some(o); } o += 4; } None } /// Plausible sound-id range for a trailer (all real cue ids are well under this). const ID_MAX: u32 = 0x1_0000; /// Offset of the nearest cue trailer strictly **before** `before` — any plausible /// id. Used to bound a cue's START when its `id-1` predecessor doesn't exist /// (the sound-id sequence has gaps, e.g. `VOICE_D_453`=7142 → `VOICE_D_454`=7188). /// Scans downward and returns the first (largest-offset) valid `(id, 0x11)` with /// the `+0x800` id-repeat. `None` if there is no trailer below `before`. pub fn find_descriptor_before(buf: &[u8], before: usize) -> Option { if before < 4 { return None; } let cap = buf.len().checked_sub(DESC_REPEAT + 4)?; let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]); // Start strictly below `before` (4-aligned) so the target's own trailer is // never returned as its predecessor. let mut o = (before - 1).min(cap) & !3; loop { let id = be(o); if id >= 1 && id < ID_MAX && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id { return Some(o); } if o < 4 { return None; } o -= 4; } } #[cfg(test)] mod tests { use super::*; #[test] fn registry_parses_name_id_pairs() { let mut b = Vec::new(); for tok in [ b"VOICE_ADV".as_slice(), b"1600", b"VOICE_RT01A", b"1601", b"NOT_A_VOICE", b"9", ] { b.extend_from_slice(tok); b.push(0); } let ids = registry_voice_ids(&b); assert_eq!(ids.get("VOICE_ADV"), Some(&1600)); assert_eq!(ids.get("VOICE_RT01A"), Some(&1601)); assert_eq!(ids.get("NOT_A_VOICE"), None); } #[test] fn find_descriptor_matches_repeat() { let mut b = vec![0u8; 0x900]; b[0..4].copy_from_slice(&1601u32.to_be_bytes()); b[4..8].copy_from_slice(&0x11u32.to_be_bytes()); b[DESC_REPEAT..DESC_REPEAT + 4].copy_from_slice(&1601u32.to_be_bytes()); assert_eq!(find_descriptor(&b, 1601), Some(0)); assert_eq!(find_descriptor(&b, 1600), None); } }