re: the voice decoder discards up to 87% of a bank — "multi-subwave" refuted

The record table gives a DIRECT binding hokyu_DS_s13A -> VOICE_D_452, where
the corpus records the movie as unbound and movie_manifest_disc.rs asserts
None, citing an in-game verdict that this exact value was "the wrong
recording". That is the only place on the disc where a runtime observation
disagrees with the record table, so it was worth settling.

First, shape: these banks are SHARED. Five slots bind VOICE_D_452, five bind
451, four 450, four 453, three 454 -- 21 hokyu slots over five banks, and the
movies repeat too. Generic resupply cutscenes, not per-stage recordings.

The recorded explanation for 453 decoding to 0.14 s and 454 to 0.43 s was
that the banks are "likely multi-subwave / not cleanly sliced". Refuted: the
count of RIFF magics EQUALS the number of sub-waves recovered in all five
banks, and the last data chunk ends exactly at EOF in four of them. Nothing
between or after sub-waves is being missed.

The real defect: slb::to_xma_riffs finds audio by searching for the RIFF
magic, and a large region PRECEDES it. 87% of VOICE_D_453 and 85% of
VOICE_D_454 sit in front of the first RIFF -- 21-27% zero over 256 distinct
byte values, i.e. content, not padding. VOICE_D_451 is the control: its
leading region is 100% zero, 1 distinct value, real padding.

So the in-game verdict listened to a decode that had discarded most of the
bank, for exactly this bank class. It is evidence about the decoder, not
about the mapping. Note also that what was rejected was a value INFERRED
from a shared demo id; the record table supplies the same value as a stored
field, and only the inference was ever tested.

This does NOT establish the binding is right -- it removes the only recorded
evidence against it. What the leading region actually holds is undecoded, and
confirming the binding needs a human listening.

Artifact: examples/voice_bank_shape.rs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-25 23:03:33 +00:00
parent 75f0664bfe
commit 49c00e0955
4 changed files with 196 additions and 3 deletions

View File

@@ -0,0 +1,76 @@
//! 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();
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;
}
}
}