Parse the sound bank container framing
A level's sound extent is two regions: an `SBlk` descriptor table and the sample data that follows it. Four u32 at the head describe the layout, and the invariant that establishes it is sblk_offset + sblk_size == daw_offset, which holds on all 27 levels. That is what pins the field meanings; the numbers alone are ambiguous about which of them is the offset. DAW is a format tag inside the SBlk header, not a region signature. It occurs exactly once in the entire extent, at sblk_offset + 12, naming the sample format -- the sample data itself starts at daw_offset with no signature at all, roughly 19 KB away on level 0. Reading it as a region magic is the obvious reading and it is wrong. daw_size never reaches the end of the file. The tail is positive on all 27 levels, 232 to 1,848 bytes, so the field describes less than the region it names; the test asserts that direction rather than inventing a size that fits. 523,720 SBlk and 37,649,008 DAW bytes across the 27 levels. The sample format inside DAW is not decoded. It looks like VAG/ADPCM from the nibble packing, and it has no bearing on drawing anything, so it stays open rather than getting a half-answer.
This commit is contained in:
@@ -1393,3 +1393,69 @@ fn occlusion_grids_parse_on_every_level() {
|
|||||||
widest unexplained tail {widest_slack} bytes"
|
widest unexplained tail {widest_slack} bytes"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The sound extent framing.
|
||||||
|
///
|
||||||
|
/// The invariant here is `sblk_offset + sblk_size == daw_offset`, which holds
|
||||||
|
/// on all 27 levels and pins the meaning of the four head fields. `DAW` never
|
||||||
|
/// reaches the end of the file; the tail is positive on every level, so the
|
||||||
|
/// size field is not the whole region and this test asserts the direction
|
||||||
|
/// rather than a size.
|
||||||
|
#[test]
|
||||||
|
fn sound_bank_framing_holds_on_every_level() {
|
||||||
|
require_disc!();
|
||||||
|
let data = std::env::var_os("GC_EXTRACTED")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||||
|
PathBuf::from(home).join("Documents/gc-extracted")
|
||||||
|
});
|
||||||
|
if !data.exists() {
|
||||||
|
eprintln!("skipped: unpack the disc first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
use gc_formats::sound_bank::parse_sound_bank;
|
||||||
|
let mut levels = 0usize;
|
||||||
|
let mut sblk_bytes = 0usize;
|
||||||
|
let mut daw_bytes = 0usize;
|
||||||
|
let (mut narrowest, mut widest) = (usize::MAX, 0usize);
|
||||||
|
|
||||||
|
for n in 0..27 {
|
||||||
|
let raw = match std::fs::read(data.join(format!("level{n}")).join("sound_bank.bin")) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let s = parse_sound_bank(&raw).unwrap_or_else(|e| panic!("level{n}: sound bank: {e}"));
|
||||||
|
|
||||||
|
assert_eq!(s.header.version, 3, "level{n}: sound bank version");
|
||||||
|
assert_eq!(s.header.unknown_4, 2, "level{n}: sound bank unknown_4");
|
||||||
|
assert_eq!(
|
||||||
|
s.header.sblk_offset, 24,
|
||||||
|
"level{n}: SBlk sits at 24 on every level"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
s.header.sblk_offset + s.header.sblk_size == s.header.daw_offset,
|
||||||
|
"level{n}: SBlk must run into DAW with no gap"
|
||||||
|
);
|
||||||
|
assert!(s.has_sblk_magic(), "level{n}: SBlk magic");
|
||||||
|
assert!(s.has_daw_tag(), "level{n}: DAW tag at SBlk+12");
|
||||||
|
assert_eq!(s.sblk_version(), Some(1), "level{n}: SBlk version");
|
||||||
|
// The tail is always in the same direction: the extent is longer than
|
||||||
|
// the DAW region it describes.
|
||||||
|
narrowest = narrowest.min(s.daw_tail);
|
||||||
|
widest = widest.max(s.daw_tail);
|
||||||
|
|
||||||
|
sblk_bytes += s.sblk.len();
|
||||||
|
daw_bytes += s.daw.len();
|
||||||
|
levels += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(levels, 27, "expected all 27 levels, got {levels}");
|
||||||
|
assert!(sblk_bytes > 300_000, "only {sblk_bytes} SBlk bytes");
|
||||||
|
assert!(daw_bytes > 20_000_000, "only {daw_bytes} DAW bytes");
|
||||||
|
eprintln!(
|
||||||
|
"sound_bank: {sblk_bytes} SBlk + {daw_bytes} DAW bytes across {levels} levels; \
|
||||||
|
unexplained tail {narrowest}..{widest} bytes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod level_core;
|
|||||||
pub mod level_data;
|
pub mod level_data;
|
||||||
pub mod level_wad;
|
pub mod level_wad;
|
||||||
pub mod occlusion;
|
pub mod occlusion;
|
||||||
|
pub mod sound_bank;
|
||||||
pub mod texture;
|
pub mod texture;
|
||||||
pub mod tfrag;
|
pub mod tfrag;
|
||||||
pub mod tfrag_light;
|
pub mod tfrag_light;
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
//! 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")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user