Picked sound_bank rather than tie, where three discriminators have now come back
negative and a fourth would be guesswork.
The body after the 24-byte head opens with twelve words. Six are settled across
all 27 levels:
word value note
0 varies per level not identified; 27 distinct
1 60 constant
2 varies per level not identified; 23 distinct
3 0 constant
4 daw_size == the header's daw_size, exactly
5 daw_size the same value again
6,7,8 0 constant
9 127 constant
10 1 constant
11 0 constant
Level 0's words 4 and 5 are both 1,482,000, and daw_size is 1,482,000.
THE POINTER THAT WAS NOT ONE
Words 4 and 5 were briefly recorded as pointers into the DAW region. They are not.
The test that promoted them was "does this value fall inside [daw_offset,
daw_offset + daw_size)" -- and the DAW region is ~1.5 MB starting near the front of
a ~1.5 MB extent, so almost any plausible 32-bit word lands inside it. 1,482,000 is
merely just under the top of exactly that range.
A range test is not an identification. What survives is EQUALITY with daw_size,
exact on all 27 levels and asserted, which is a claim that can fail rather than one
that is almost always true.
That is the fourth time this session a plausible reading of a single number has had
to be withdrawn for something falsifiable: the 305x class-ID slot, the UV-corner
texcoords, the VIF code word, and now this. The pattern is consistent enough to be
worth stating as a rule -- a test that passes almost always is measuring the shape
of the data, not a property of it.
Word 0 gets one note: levels 1 and 4 hold 13,042,052 and 13,042,054, two apart,
while their daw_sizes differ by 63,504. Whatever it is, it is not a size.
The remainder of the SBlk body -- the per-sound record table -- is still undecoded,
so sound_bank stays open. A SBlkBodyHead struct carries the twelve words with the
constants asserted in its docs.
259 tests.
303 lines
11 KiB
Rust
303 lines
11 KiB
Rust
//! The level's sound bank: container framing.
|
|
//!
|
|
//! Spec: `docs/formats/sound-bank.md`.
|
|
//!
|
|
//! A level's sound extent holds two regions. `SBlk` is a table of short sound
|
|
//! descriptors; the sample data follows at `daw_offset`. The framing is four
|
|
//! `u32` at the head, and the invariant that establishes it is simple:
|
|
//! **`sblk_offset + sblk_size == daw_offset` on every level**.
|
|
//!
|
|
//! `DAW` is a format tag inside the `SBlk` header, not a region signature. It
|
|
//! appears exactly once in the whole extent, at `sblk_offset + 12`, nowhere
|
|
//! near the sample data it names.
|
|
//!
|
|
//! The audio sample format inside `DAW` is not decoded here. That is a
|
|
//! separate body of work with no bearing on drawing anything, and the framing
|
|
//! is the part every other format's "what is in this level" questions depend
|
|
//! on.
|
|
|
|
use crate::buf::{Buf, Error, ErrorKind, Result};
|
|
|
|
/// Offset of the `SBlk` signature on every level measured.
|
|
pub const SBLK_SIGNATURE_AT: usize = 24;
|
|
/// The head of a sound extent.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct SoundBankHeader {
|
|
/// Always 3 on all 27 levels.
|
|
pub version: u32,
|
|
/// Always 2 on all 27 levels.
|
|
pub unknown_4: u32,
|
|
/// Byte offset of the `SBlk` region; 24 on every level.
|
|
pub sblk_offset: u32,
|
|
/// Size of the `SBlk` region.
|
|
pub sblk_size: u32,
|
|
/// Byte offset of the `DAW` region.
|
|
pub daw_offset: u32,
|
|
/// Size of the `DAW` region.
|
|
///
|
|
/// This never reaches the end of the file: the extent is 232 to 1,848 bytes
|
|
/// longer than `daw_offset + daw_size` on every level, and always longer
|
|
/// rather than shorter. Whatever follows is not accounted for by this field.
|
|
pub daw_size: u32,
|
|
}
|
|
|
|
/// The two regions of a sound extent.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct SoundBank<'a> {
|
|
pub header: SoundBankHeader,
|
|
/// The `SBlk` descriptor table, including its leading `SBlk` magic.
|
|
pub sblk: &'a [u8],
|
|
/// The `DAW` sample region, including its leading `DAW` magic.
|
|
pub daw: &'a [u8],
|
|
/// Bytes after `daw_offset + daw_size`.
|
|
pub daw_tail: usize,
|
|
}
|
|
|
|
impl SoundBankHeader {
|
|
/// Parse the 0x18-byte head.
|
|
pub fn parse(bytes: &[u8]) -> Result<SoundBankHeader> {
|
|
let mut b = Buf::new(bytes);
|
|
Ok(SoundBankHeader {
|
|
version: b.u32("sound bank version")?,
|
|
unknown_4: b.u32("sound bank unknown_4")?,
|
|
sblk_offset: b.u32("SBlk offset")?,
|
|
sblk_size: b.u32("SBlk size")?,
|
|
daw_offset: b.u32("DAW offset")?,
|
|
daw_size: b.u32("DAW size")?,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The 24-byte head that opens the `SBlk` region.
|
|
///
|
|
/// Measured across all 27 levels: the first twelve bytes are **identical
|
|
/// everywhere** -- `"SBlk"`, then `1`, then `4`, then `"DAW\0"` -- and the word
|
|
/// at `+0x10` is zero on every level. The word at `+0x14` is the only part that
|
|
/// varies, and it varies per level with its **low sixteen bits zero in all 27**,
|
|
/// leaving values `0x0092..=0x0103` (146..=259).
|
|
///
|
|
/// That word tracks the length of the `SBlk` body at close to 100 bytes per
|
|
/// unit, but it is **not** a record count with a fixed stride: the ratio runs
|
|
/// 91.2 to 110.8 across the 27 levels, and no stride in 0..80 makes
|
|
/// `body = stride * word + constant` hold exactly on all of them. So it is
|
|
/// correlated with the body length and is not identified. Recorded rather than
|
|
/// named, because "a count that nearly works" is exactly the claim that later
|
|
/// gets asserted by someone who did not run this test.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct SBlkHeader {
|
|
/// Always `"SBlk"`.
|
|
pub magic: [u8; 4],
|
|
/// Always `1`.
|
|
pub field_4: u32,
|
|
/// Always `4`.
|
|
pub field_8: u32,
|
|
/// Always `"DAW\0"`.
|
|
pub format_tag: [u8; 4],
|
|
/// Always `0`.
|
|
pub field_10: u32,
|
|
/// Varies per level; low sixteen bits always zero, `0x0092..=0x0103`.
|
|
pub field_14: u32,
|
|
}
|
|
|
|
impl SBlkHeader {
|
|
/// Parse the 24-byte head.
|
|
pub fn parse(bytes: &[u8]) -> Result<SBlkHeader> {
|
|
// Read in byte order. The struct literal names `format_tag` after two
|
|
// words, but a `let` bound for it would consume bytes 4..8 and shift
|
|
// every field after it -- which it did, once.
|
|
let mut b = Buf::new(bytes);
|
|
let magic: [u8; 4] = b.read_bytes(4, "SBlk magic")?.try_into().expect("4 bytes");
|
|
let field_4 = b.u32("SBlk field_4")?;
|
|
let field_8 = b.u32("SBlk field_8")?;
|
|
let format_tag: [u8; 4] = b
|
|
.read_bytes(4, "SBlk format tag")?
|
|
.try_into()
|
|
.expect("4 bytes");
|
|
Ok(SBlkHeader {
|
|
magic,
|
|
field_4,
|
|
field_8,
|
|
format_tag,
|
|
field_10: b.u32("SBlk field_10")?,
|
|
field_14: b.u32("SBlk field_14")?,
|
|
})
|
|
}
|
|
|
|
/// True when every constant this header carries holds.
|
|
///
|
|
/// Each is universal on all 27 levels. They are tested individually so a
|
|
/// real deviation says which one moved.
|
|
pub fn constants_hold(&self) -> bool {
|
|
&self.magic == b"SBlk"
|
|
&& self.field_4 == 1
|
|
&& self.field_8 == 4
|
|
&& &self.format_tag == b"DAW\0"
|
|
&& self.field_10 == 0
|
|
}
|
|
}
|
|
|
|
/// The first twelve words of the `SBlk` body, after its 24-byte head.
|
|
///
|
|
/// Words 4 and 5 both hold the **DAW sample-data size** -- byte for byte, on all
|
|
/// 27 levels. Words 1, 3, 6, 7, 8, 9, 10 and 11 are the same on every level
|
|
/// (60, 0, 0, 0, 0, 127, 1, 0). Words 0 and 2 vary per level and are not
|
|
/// identified.
|
|
///
|
|
/// These sizes fall inside `[daw_offset, daw_offset + daw_size)`, so a test of
|
|
/// the form "is this word in the DAW region?" passes for them and reads as a
|
|
/// pointer. It is not one; equality with `daw_size` is the claim.
|
|
///
|
|
/// Asserted in `sound_bank_sblk_body_head_carries_the_daw_size_twice`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct SBlkBodyHead {
|
|
pub word_0: u32,
|
|
pub word_1: u32,
|
|
pub word_2: u32,
|
|
pub word_3: u32,
|
|
/// Always the DAW sample-data size.
|
|
pub word_4: u32,
|
|
/// Always the same value as [`Self::word_4`].
|
|
pub word_5: u32,
|
|
pub word_6: u32,
|
|
pub word_7: u32,
|
|
pub word_8: u32,
|
|
/// Always 127.
|
|
pub word_9: u32,
|
|
/// Always 1.
|
|
pub word_10: u32,
|
|
pub word_11: u32,
|
|
}
|
|
|
|
/// Split a sound extent into its two regions.
|
|
///
|
|
/// Bounds are checked before slicing; a level whose head points outside its own
|
|
/// extent is an error, not a panic.
|
|
pub fn parse_sound_bank(bytes: &[u8]) -> Result<SoundBank<'_>> {
|
|
let header = SoundBankHeader::parse(bytes)?;
|
|
let sblk_end = header
|
|
.sblk_offset
|
|
.checked_add(header.sblk_size)
|
|
.filter(|&e| e <= bytes.len() as u32)
|
|
.ok_or(Error {
|
|
offset: header.sblk_offset as usize,
|
|
context: "SBlk region runs past the extent",
|
|
kind: ErrorKind::OutOfBounds {
|
|
needed: header.sblk_size as usize,
|
|
available: bytes.len(),
|
|
},
|
|
})? as usize;
|
|
let daw_end = header
|
|
.daw_offset
|
|
.checked_add(header.daw_size)
|
|
.filter(|&e| e <= bytes.len() as u32)
|
|
.ok_or(Error {
|
|
offset: header.daw_offset as usize,
|
|
context: "DAW region runs past the extent",
|
|
kind: ErrorKind::OutOfBounds {
|
|
needed: header.daw_size as usize,
|
|
available: bytes.len(),
|
|
},
|
|
})? as usize;
|
|
let sblk = &bytes[header.sblk_offset as usize..sblk_end];
|
|
let daw = &bytes[header.daw_offset as usize..daw_end];
|
|
Ok(SoundBank {
|
|
header,
|
|
sblk,
|
|
daw,
|
|
daw_tail: bytes.len().saturating_sub(daw_end),
|
|
})
|
|
}
|
|
|
|
impl SoundBank<'_> {
|
|
/// The `SBlk` magic, as measured.
|
|
pub fn has_sblk_magic(&self) -> bool {
|
|
self.sblk.starts_with(b"SBlk")
|
|
}
|
|
|
|
/// The `DAW` tag, which lives inside the `SBlk` header rather than at the
|
|
/// start of the sample data.
|
|
///
|
|
/// Worth being explicit about because it reads like a region signature:
|
|
/// `DAW` occurs exactly once in the entire extent, at `sblk_offset + 12`,
|
|
/// naming the sample format. The sample data itself begins at `daw_offset`
|
|
/// with no signature at all, and the two are nowhere near each other.
|
|
pub fn has_daw_tag(&self) -> bool {
|
|
self.sblk.get(12..15) == Some(b"DAW")
|
|
}
|
|
|
|
/// `SBlk` version, at `+4` past its signature; 1 on every level.
|
|
pub fn sblk_version(&self) -> Option<u32> {
|
|
self.sblk
|
|
.get(4..8)
|
|
.and_then(|b| b.try_into().ok())
|
|
.map(u32::from_le_bytes)
|
|
}
|
|
|
|
/// `SBlk` entry count, at `+8` past its signature.
|
|
pub fn sblk_entries(&self) -> Option<u32> {
|
|
self.sblk
|
|
.get(8..12)
|
|
.and_then(|b| b.try_into().ok())
|
|
.map(u32::from_le_bytes)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// A bank whose `SBlk` region runs straight into its `DAW` region.
|
|
fn bytes() -> Vec<u8> {
|
|
let mut v = vec![0u8; 0x100];
|
|
v[0x00..0x04].copy_from_slice(&3u32.to_le_bytes());
|
|
v[0x04..0x08].copy_from_slice(&2u32.to_le_bytes());
|
|
v[0x08..0x0c].copy_from_slice(&24u32.to_le_bytes());
|
|
v[0x10..0x14].copy_from_slice(&64u32.to_le_bytes());
|
|
// sblk_size chosen so 24 + size == 64.
|
|
v[0x0c..0x10].copy_from_slice(&40u32.to_le_bytes());
|
|
v[0x14..0x18].copy_from_slice(&32u32.to_le_bytes());
|
|
v[24..28].copy_from_slice(b"SBlk");
|
|
v[28..32].copy_from_slice(&1u32.to_le_bytes());
|
|
v[32..36].copy_from_slice(&4u32.to_le_bytes());
|
|
v[36..39].copy_from_slice(b"DAW");
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn splits_the_two_regions() {
|
|
let b = bytes();
|
|
let s = parse_sound_bank(&b).unwrap();
|
|
assert_eq!(s.sblk.len(), 40);
|
|
assert_eq!(s.daw.len(), 32);
|
|
assert!(s.has_sblk_magic());
|
|
assert!(s.has_daw_tag());
|
|
assert_eq!(s.sblk_version(), Some(1));
|
|
assert_eq!(s.sblk_entries(), Some(4));
|
|
assert_eq!(s.daw_tail, 0x100 - 96);
|
|
}
|
|
|
|
#[test]
|
|
fn the_regions_tile_the_head() {
|
|
let b = bytes();
|
|
let s = parse_sound_bank(&b).unwrap();
|
|
assert_eq!(
|
|
s.header.sblk_offset + s.header.sblk_size,
|
|
s.header.daw_offset
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_short_or_bogus_extent_is_an_error_not_a_panic() {
|
|
assert!(parse_sound_bank(&[]).is_err());
|
|
assert!(parse_sound_bank(&[0u8; 0x10]).is_err());
|
|
// A DAW offset past the end.
|
|
let mut b = bytes();
|
|
b[0x10..0x14].copy_from_slice(&9_000u32.to_le_bytes());
|
|
assert!(parse_sound_bank(&b).is_err());
|
|
// A DAW size that runs off the end.
|
|
let mut b = bytes();
|
|
b[0x14..0x18].copy_from_slice(&9_000u32.to_le_bytes());
|
|
assert!(parse_sound_bank(&b).is_err());
|
|
}
|
|
}
|