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
125 lines
5.2 KiB
Rust
125 lines
5.2 KiB
Rust
//! Real-disc tests for media assembly. Skipped without `SYLPHEED_DISC`.
|
|
//!
|
|
//! This logic used to live in the Bevy viewer, where it had no test at all. It
|
|
//! is the trickiest reading on the disc — a cutscene's voice is a byte region of
|
|
//! a continuous stream, not the bank its name points at — so it gets pinned here
|
|
//! before anything else is built on top of it.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
|
use sylpheed_formats::slb::VoiceLang;
|
|
|
|
fn disc() -> Option<DirectorySource> {
|
|
let p = PathBuf::from(std::env::var("SYLPHEED_DISC").ok()?);
|
|
p.join("dat").is_dir().then(|| DirectorySource::new(p))
|
|
}
|
|
|
|
/// A segment-spanning read returns the same bytes as slicing the whole archive.
|
|
///
|
|
/// The control that matters: `sound.pak`'s data is five segments, so a TOC
|
|
/// offset late in the archive addresses a position no single file has. If the
|
|
/// walk were off by a segment this would return plausible-looking wrong bytes
|
|
/// rather than fail, which is exactly why it is asserted against the archive's
|
|
/// own read rather than against a length.
|
|
#[test]
|
|
fn segment_range_matches_the_archive_read() {
|
|
let Some(src) = disc() else {
|
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
|
return;
|
|
};
|
|
let name = "BGM_020.slb";
|
|
let hash = sylpheed_formats::hash::name_hash(name);
|
|
let via_range = media::read_sound_bank(&src, hash).expect("segment range read");
|
|
|
|
let toc = src.read_file("dat/sound.pak").unwrap();
|
|
let entries = sylpheed_formats::PakArchive::parse_toc(&toc).unwrap();
|
|
let e = entries
|
|
.iter()
|
|
.find(|e| e.name_hash == hash)
|
|
.expect("BGM_020 in the TOC");
|
|
assert_eq!(via_range.len(), e.comp_size as usize);
|
|
|
|
// And it decodes, which a misaligned read would not do.
|
|
let riffs = media::sound_bank_riffs(&src, name).expect("riffs");
|
|
assert!(!riffs.is_empty(), "no sub-waves recovered");
|
|
}
|
|
|
|
/// A movie's voice resolves to a byte region, and the region is sane.
|
|
///
|
|
/// `RT01A` is one of the cutscenes whose voice spans more than one `.slb`
|
|
/// chunk — the case that motivated regions over per-bank reads in the first
|
|
/// place. The assertions are deliberately about *shape* (ordered, non-empty,
|
|
/// smaller than one bank) rather than exact offsets, because the offsets are
|
|
/// disc facts we have no independent oracle for here; a regression that
|
|
/// reversed or emptied the region would still be caught.
|
|
#[test]
|
|
fn movie_voice_resolves_to_a_region_that_decodes() {
|
|
let Some(src) = disc() else {
|
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
|
return;
|
|
};
|
|
let (start, end) = media::resolve_movie_voice_region(&src, "RT01A", VoiceLang::English)
|
|
.expect("RT01A has a bound voice track");
|
|
assert!(start < end, "region is inverted: {start}..{end}");
|
|
assert!(end - start > 4096, "region is implausibly small");
|
|
assert!(end - start < 1_500_000, "region spans more than one bank");
|
|
|
|
let riffs = media::voice_region_riffs(&src, start, end).expect("region riffs");
|
|
assert!(!riffs.is_empty(), "region decoded to no audio");
|
|
}
|
|
|
|
/// An unbound movie stays unvoiced rather than borrowing a neighbour's clip.
|
|
///
|
|
/// This is a *negative* the corpus paid for: extending resolution to unbound
|
|
/// resupply movies by shared demo line played the WRONG recording. The guard
|
|
/// keeps that door shut.
|
|
#[test]
|
|
fn manifest_binding_is_the_only_route() {
|
|
let Some(src) = disc() else {
|
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
|
return;
|
|
};
|
|
// A movie the manifest does not bind must resolve to nothing, not to a guess.
|
|
assert_eq!(
|
|
media::resolve_movie_voice_clip(&src, "no_such_movie_xyz", VoiceLang::English),
|
|
None
|
|
);
|
|
}
|
|
|
|
/// The resolved `ADV` voice region must contain **all three** streams the running
|
|
/// decoder decodes — not a truncated first one.
|
|
///
|
|
/// Ground truth is the emulator, not this crate: booting with `--xma_param_probe`
|
|
/// reports three XMA contexts with `byte_size` 1 294 336 / 1 118 208 / 1 171 456
|
|
/// (`docs/re/structures/voice-three-streams-are-concurrent.md`). Until 2026-08-30
|
|
/// the resolver's start filter capped a region at 1.5 MB, `ADV`'s span is 3.6 MB,
|
|
/// so the start fell back to `anchor` — a TOC offset, 238 packets into the first
|
|
/// stream — and this returned 806 912 for the first chunk.
|
|
///
|
|
/// This is a regression test against an EXTERNAL measurement, which is the only
|
|
/// kind that can catch the class of bug it was written for: every internal check
|
|
/// passed happily while a third of a stream was missing.
|
|
#[test]
|
|
fn adv_voice_region_holds_all_three_decoded_streams() {
|
|
let Some(src) = disc() else {
|
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
|
return;
|
|
};
|
|
let (start, end) = media::resolve_movie_voice_region(&src, "ADV", VoiceLang::English)
|
|
.expect("ADV voice region");
|
|
let bytes = src
|
|
.read_segment_range("dat/sound", start, (end - start) as usize)
|
|
.expect("region bytes");
|
|
let sizes: Vec<usize> = sylpheed_formats::slb::to_xma_riffs(&bytes)
|
|
.iter()
|
|
.map(|r| r.len() - 60)
|
|
.collect();
|
|
assert_eq!(
|
|
sizes,
|
|
vec![1_294_336, 1_118_208, 1_171_456],
|
|
"the region must reproduce the RUNNING DECODER's byte_sizes; \
|
|
a first chunk of 806912 means the start filter has come back"
|
|
);
|
|
}
|