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 /// 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 /// 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*`.) /// 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. /// 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`, /// 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 ri > start {
if let Some(data) = slb.get(start..ri) { if let Some(data) = slb.get(start..ri) {
if data.iter().any(|b| *b != 0) { 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 *not* the packet count. `auto/slb-loader` reports chaining `seek` packet counts
successfully across consecutive entries — whatever field it used is not one of 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. 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