Some checks failed
The trickiest reading on the disc lived in the Bevy viewer: resolving a cutscene's voice to a continuous byte REGION of the sound stream, because the movie voices are one XMA stream chunked into VOICE_*.slb entries whose boundaries do not match the cues -- a cue routinely spans two chunks, so a .slb need not hold the track its name claims. That put the logic most likely to be re-derived incorrectly in the crate least likely to be reused. The Godot port's exporter needs the same answers, and there must be one implementation of them. New `sylpheed_formats::media` owns every case where the bytes of one playable thing are not one archive entry: segment-spanning reads, multi-sub-wave banks, and the voice-region resolution. Callers supply bytes through a `DiscSource` trait, so the viewer keeps its ISO/directory abstraction and a headless consumer gets `DirectorySource` for free. The seam is deliberate: this module returns XMA RIFFs, not PCM. Decoding means shelling out to FFmpeg, which is native-only and a policy decision for the consumer -- everything up to "here are the bytes that belong together" is disc knowledge, everything after it is a codec choice. The four moved functions were previously untested; `tests/media_disc.rs` now pins them, including the negative the corpus paid for -- an unbound movie must stay unvoiced rather than borrow a neighbour's clip, which was tried and played the WRONG recording. The algorithm is unchanged, moved verbatim (same window sizes, same fallbacks). The new disc tests pass; the broader audio suite was not re-run in this pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
3.6 KiB
Rust
89 lines
3.6 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
|
|
);
|
|
}
|