slb: read Channels instead of assuming mono -- and retract the TCAF_608 conclusion

I read the seek chunk's packet count big-endian; it is little-endian at seek+12,
with size == 8 + 4*count. And a seek sits immediately AFTER its own data, so an
entry's first seek usually belongs to the PREVIOUS bank (implied start -25232
for D_452, -145988 for TCAF_608). I was comparing an entry's first seek against
its first data -- different waves by construction, which is why no reading lined
up.

With that fixed, the declared sizes are honest: every RIFF-bearing entry on the
disc has seek magic at exactly data_at + declared_size with count*2048 ==
declared. 7620/7620, zero failures.

VOICE_TCAF_608 is not truncated. Its Channels is 2 and I decoded it as mono;
read as stereo it gives 6520176 bytes = 33.96 s, agreeing with both length
signals in the bank (33.88 s from cumulative samples, 33.97 s from
PsuedoBytesPerSec). 170 of 8021 banks (2.12%) are stereo -- exactly the rate of
my 1-in-60 outlier.

This is the mono/stereo trap already documented on this very page, met from the
other direction: I had written 'at two channels every bank yields one frame' and
then spent several passes blaming missing data for a one-frame decode.

Code fix: to_xma_riffs built the leading segment with a hard-wired mono fmt. It
now reads Channels from the bank's first RIFF. 7 disc tests pass.
This commit is contained in:
Sylpheed RE agent
2026-08-26 05:27:42 +00:00
parent f1f701daf7
commit aa84aaf53f
2 changed files with 85 additions and 1 deletions

View File

@@ -131,6 +131,18 @@ fn parse_voice_clip(name: &str) -> VoiceClip {
/// that yields the full track for segment banks while the clamp drops the
/// duplicate takes for alternate-take banks. (Dynamic RE via Canary file-I/O
/// tracing confirmed the movie→voice binding; this fixes the *decode* of `RT*`.)
/// A bank's channel count, read from its first `RIFF` sub-wave.
///
/// `XMASTREAMFORMAT.Channels` sits at `RIFF + 49`. **2.12 % of banks are stereo**
/// (170 of 8 021), and decoding one of those as mono yields a single frame and
/// stops — the same signature already recorded for the leading segment. So the
/// channel count has to be read, not assumed. Returns `None` when there is no
/// `RIFF` to read it from.
fn riff_channels(slb: &[u8]) -> Option<u8> {
let ri = find(slb, b"RIFF", 0)?;
slb.get(ri + 49).copied().filter(|c| *c == 1 || *c == 2)
}
/// The four data offsets that occur on the disc, in ascending order.
///
/// Measured over all 7 358 banks whose offset is *known* (they carry a `RIFF`,
@@ -280,7 +292,12 @@ pub fn to_xma_riffs(slb: &[u8]) -> Vec<Vec<u8>> {
if ri > start {
if let Some(data) = slb.get(start..ri) {
if data.iter().any(|b| *b != 0) {
out.push(build_riff(&synth_xma1_fmt(1, 0, 48000), data));
// Channels come from the bank's own `fmt `, not a constant:
// 170 of 8 021 banks are stereo and decode to one frame if
// forced to mono.
let ch = riff_channels(slb).unwrap_or(1);
let mask = if ch == 2 { 2 } else { 0 };
out.push(build_riff(&synth_xma1_fmt(ch, mask, 48000), data));
}
}
}

View File

@@ -436,3 +436,70 @@ little-endian, three header words precede the table, and the entry count is
*not* the packet count. `auto/slb-loader` reports chaining `seek` packet counts
successfully across consecutive entries — whatever field it used is not one of
the two tried here, and reconciling the two readings is the cheapest way in.
## ❌❌ `VOICE_TCAF_608` is NOT truncated — I was decoding it as mono
**2026-08-26, resolving the disagreement above in the other branch's favour.**
Everything I concluded about this bank was an artefact of a wrong `fmt ` chunk,
and the declared sizes are honest after all.
**The `seek` chunk, read correctly.** I read the packet count big-endian. It is
**little-endian**, and the layout is:
+0 'seek'
+4 u32 LE chunk size (always 8 + 4*packets)
+8 u32 LE stream count (always 1) <-- my "0x01000000" was LE 1 here
+12 u32 LE PACKET COUNT <-- my "<varies>" read big-endian
+16 packets x u32 LE cumulative decoded sample totals
Verified: `size == 8 + 4*count` on every bank checked. My "entries 3" reading
matched `VOICE_D_452` by coincidence; the real relation is `size/4 2`.
**And a `seek` sits immediately *after* its own data**, so the *first* `seek` in
an entry usually belongs to the *previous* bank — its implied start is negative
(25 232 for D_452, 145 988 for TCAF_608). I was comparing an entry's first
`seek` against its first `data`, which are different waves by construction. That
is why no reading could line up.
**The declared sizes are honest — 7 620 / 7 620.** For every `RIFF`-bearing entry
on the disc there is `seek` magic at exactly `data_at + declared_size`, and its
packet count × 2048 equals the declared size. **Zero failures.** For TCAF_608:
probe at 663 207 356 → `seek`, count **371**, 371 × 2048 = **759 808** = declared.
**Why it decoded to 896 bytes:** its `Channels` is **2**. I decoded it as mono.
Reading it as stereo gives **6 520 176 bytes = 33.96 s** — and two independent
length signals in the bank agree: the last cumulative sample 1 626 112 / 48 000 =
33.88 s, and 759 808 / `PsuedoBytesPerSec` = 33.97 s. The audio was there the
whole time.
**170 of 8 021 banks (2.12 %) are stereo**`Channels` is the byte at
`RIFF + 49`. That is exactly the 1-in-60 rate of my "gains nothing" outlier.
### What this retracts
* ❌ "`VOICE_TCAF_608`'s missing bytes cannot be recovered from the stream" —
**wrong**, nothing was missing.
* ❌ "The declared `data` size exceeds the TOC window for 5 296 banks" stands as
a fact about the *window*, but my framing of it as a problem is withdrawn: the
window is simply not the wave boundary, and `data_at + declared_size` is.
* ❌ My `seek`-layout write-up above (entry count, "failed readings") was wrong
in its endianness and in its pairing assumption. Left in place as a record.
This is the mono/stereo trap **already documented on this page** — "at two
channels every bank yields exactly 1792 bytes, one frame" — met from the other
direction. Having written that down, I then spent several passes attributing a
one-frame decode to missing data instead of checking the channel count.
### Code fixed
`to_xma_riffs` built the leading segment with a hard-wired mono `fmt `. It now
reads `Channels` from the bank's own first `RIFF` (`riff_channels`), falling
back to mono only when there is no `RIFF` to read. 7 disc tests pass.
### The decoder-independent boundary, for the record
bytes = u32 LE at seek+12 x 2048 (== the `data` chunk size)
validate = 'seek' magic at data_at + declared_size (7 620/7 620)
samples = the LAST u32 LE entry in the seek table
channels = byte at RIFF + 49 <-- read it, never assume