The structure is exact. In all five resupply banks the first RIFF sits at HEADERLESS_DATA_OFFSET + n*2048, where 1392 is a constant this crate already had and 2048 is the XMA1 packet size: n = 8, 1, 7, 22, 29. No free parameter to tune, and the raw bytes agree -- high entropy from offset 0, then a zero run immediately before the RIFF. VOICE_D_451 is the control, its single packet being all zeros. So I made the obvious fix, emitting that region as a sub-wave, and then withdrew it on two measurements: * It does not recover audio. Coverage went 5.4% -> 89.9% for VOICE_D_453, but the emitted stream decodes through FFmpeg to 1792 PCM bytes -- silence -- while the RIFF sub-waves from the same banks decode to 150-270 KB. Byte coverage was the wrong success metric and it looked like progress. * It is not narrow. The rule matches 1524 of the 8021 RIFF-bearing entries in sound.pak, including RT* movie banks that decode correctly today. Landing it would have risked a wide regression in order to not-fix five banks. to_xma_riffs is back to its previous behaviour, verified by re-measuring: coverage is 5.4% / 9.7% again. The refuted attempt is recorded in the code beside the branch it would have changed, so the next person does not re-derive the arithmetic and re-make the change. XMA1_PACKET is kept as a named constant because the blast-radius scan uses it. Artifacts: examples/voice_bank_shape.rs (structure), voice_bank_dump.rs (sub-waves for decoding), slb_hybrid_scan.rs (the 1524 count). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
86 lines
4.1 KiB
Rust
86 lines
4.1 KiB
Rust
//! How many sub-waves does each resupply voice bank hold?
|
|
//!
|
|
//! The corpus records `VOICE_D_452` as the binding the game rejected in-game
|
|
//! ("wrong recording"), and separately notes that `VOICE_D_453`/`454` decode to
|
|
//! 0.14 s / 0.43 s — "far too short for the spoken line". Both observations are
|
|
//! explained if these banks are multi-sub-wave and the extractor plays only the
|
|
//! first. This prints the shape so that stops being a guess.
|
|
use sylpheed_formats::{slb, PakArchive};
|
|
|
|
fn main() {
|
|
let disc = std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC");
|
|
let snd = PakArchive::open(format!("{disc}/dat/sound.pak")).expect("sound.pak");
|
|
println!(
|
|
"{:<14} {:>7} {:>6} {:>6} {:>7} sub-wave data sizes",
|
|
"bank", "bytes", "RIFFs", "waves", "cover"
|
|
);
|
|
for n in 450..=454 {
|
|
for dir in ["etc", "Voice", "Movie"] {
|
|
let path = format!("eng\\{dir}\\VOICE_D_{n}.slb");
|
|
let Some(entry) = snd.find_by_name(&path) else { continue };
|
|
let bytes = snd.read(entry).expect("read");
|
|
let riffs = slb::to_xma_riffs(&bytes);
|
|
let sizes: Vec<usize> = riffs.iter().map(|r| r.len()).collect();
|
|
// How many RIFF magics does the bank actually contain, versus how
|
|
// many sub-waves the walker recovered? A gap means the walk stops
|
|
// early, and the missing bytes are the missing audio.
|
|
let magics = bytes.windows(4).filter(|w| *w == b"RIFF").count();
|
|
let covered: usize = sizes.iter().sum();
|
|
// What are the UNCOVERED bytes? If the tail past the last data
|
|
// chunk is all zero it is padding and the short duration is real;
|
|
// if it is high-entropy it is audio the parse is throwing away.
|
|
let last = bytes
|
|
.windows(4)
|
|
.rposition(|w| w == b"data")
|
|
.map(|i| {
|
|
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
|
|
(i + 8 + sz).min(bytes.len())
|
|
})
|
|
.unwrap_or(0);
|
|
// Where does the RIFF structure START? If it begins far into the
|
|
// file, the uncovered bytes are a leading region the parse skips,
|
|
// not a missed sub-wave.
|
|
let first_riff = bytes.windows(4).position(|w| w == b"RIFF").unwrap_or(0);
|
|
let datas = bytes.windows(4).filter(|w| *w == b"data").count();
|
|
// Is the leading region padding, or content? Padding is nearly all
|
|
// zero and uses few distinct byte values.
|
|
let head = &bytes[..first_riff];
|
|
let head_zero = head.iter().filter(|b| **b == 0).count();
|
|
let head_distinct = {
|
|
let mut seen = [false; 256];
|
|
for b in head {
|
|
seen[*b as usize] = true;
|
|
}
|
|
seen.iter().filter(|s| **s).count()
|
|
};
|
|
let tail = &bytes[last..];
|
|
let zeros = tail.iter().filter(|b| **b == 0).count();
|
|
// Dump the first bytes of the leading region so its structure is
|
|
// visible rather than guessed at.
|
|
let hex: String = head
|
|
.iter()
|
|
.take(48)
|
|
.map(|b| format!("{b:02x}"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
println!(" head[0..48] {hex}");
|
|
println!(
|
|
"{:<14} {:>7} {:>6} {:>6} {:>6.1}% 1st RIFF @{:>6} data chunks {} head {:>5.1}% zero/{:>3} distinct tail {:>5} B ({:>5.1}% zero) {:?}",
|
|
format!("VOICE_D_{n}"),
|
|
bytes.len(),
|
|
magics,
|
|
riffs.len(),
|
|
100.0 * covered as f64 / bytes.len() as f64,
|
|
first_riff,
|
|
datas,
|
|
if head.is_empty() { 0.0 } else { 100.0 * head_zero as f64 / head.len() as f64 },
|
|
head_distinct,
|
|
tail.len(),
|
|
if tail.is_empty() { 0.0 } else { 100.0 * zeros as f64 / tail.len() as f64 },
|
|
sizes
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|