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:
76
crates/sylpheed-formats/examples/voice_bank_shape.rs
Normal file
76
crates/sylpheed-formats/examples/voice_bank_shape.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user