//! 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 { 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> { 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 { 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 { 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 { 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()); } }