openpol/
sounddat.rs

1//! sound.dat data access operations.
2//!
3//! # sound.dat file format
4//! The file consists of concatenated N sounds followed by N little endian 32-bit unsigned
5//! integer sizes (in bytes). The nth size corresponds to the nth sound, the file layout
6//! looks like this:
7//!
8//! `[ 0th sound ] [ 1st sound ] ... [ N-2th sound ] [N-1th sound]
9//! [ 0th size ] [ 1st size ] ... [ N-2th size ] [ N-1th size]`
10//!
11//! The original game hardcodes the number of sounds when opening the sound.dat file, but it's
12//! possible to autodect it. This module performs autodetection like this: read 4-byte integers
13//! starting with the very end of the file and add them together until the sum is equal to
14//! B - 4 * N (where B is total file size in bytes and N is the number of sizes read so far).
15//!
16//! The algorithm has been verified with sound.dat coming from the CD version of Polanie
17//! (SHA1 hash `8033978a51c176122ba507e417e8d758fdaa70a9`, 3 681 170 bytes) - the file contains 183 sounds.
18//!
19//! # Sound format
20//!
21//! The individual sounds are unsigned bytes containing single channel of 22 050Hz-sampled raw audio data.
22//!
23//! # Example
24//!
25//! An `openpol-extract-audio` sample binary which uses this code is provided. You can listen to
26//! a chosen sound using sox and mpv like this:
27//!
28//! `sox -r22050 -t ub -c 1 <(cargo run --bin openpol-extract-audio -- SOUND.DAT 20) -t wav - | mpv -`
29use std::convert::TryInto;
30use std::io;
31
32/// A way to access sound.dat contents.
33pub struct Sounddat {
34    data: Vec<u8>,
35    sizes: Vec<usize>,
36    offsets: Vec<usize>,
37}
38
39impl Sounddat {
40    /// Load sound.dat contents. All of it is read into memory.
41    ///
42    /// # Errors
43    /// The code will panic if `reader` cannot read to end. If the number of sounds can't be
44    /// autodetected (the file contains unexpected data) the function will return `None`.
45    pub fn load<T: io::Read>(mut reader: T) -> Option<Sounddat> {
46        let mut data = Vec::new();
47        reader.read_to_end(&mut data).unwrap();
48
49        let total_bytes = data.len();
50        let mut accumulator = 0usize;
51        const ENTRY_SIZE: usize = 4;
52        let mut sounds = 0;
53        let mut data_bytes = total_bytes;
54        let mut sizes = Vec::new();
55
56        loop {
57            let offset = total_bytes - ENTRY_SIZE * (sounds + 1);
58            let entry =
59                u32::from_le_bytes(data[offset..offset + ENTRY_SIZE].try_into().unwrap()) as usize;
60            data_bytes -= ENTRY_SIZE;
61            sounds += 1;
62            sizes.push(entry);
63            accumulator += entry;
64            if accumulator > data_bytes {
65                return None;
66            }
67            if accumulator == data_bytes {
68                break;
69            }
70        }
71
72        sizes.reverse();
73        let mut offsets = Vec::new();
74        let mut offset = 0;
75        for size in &sizes {
76            offsets.push(offset);
77            offset += size;
78        }
79
80        Some(Sounddat {
81            data,
82            sizes,
83            offsets,
84        })
85    }
86
87    /// The number of sounds in the file.
88    pub fn sounds(&self) -> usize {
89        self.sizes.len()
90    }
91
92    /// The `sound`'s data (`sound` is 0-based). The data is to be interpreted as described by the
93    /// [module's documentation on the sound format](index.html#sound-format).
94    pub fn sound_data(&self, sound: usize) -> &[u8] {
95        let offset = self.offsets[sound];
96        &self.data[offset..offset + self.sizes[sound]]
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use crate::sounddat::Sounddat;
103
104    #[test]
105    fn test_sounddat_loading_works() {
106        let data = [1, 2, 3, 4, 5, 6, 4, 0, 0, 0, 2, 0, 0, 0];
107        let sounddat = Sounddat::load(&data[..]).unwrap();
108        assert_eq!(sounddat.sounds(), 2);
109        assert_eq!(sounddat.sound_data(0), [1, 2, 3, 4]);
110        assert_eq!(sounddat.sound_data(1), [5, 6]);
111    }
112}