The port hit a 2+1 chunk signature on a resolved movie-voice region and asked
whether the bank-header explanation that closed HANDOFF Q10 also covers it,
rather than assuming it. It does not, and the discriminator is mechanical.
Disc-wide over the 95 English movie-voice regions the manifest binds:
78 open with a bank header -- bank_header_len fires, 10240 B = 5 packets
exactly, every time. That is the BGM case.
17 open with a leading headerless stream -- bank_header_len is None, and all
17 have length congruent to 1392 mod 2048, the disc s own derived data
offset. No other residue occurs.
0 begin at a RIFF.
Counting chunks does not discriminate: 8 bank-header regions also yield three
chunks. slb.rs already predicted this in its own doc comment -- the header
signature has "zero false positives on the 7993 mid-bank windows, where the
leading region IS real" -- and a voice region is a mid-bank window by
construction.
Also tested the obvious defence of dropping the leading chunk, that it is the
predecessor cue s audio: 0 of 17 leading spans lie inside any other resolved
region, 0.0 percent on every one. The test finds overlaps where they exist (16
overlapping pairs among the regions, 60 exactly-adjacent boundaries, 73 of 78
bank-header regions starting where another ends), so the zero is not the
instrument.
Left open, with reach: the census covers movie-voice regions only, and the same
stream carries the in-mission VOICE_D_* cues, which are not enumerated -- the
leading bytes plausibly belong to one of those. Could not be settled by
listening: no XMA1 decoder in this container, and sylpheed-cli audio info
reports these chunks as 16 channels / 4310 Hz / 2-bit, which is visibly wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
98 lines
4.2 KiB
Rust
98 lines
4.2 KiB
Rust
//! What ARE the chunks a movie-voice region decodes to?
|
|
//!
|
|
//! The port reports a resolved voice region decoding to **three** chunks — two
|
|
//! of equal duration each spanning the whole movie, and a leading one that
|
|
//! "matches nothing" — and notes that this is the same 2+1 signature the BGM
|
|
//! banks showed before `bank_header_len` attributed the extra to a bank header.
|
|
//! Two different asset kinds with one signature is worth checking, because if
|
|
//! the same explanation applies then `bank_header_len` is incomplete, and if it
|
|
//! does not then the leading chunk is something we are discarding.
|
|
//!
|
|
//! `slb.rs`'s own doc comment already predicts the answer and disagrees with
|
|
//! "drop it": the header signature fires on 28 entries, all music banks, with
|
|
//! "zero false positives on the 7 993 mid-bank windows, WHERE THE LEADING
|
|
//! REGION IS REAL". A voice region is a mid-bank window by construction —
|
|
//! `resolve_movie_voice_region` starts it at the PREDECESSOR cue's trailer.
|
|
//!
|
|
//! cargo run -p sylpheed-formats --example voice_region_chunks -- <disc-dir>
|
|
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
|
use sylpheed_formats::slb::{self, VoiceLang};
|
|
|
|
fn main() {
|
|
let disc = std::env::args()
|
|
.nth(1)
|
|
.unwrap_or_else(|| std::env::var("SYLPHEED_DISC").expect("disc dir"));
|
|
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
|
|
|
// Disc-wide, not the four movies that motivated the question: every movie
|
|
// the manifest binds a voice to.
|
|
let movies: Vec<String> = {
|
|
use sylpheed_formats::movie_manifest;
|
|
let tpak = src.open_pak("dat/tables.pak").expect("tables.pak");
|
|
let manifest = tpak
|
|
.entries()
|
|
.iter()
|
|
.find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b)))
|
|
.expect("movie manifest");
|
|
movie_manifest::parse(&manifest)
|
|
.into_iter()
|
|
.map(|m| m.movie)
|
|
.collect()
|
|
};
|
|
println!("{} movies in the manifest\n", movies.len());
|
|
let mut census: std::collections::BTreeMap<usize, usize> = Default::default();
|
|
let (mut with_header, mut with_leading, mut no_leading) = (0, 0, 0);
|
|
for movie in movies.iter().map(|s| s.as_str()) {
|
|
let Some((start, end)) = media::resolve_movie_voice_region(&src, movie, VoiceLang::English)
|
|
else {
|
|
continue;
|
|
};
|
|
let len = end - start;
|
|
let Ok(bytes) = src.read_segment_range("dat/sound", start, len as usize) else {
|
|
println!("{movie:8} region {start}..{end} unreadable");
|
|
continue;
|
|
};
|
|
// Where does the first RIFF sit? Everything before it is the leading
|
|
// headerless packet region.
|
|
let first_riff = bytes
|
|
.windows(4)
|
|
.position(|w| w == b"RIFF")
|
|
.map(|p| p as i64)
|
|
.unwrap_or(-1);
|
|
let hdr = slb::bank_header_len(&bytes);
|
|
let riffs = slb::to_xma_riffs(&bytes);
|
|
let kind = if first_riff <= 0 {
|
|
no_leading += 1;
|
|
"no leading region".to_string()
|
|
} else if hdr == Some(first_riff as usize) {
|
|
with_header += 1;
|
|
format!("BANK HEADER ({first_riff} B = {} packets exactly)", first_riff / 2048)
|
|
} else {
|
|
with_leading += 1;
|
|
*census.entry(first_riff as usize % 2048).or_default() += 1;
|
|
format!(
|
|
"leading STREAM ({first_riff} B = {} packets + {} B)",
|
|
first_riff / 2048,
|
|
first_riff % 2048
|
|
)
|
|
};
|
|
println!(
|
|
"{movie:10} {start:12}..{end:12} {len:9} B chunks {} {kind}",
|
|
riffs.len()
|
|
);
|
|
if let Ok(dir) = std::env::var("VOICE_CHUNK_DUMP") {
|
|
for (i, r) in riffs.iter().enumerate() {
|
|
let _ = std::fs::write(format!("{dir}/{movie}-chunk{i}.wav"), r);
|
|
}
|
|
}
|
|
}
|
|
println!(
|
|
"\n{with_header} region(s) open with a BANK HEADER (bank_header_len fires)\n\
|
|
{with_leading} open with a leading STREAM\n{no_leading} start at a RIFF"
|
|
);
|
|
println!("leading-stream length mod 2048, i.e. the derived data offset:");
|
|
for (rem, n) in &census {
|
|
println!(" {rem:5} B x{n}");
|
|
}
|
|
}
|