Files
Sylpheed/crates/sylpheed-formats/examples/voice_region_start_why.rs
sylph-decoder 3dbfa320ae formats: drop the 1.5 MB cap that truncated 17 voice regions' first stream
The cause, and the fix, with a disc-wide check.

resolve_movie_voice_region picks start = the predecessor cue's trailer, then
filtered it with 'end - s < 1_500_000' -- 'only within one bank'. ADV's
predecessor sits 3 618 816 B before end, so the filter rejected it and start fell
back to anchor, which is a TOC offset and not a stream boundary. That explains
the shape of the defect exactly: it strikes regions larger than 1.5 MB, which is
why the three-stream multichannel regions are hit and single-stream ones never
are. 17 of 95 resolving movies took the fallback.

ADV's predecessor trailer at 433 425 776 plus 17 040 B of descriptor and padding
is 433 442 816 -- the -238-packet start measured against the decoder, to the byte.

Dropping the cap: unchanged 78, fixed cleanly 17, changed in any other way ZERO.
In all 17 the only difference is a larger first chunk with every later chunk
byte-identical, which is what a corrected start looks like and what pulling in a
neighbouring asset does not.

Regression test pinned to the RUNNING DECODER's byte_sizes rather than to this
crate's own output. That is the point of it: every internal check passed happily
while a third of a stream was missing, so only an external number could have
caught this class of bug.

sylpheed-formats: 136 tests pass, 0 fail (the one still running at commit time is
an unrelated long mesh test).

Exact clips for the other 16 are not independently verified -- the sweep is
strong but ADV is the only one with a decoder measurement behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 08:55:47 +00:00

71 lines
3.8 KiB
Rust

//! WHY does `resolve_movie_voice_region` start inside the first stream?
//!
//! [`voice-region-starts-late.md`] establishes that it does — 238 packets late for
//! `ADV`, 8 of 10 multichannel regions disc-wide — but not why, and a fix guessed
//! from one movie would be worse than a documented defect. This reproduces the
//! resolver's own steps and prints each candidate, so the failing branch is visible
//! rather than inferred.
//!
//! The suspicion the code itself raises: the start is filtered by
//! `end - s < 1_500_000` — "only within one bank" — and `ADV`'s region has to span
//! **3.6 MB**. If that filter rejects the real predecessor, `start` silently falls
//! back to `anchor`, which is a TOC offset and not a stream boundary at all.
//!
//! cargo run -p sylpheed-formats --example voice_region_start_why
use sylpheed_formats::hash::name_hash;
use sylpheed_formats::media::{DirectorySource, DiscSource};
use sylpheed_formats::pak::PakArchive;
use sylpheed_formats::slb::VoiceLang;
use sylpheed_formats::{movie_manifest, movie_voice};
fn main() {
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
let code = VoiceLang::English.code_pub();
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("manifest");
let marker = format!("{code}\\Movie\\VOICE_ADV.slb");
let registry = tpak.entries().iter().find_map(|e| {
tpak.read(e).ok().filter(|b| b.windows(marker.len()).any(|w| w == marker.as_bytes()))
}).expect("registry");
let ids = movie_voice::registry_voice_ids(&registry);
let stoc = src.read_file("dat/sound.pak").expect("sound.pak");
let entries = PakArchive::parse_toc(&stoc).expect("toc");
println!("{:10} {:>6} {:>12} {:>12} {:>12} {:>10} {:>9} {}",
"movie","id","anchor","pred(id-1)","pred(before)","span","chosen","note");
for m in movie_manifest::parse(&manifest) {
let movie = m.movie;
let Some(token) = movie_manifest::voice_token(&manifest, &movie) else { continue };
let Some(&id) = ids.get(&token) else { continue };
let Some(anchor) = ["Movie","etc","Voice"].iter().find_map(|dir| {
let h = name_hash(&format!("{code}\\{dir}\\{token}.slb"));
entries.binary_search_by_key(&h, |e| e.name_hash).ok().map(|i| entries[i].offset as u64)
}) else { continue };
let win_start = anchor.saturating_sub(2*1024*1024) & !3;
let Ok(window) = src.read_segment_range("dat/sound", win_start, 8*1024*1024) else { continue };
let Some(end_local) = movie_voice::find_descriptor(&window, id) else { continue };
let end = win_start + end_local as u64;
let p1 = movie_voice::find_descriptor(&window, id.wrapping_sub(1)).map(|o| win_start + o as u64);
let pb = movie_voice::find_descriptor_before(&window, end_local).map(|o| win_start + o as u64);
let cand = p1.or(pb);
// the resolver's own filter
let kept = cand.filter(|&s| s < end && end - s < 1_500_000);
let chosen = kept.unwrap_or(anchor);
let span = cand.map(|s| end.saturating_sub(s)).unwrap_or(0);
let note = match (cand, kept) {
(Some(_), None) => "REJECTED by the 1.5 MB filter -> fell back to anchor",
(Some(_), Some(_)) => "predecessor kept",
(None, _) => "no predecessor found -> anchor",
};
println!("{movie:10} {id:>6} {anchor:>12} {:>12} {:>12} {span:>10} {:>9} {note}",
p1.map(|v| v.to_string()).unwrap_or("-".into()),
pb.map(|v| v.to_string()).unwrap_or("-".into()),
if chosen == anchor { "anchor" } else { "pred" });
}
}