Files
Sylpheed/crates/sylpheed-formats/examples/voice_bank_shape.rs
Sylpheed RE agent 6fa6564be8 re: the .slb leading region is 1392+n*2048 — and my fix for it is withdrawn
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
2026-08-25 23:30:42 +00:00

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;
}
}
}