From aa84aaf53f92c327d8620ebdfd722c857a6122eb Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 05:27:42 +0000 Subject: [PATCH] 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. --- crates/sylpheed-formats/src/slb.rs | 19 +++++++- docs/re/structures/slb-data-offset.md | 67 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/crates/sylpheed-formats/src/slb.rs b/crates/sylpheed-formats/src/slb.rs index ec2d8ee2..9a16770a 100644 --- a/crates/sylpheed-formats/src/slb.rs +++ b/crates/sylpheed-formats/src/slb.rs @@ -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 { + 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> { 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)); } } } diff --git a/docs/re/structures/slb-data-offset.md b/docs/re/structures/slb-data-offset.md index 0f5e840d..ff38e9a0 100644 --- a/docs/re/structures/slb-data-offset.md +++ b/docs/re/structures/slb-data-offset.md @@ -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 "" 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