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:
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user