Skip to main content

rakata_formats/wav/
reader.rs

1//! WAV binary reader.
2
3use std::io::Read;
4
5use super::{
6    binary, Wav, WavEncodingCode, WavError, WavType, WavWaveMetadata, WavWrapperKind,
7    DATA_CHUNK_ID, FMT_CHUNK_ID, RIFF_MAGIC, SFX_HEADER_SIZE, WAVE_MAGIC,
8};
9
10/// Reads WAV data from a reader.
11///
12/// # Errors
13///
14/// [`WavError::Io`] when the stream will not read to end, and whatever
15/// [`read_wav_from_bytes`] reports for the bytes it collected.
16#[cfg_attr(
17    feature = "tracing",
18    tracing::instrument(level = "debug", skip(reader))
19)]
20pub fn read_wav<R: Read>(reader: &mut R) -> Result<Wav, WavError> {
21    let mut bytes = Vec::new();
22    reader.read_to_end(&mut bytes)?;
23    read_wav_from_bytes(&bytes)
24}
25
26/// Reads WAV data from bytes.
27///
28/// # Errors
29///
30/// [`WavError::InvalidHeader`] when `bytes` are not a RIFF/WAVE container, and
31/// [`WavError::InvalidChunk`] when a chunk's declared length runs past the end
32/// or a required chunk is absent.
33///
34/// The engine's SFX prefix is optional on read: a file carrying one and a file
35/// without both read, since the engine does not require it either.
36#[cfg_attr(
37    feature = "tracing",
38    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
39)]
40pub fn read_wav_from_bytes(bytes: &[u8]) -> Result<Wav, WavError> {
41    let file_len = u64::try_from(bytes.len()).expect("a slice's length fits in u64");
42    let kind = classify_wav(bytes, file_len).ok_or_else(|| {
43        WavError::InvalidHeader("no RIFF at the start of the file or at 0x1D6".into())
44    })?;
45
46    match kind {
47        WavWrapperKind::Standard => parse_wave_payload(bytes, WavType::Standard),
48        WavWrapperKind::SfxHeader => parse_wave_payload(&bytes[SFX_HEADER_SIZE..], WavType::Sfx),
49        WavWrapperKind::Mp3InWav => {
50            let mp3 = riff_chunk_end(bytes)
51                .and_then(|end| usize::try_from(end).ok())
52                .and_then(|end| bytes.get(end..))
53                .ok_or_else(|| {
54                    WavError::InvalidHeader("MP3-in-WAV payload offset is unreadable".into())
55                })?;
56            Ok(Wav::new_mp3(WavType::Vo, mp3.to_vec()))
57        }
58    }
59}
60
61/// How many leading bytes [`classify_wav`] reads: through the `RIFF` a
62/// prefixed file carries at `0x1D6`.
63pub const WAV_CLASSIFY_LEN: usize = SFX_HEADER_SIZE + RIFF_MAGIC.len();
64
65/// Which of the engine's three forms a `.wav` file takes.
66///
67/// `head` is the start of the file, at least [`WAV_CLASSIFY_LEN`] bytes of it
68/// or the whole file when it is shorter, and `file_len` is the whole file's
69/// length. Nothing past `WAV_CLASSIFY_LEN` is read, so a caller checking many
70/// files reads their headers rather than their audio.
71///
72/// A file starting with `RIFF` is MP3-in-WAV when its declared RIFF size plus
73/// eight falls short of `file_len`, and plain otherwise. A file without `RIFF`
74/// at the start is the prefixed form when `RIFF` sits at `0x1D6`. These are
75/// the engine's own tests, laid out in `docs/src/formats/audio/wav.md`.
76///
77/// Returns `None` when neither test passes, so the engine takes the file as
78/// none of the three. The forms are told apart here, not validated: a plain
79/// file can still fail to parse.
80///
81/// # Panics
82///
83/// In debug builds, when `head` holds fewer bytes than the file has up to
84/// `WAV_CLASSIFY_LEN`. A short head would hide the prefixed form's `RIFF`
85/// and answer `None` for a file the engine plays.
86pub fn classify_wav(head: &[u8], file_len: u64) -> Option<WavWrapperKind> {
87    let needed =
88        usize::try_from(file_len).map_or(WAV_CLASSIFY_LEN, |len| len.min(WAV_CLASSIFY_LEN));
89    debug_assert!(
90        head.len() >= needed,
91        "classify_wav needs the file's first {needed} bytes and was given {}",
92        head.len()
93    );
94    if head.starts_with(&RIFF_MAGIC) {
95        return Some(match riff_chunk_end(head) {
96            Some(end) if end < file_len => WavWrapperKind::Mp3InWav,
97            _ => WavWrapperKind::Standard,
98        });
99    }
100    // The prefix's own bytes are never examined, so the `FF F3 60 C4` every
101    // retail prefix opens with is not the test.
102    let at_prefix_end = head.get(SFX_HEADER_SIZE..WAV_CLASSIFY_LEN)?;
103    (at_prefix_end == RIFF_MAGIC).then_some(WavWrapperKind::SfxHeader)
104}
105
106/// Where the RIFF chunk that opens `bytes` ends: its declared size plus the
107/// eight bytes of tag and size in front of it.
108fn riff_chunk_end(bytes: &[u8]) -> Option<u64> {
109    binary::read_u32(bytes, 4)
110        .ok()
111        .map(|size| u64::from(size) + 8)
112}
113
114fn parse_wave_payload(bytes: &[u8], wav_type: WavType) -> Result<Wav, WavError> {
115    if bytes.len() < 12 {
116        return Err(WavError::InvalidHeader(
117            "WAVE payload shorter than 12-byte RIFF header".into(),
118        ));
119    }
120    if !bytes.starts_with(&RIFF_MAGIC) {
121        return Err(WavError::InvalidHeader(
122            "missing RIFF magic after wrapper normalization".into(),
123        ));
124    }
125    if bytes[8..12] != WAVE_MAGIC {
126        return Err(WavError::InvalidHeader(
127            "missing WAVE magic in RIFF header".into(),
128        ));
129    }
130
131    let mut cursor = 12usize;
132    let mut fmt_fields: Option<(WavEncodingCode, u16, u32, u32, u16, u16)> = None;
133    let mut data_chunk: Option<Vec<u8>> = None;
134
135    while cursor + 8 <= bytes.len() {
136        let chunk_id = [
137            bytes[cursor],
138            bytes[cursor + 1],
139            bytes[cursor + 2],
140            bytes[cursor + 3],
141        ];
142        let chunk_size_u32 = binary::read_u32(bytes, cursor + 4).map_err(|_| {
143            WavError::InvalidChunk(format!(
144                "unable to read chunk size at offset {}",
145                cursor + 4
146            ))
147        })?;
148        let chunk_size = usize::try_from(chunk_size_u32).map_err(|_| {
149            WavError::InvalidChunk(format!("chunk size {chunk_size_u32} does not fit in usize"))
150        })?;
151        cursor += 8;
152
153        let chunk_end = cursor.checked_add(chunk_size).ok_or_else(|| {
154            WavError::InvalidChunk(format!("chunk size overflow at chunk offset {cursor}"))
155        })?;
156        if chunk_end > bytes.len() {
157            return Err(WavError::InvalidChunk(format!(
158                "chunk at offset {} exceeds file bounds",
159                cursor - 8
160            )));
161        }
162        let chunk_data = &bytes[cursor..chunk_end];
163
164        if chunk_id == FMT_CHUNK_ID {
165            if chunk_data.len() < 16 {
166                return Err(WavError::InvalidChunk(
167                    "`fmt ` chunk shorter than 16 bytes".into(),
168                ));
169            }
170
171            let encoding =
172                WavEncodingCode::from_raw(u16::from_le_bytes([chunk_data[0], chunk_data[1]]));
173            let channels = u16::from_le_bytes([chunk_data[2], chunk_data[3]]);
174            let sample_rate =
175                u32::from_le_bytes([chunk_data[4], chunk_data[5], chunk_data[6], chunk_data[7]]);
176            let bytes_per_sec =
177                u32::from_le_bytes([chunk_data[8], chunk_data[9], chunk_data[10], chunk_data[11]]);
178            let block_align = u16::from_le_bytes([chunk_data[12], chunk_data[13]]);
179            let bits_per_sample = u16::from_le_bytes([chunk_data[14], chunk_data[15]]);
180            fmt_fields = Some((
181                encoding,
182                channels,
183                sample_rate,
184                bytes_per_sec,
185                block_align,
186                bits_per_sample,
187            ));
188        } else if chunk_id == DATA_CHUNK_ID {
189            data_chunk = Some(chunk_data.to_vec());
190            break;
191        }
192
193        cursor = chunk_end;
194        if chunk_size % 2 == 1 && cursor < bytes.len() {
195            cursor += 1;
196        }
197    }
198
199    let (encoding, channels, sample_rate, bytes_per_sec, block_align, bits_per_sample) =
200        fmt_fields.ok_or_else(|| WavError::InvalidChunk("missing required `fmt ` chunk".into()))?;
201    let data =
202        data_chunk.ok_or_else(|| WavError::InvalidChunk("missing required `data` chunk".into()))?;
203
204    Ok(Wav::new_wave(
205        wav_type,
206        WavWaveMetadata {
207            encoding,
208            channels,
209            sample_rate,
210            bytes_per_sec,
211            block_align,
212            bits_per_sample,
213        },
214        data,
215    ))
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::binary::{DecodeBinary, EncodeBinary};
222    use crate::wav::{
223        write_wav_to_vec, write_wav_to_vec_with_options, WavAudioFormat, WavEncoding, WavWriteMode,
224        WavWriteOptions, SFX_MAGIC,
225    };
226
227    fn sample_wave() -> Wav {
228        Wav::new_wave(
229            WavType::Standard,
230            WavWaveMetadata {
231                encoding: WavEncodingCode::from(WavEncoding::Pcm),
232                channels: 1,
233                sample_rate: 22_050,
234                bytes_per_sec: 44_100,
235                block_align: 2,
236                bits_per_sample: 16,
237            },
238            vec![1, 2, 3, 4, 5, 6],
239        )
240    }
241
242    #[test]
243    fn roundtrip_clean_wave_payload() {
244        let wav = sample_wave();
245        let bytes = write_wav_to_vec_with_options(
246            &wav,
247            WavWriteOptions {
248                mode: WavWriteMode::Clean,
249            },
250        )
251        .expect("write clean WAV");
252        let parsed = read_wav_from_bytes(&bytes).expect("read clean WAV");
253
254        assert_eq!(parsed.audio_format, WavAudioFormat::Wave);
255        assert_eq!(parsed.encoding.known(), Some(WavEncoding::Pcm));
256        assert_eq!(parsed.channels, 1);
257        assert_eq!(parsed.sample_rate, 22_050);
258        assert_eq!(parsed.data, wav.data);
259        // Clean-mode write produces a plain RIFF file; reading it back gives WavType::Standard.
260        assert_eq!(parsed.wav_type, WavType::Standard);
261    }
262
263    #[test]
264    fn writer_is_deterministic_for_synthetic_wave() {
265        let wav = sample_wave();
266        let first = write_wav_to_vec(&wav).expect("first write should succeed");
267        let second = write_wav_to_vec(&wav).expect("second write should succeed");
268        assert_eq!(first, second);
269    }
270
271    #[test]
272    fn roundtrip_game_sfx_wrapper() {
273        let mut wav = sample_wave();
274        wav.wav_type = WavType::Sfx;
275
276        let bytes = write_wav_to_vec(&wav).expect("write game WAV");
277        assert!(bytes.starts_with(&SFX_MAGIC));
278
279        let parsed = read_wav_from_bytes(&bytes).expect("read SFX WAV");
280        assert_eq!(parsed.wav_type, WavType::Sfx);
281        assert_eq!(parsed.audio_format, WavAudioFormat::Wave);
282        assert_eq!(parsed.data, wav.data);
283    }
284
285    #[test]
286    fn wrapped_sfx_is_detected_without_the_vanilla_prefix() {
287        // The engine finds a wrapped file by seeking to the fixed offset and
288        // requiring RIFF there, never by matching the prefix. A file with a
289        // different wrapper is one the game loads and this reader used to
290        // refuse, so the prefix must not be the gate.
291        let mut wav = sample_wave();
292        wav.wav_type = WavType::Sfx;
293        let mut bytes = write_wav_to_vec(&wav).expect("write game WAV");
294        bytes[0..4].copy_from_slice(b"\x00\x11\x22\x33");
295        assert!(!bytes.starts_with(&SFX_MAGIC));
296
297        let parsed = read_wav_from_bytes(&bytes).expect("read SFX WAV with a foreign prefix");
298        assert_eq!(parsed.wav_type, WavType::Sfx);
299        assert_eq!(parsed.data, wav.data);
300    }
301
302    #[test]
303    fn a_file_with_riff_nowhere_is_not_treated_as_wrapped() {
304        // The negative half of the same rule: no RIFF at the start and none at
305        // the wrapper offset either means the file is not a wrapped SFX.
306        let bytes = vec![0u8; SFX_HEADER_SIZE + 64];
307        let err = read_wav_from_bytes(&bytes);
308        assert!(err.is_err(), "a file with no RIFF anywhere must not parse");
309    }
310
311    /// A RIFF chunk declaring `riff_size` bytes, zero-filled, then `tail`.
312    fn riff_then(riff_size: u32, tail: &[u8]) -> Vec<u8> {
313        let mut bytes = Vec::new();
314        bytes.extend_from_slice(&RIFF_MAGIC);
315        bytes.extend_from_slice(&riff_size.to_le_bytes());
316        bytes.extend_from_slice(&WAVE_MAGIC);
317        bytes.resize(usize::try_from(riff_size).expect("test size fits") + 8, 0);
318        bytes.extend_from_slice(tail);
319        bytes
320    }
321
322    fn classify(bytes: &[u8]) -> Option<WavWrapperKind> {
323        classify_wav(bytes, u64::try_from(bytes.len()).expect("test length fits"))
324    }
325
326    #[test]
327    fn mp3_in_wav_payload_starts_where_the_riff_chunk_ends() {
328        // A chunk size other than vanilla's 50, so the offset has to be
329        // computed rather than assumed.
330        let mp3_payload = vec![0x49, 0x44, 0x33, 0x04, 0x00];
331        let wrapped = riff_then(36, &mp3_payload);
332
333        let parsed = read_wav_from_bytes(&wrapped).expect("read MP3-in-WAV");
334        assert_eq!(parsed.wav_type, WavType::Vo);
335        assert_eq!(parsed.audio_format, WavAudioFormat::Mp3);
336        assert_eq!(parsed.encoding.known(), Some(WavEncoding::Mp3));
337        assert_eq!(parsed.data, mp3_payload);
338    }
339
340    #[test]
341    fn classify_tells_the_three_forms_apart() {
342        let plain = write_wav_to_vec(&sample_wave()).expect("write plain");
343        let mut prefixed_wav = sample_wave();
344        prefixed_wav.wav_type = WavType::Sfx;
345        let prefixed = write_wav_to_vec(&prefixed_wav).expect("write prefixed");
346        let mp3 = riff_then(50, &[0xFF, 0xFB]);
347
348        assert_eq!(classify(&plain), Some(WavWrapperKind::Standard));
349        assert_eq!(classify(&prefixed), Some(WavWrapperKind::SfxHeader));
350        assert_eq!(classify(&mp3), Some(WavWrapperKind::Mp3InWav));
351        assert_eq!(classify(&[0_u8; WAV_CLASSIFY_LEN]), None);
352    }
353
354    #[test]
355    fn a_riff_chunk_reaching_past_the_file_is_plain() {
356        // Only a chunk ending short of the file makes MP3-in-WAV. One that
357        // claims more than the file holds is plain, and fails to parse later.
358        let mut truncated = riff_then(64, &[]);
359        truncated.truncate(40);
360        assert_eq!(classify(&truncated), Some(WavWrapperKind::Standard));
361    }
362
363    #[test]
364    fn a_second_riff_at_byte_twenty_is_mp3_in_wav() {
365        // A RIFF header whose chunk ends at byte 20 and a whole RIFF/WAVE
366        // after it: by the engine's test that is MP3-in-WAV, whatever follows.
367        let inner = write_wav_to_vec(&sample_wave()).expect("write plain");
368        assert_eq!(
369            classify(&riff_then(12, &inner)),
370            Some(WavWrapperKind::Mp3InWav)
371        );
372    }
373
374    #[test]
375    #[cfg(debug_assertions)]
376    #[should_panic(expected = "classify_wav needs the file's first")]
377    fn classify_refuses_a_head_shorter_than_its_prefix() {
378        let _ = classify_wav(&[0_u8; 16], 4096);
379    }
380
381    #[test]
382    fn classify_reads_no_further_than_its_prefix() {
383        let mut prefixed_wav = sample_wave();
384        prefixed_wav.wav_type = WavType::Sfx;
385        let mut prefixed = write_wav_to_vec(&prefixed_wav).expect("write prefixed");
386        prefixed.extend_from_slice(&[0_u8; 1024]);
387        let mp3 = riff_then(50, &[0xFF; 1024]);
388
389        for bytes in [prefixed, mp3] {
390            let len = u64::try_from(bytes.len()).expect("test length fits");
391            assert_eq!(
392                classify_wav(&bytes[..WAV_CLASSIFY_LEN], len),
393                classify_wav(&bytes, len)
394            );
395        }
396    }
397
398    #[test]
399    fn clean_mode_mp3_writes_raw_payload() {
400        let wav = Wav::new_mp3(WavType::Vo, vec![0x01, 0x02, 0x03]);
401        let bytes = write_wav_to_vec_with_options(
402            &wav,
403            WavWriteOptions {
404                mode: WavWriteMode::Clean,
405            },
406        )
407        .expect("write clean MP3");
408        assert_eq!(bytes, vec![0x01, 0x02, 0x03]);
409    }
410
411    #[test]
412    fn rejects_non_riff_wave_payload() {
413        let err = read_wav_from_bytes(b"not-riff").expect_err("must fail");
414        assert!(matches!(err, WavError::InvalidHeader(_)));
415    }
416
417    #[test]
418    fn rejects_truncated_header() {
419        let err = read_wav_from_bytes(&RIFF_MAGIC).expect_err("must fail");
420        assert!(matches!(err, WavError::InvalidHeader(_)));
421    }
422
423    #[test]
424    fn rejects_missing_fmt_chunk() {
425        let mut bytes = Vec::new();
426        bytes.extend_from_slice(&RIFF_MAGIC);
427        bytes.extend_from_slice(&(12_u32 + 8 + 4).to_le_bytes());
428        bytes.extend_from_slice(&WAVE_MAGIC);
429        bytes.extend_from_slice(&DATA_CHUNK_ID);
430        bytes.extend_from_slice(&(4_u32).to_le_bytes());
431        bytes.extend_from_slice(&[0, 1, 2, 3]);
432
433        let err = read_wav_from_bytes(&bytes).expect_err("must fail");
434        assert!(matches!(err, WavError::InvalidChunk(_)));
435    }
436
437    #[test]
438    fn rejects_truncated_chunk_payload() {
439        let mut bytes = Vec::new();
440        bytes.extend_from_slice(&RIFF_MAGIC);
441        bytes.extend_from_slice(&(12_u32 + 8 + 16).to_le_bytes());
442        bytes.extend_from_slice(&WAVE_MAGIC);
443        bytes.extend_from_slice(&FMT_CHUNK_ID);
444        bytes.extend_from_slice(&(16_u32).to_le_bytes());
445        bytes.extend_from_slice(&[1, 0, 1, 0]); // truncated fmt chunk
446
447        let err = read_wav_from_bytes(&bytes).expect_err("must fail");
448        assert!(matches!(err, WavError::InvalidChunk(_)));
449    }
450
451    #[test]
452    fn decode_encode_traits_roundtrip() {
453        let mut wav = sample_wave();
454        wav.wav_type = WavType::Sfx;
455
456        let bytes = wav.encode_binary().expect("encode");
457        let decoded = Wav::decode_binary(&bytes).expect("decode");
458
459        assert_eq!(decoded.wav_type, WavType::Sfx);
460        assert_eq!(decoded.audio_format, WavAudioFormat::Wave);
461        assert_eq!(decoded.data, wav.data);
462    }
463}