Skip to main content

rakata_formats/wav/
writer.rs

1//! WAV binary writer.
2
3use std::io::Write;
4
5use super::{
6    Wav, WavAudioFormat, WavError, WavType, WavWriteMode, WavWriteOptions, DATA_CHUNK_ID,
7    FMT_CHUNK_ID, MP3_IN_WAV_HEADER, RIFF_MAGIC, SFX_HEADER_SIZE, SFX_MAGIC, WAVE_MAGIC,
8};
9
10/// Writes WAV data to a writer using [`WavWriteMode::Game`].
11///
12/// # Errors
13///
14/// The same as [`write_wav_with_options`] under that mode.
15#[cfg_attr(
16    feature = "tracing",
17    tracing::instrument(level = "debug", skip(writer, wav))
18)]
19pub fn write_wav<W: Write>(writer: &mut W, wav: &Wav) -> Result<(), WavError> {
20    write_wav_with_options(writer, wav, WavWriteOptions::default())
21}
22
23/// Writes WAV data to a writer with explicit options.
24///
25/// # Errors
26///
27/// [`WavError::InvalidChunk`] when a derived header field overflows its
28/// on-disk width: the data size, the byte rate from sample rate times block
29/// align, the RIFF size, or the block align from channels times bytes per
30/// sample. In game mode, [`WavError::MismatchedPayload`] when the form cannot
31/// hold the payload, and [`WavError::EmptyMp3`] for an MP3-in-WAV with no MP3.
32/// The payload is built before anything is written, so these leave the writer
33/// untouched. [`WavError::Io`] when the writer fails.
34#[cfg_attr(
35    feature = "tracing",
36    tracing::instrument(level = "debug", skip(writer, wav, options))
37)]
38pub fn write_wav_with_options<W: Write>(
39    writer: &mut W,
40    wav: &Wav,
41    options: WavWriteOptions,
42) -> Result<(), WavError> {
43    let clean_payload = match wav.audio_format {
44        WavAudioFormat::Wave => build_clean_wave_payload(wav)?,
45        WavAudioFormat::Mp3 => wav.data.clone(),
46    };
47
48    let out = match options.mode {
49        WavWriteMode::Clean => clean_payload,
50        WavWriteMode::Game => obfuscate_for_game(&clean_payload, wav)?,
51    };
52    writer.write_all(&out)?;
53    Ok(())
54}
55
56/// Serializes WAV data to a byte vector using [`WavWriteMode::Game`].
57///
58/// # Errors
59///
60/// The same as [`write_wav_to_vec_with_options`] under that mode.
61#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(wav)))]
62pub fn write_wav_to_vec(wav: &Wav) -> Result<Vec<u8>, WavError> {
63    write_wav_to_vec_with_options(wav, WavWriteOptions::default())
64}
65
66/// Serializes WAV data to a byte vector with explicit options.
67///
68/// # Errors
69///
70/// [`WavError::InvalidChunk`] on the terms [`write_wav_with_options`] gives.
71/// The `Vec` target has no I/O to fail at.
72#[cfg_attr(
73    feature = "tracing",
74    tracing::instrument(level = "debug", skip(wav, options))
75)]
76pub fn write_wav_to_vec_with_options(
77    wav: &Wav,
78    options: WavWriteOptions,
79) -> Result<Vec<u8>, WavError> {
80    let mut out = Vec::new();
81    write_wav_with_options(&mut out, wav, options)?;
82    Ok(out)
83}
84
85fn build_clean_wave_payload(wav: &Wav) -> Result<Vec<u8>, WavError> {
86    let data_size = u32::try_from(wav.data.len())
87        .map_err(|_| WavError::InvalidChunk("audio payload exceeds 4GiB RIFF limit".into()))?;
88
89    let block_align = if wav.block_align != 0 {
90        wav.block_align
91    } else {
92        default_block_align(wav.channels, wav.bits_per_sample)?
93    };
94    let bytes_per_sec = if wav.bytes_per_sec != 0 {
95        wav.bytes_per_sec
96    } else {
97        wav.sample_rate
98            .checked_mul(u32::from(block_align))
99            .ok_or_else(|| {
100                WavError::InvalidChunk("sample_rate * block_align overflows u32".into())
101            })?
102    };
103
104    let fmt_chunk_size: u32 = 16;
105    let riff_size = 4_u32
106        .checked_add(8)
107        .and_then(|value| value.checked_add(fmt_chunk_size))
108        .and_then(|value| value.checked_add(8))
109        .and_then(|value| value.checked_add(data_size))
110        .ok_or_else(|| WavError::InvalidChunk("RIFF size overflows u32".into()))?;
111
112    let mut out = Vec::with_capacity(
113        usize::try_from(8_u64 + u64::from(riff_size))
114            .unwrap_or(0)
115            .max(usize::from(44_u16)),
116    );
117    out.extend_from_slice(&RIFF_MAGIC);
118    out.extend_from_slice(&riff_size.to_le_bytes());
119    out.extend_from_slice(&WAVE_MAGIC);
120    out.extend_from_slice(&FMT_CHUNK_ID);
121    out.extend_from_slice(&fmt_chunk_size.to_le_bytes());
122    out.extend_from_slice(&wav.encoding.raw().to_le_bytes());
123    out.extend_from_slice(&wav.channels.to_le_bytes());
124    out.extend_from_slice(&wav.sample_rate.to_le_bytes());
125    out.extend_from_slice(&bytes_per_sec.to_le_bytes());
126    out.extend_from_slice(&block_align.to_le_bytes());
127    out.extend_from_slice(&wav.bits_per_sample.to_le_bytes());
128    out.extend_from_slice(&DATA_CHUNK_ID);
129    out.extend_from_slice(&data_size.to_le_bytes());
130    out.extend_from_slice(&wav.data);
131
132    Ok(out)
133}
134
135fn default_block_align(channels: u16, bits_per_sample: u16) -> Result<u16, WavError> {
136    let bytes_per_sample = bits_per_sample / 8;
137    channels
138        .checked_mul(bytes_per_sample)
139        .ok_or_else(|| WavError::InvalidChunk("channels * bytes_per_sample overflows u16".into()))
140}
141
142fn obfuscate_for_game(clean_payload: &[u8], wav: &Wav) -> Result<Vec<u8>, WavError> {
143    // TODO(wav-decode): Keep codec-level decode/resample helpers out of this container module;
144    // add optional PCM/ADPCM decoding in higher-level playback/tooling APIs.
145    Ok(match (wav.wav_type, wav.audio_format) {
146        (WavType::Standard, WavAudioFormat::Wave) => clean_payload.to_vec(),
147        (WavType::Sfx, WavAudioFormat::Wave) => {
148            // Deliberately asymmetric with the reader, which does not
149            // require this prefix because the engine does not either. It is
150            // emitted anyway: every retail SFX carries it, so writing it
151            // keeps this output comparable with shipped files, and other
152            // tools do key on it even though the game does not.
153            let mut out = Vec::with_capacity(SFX_HEADER_SIZE + clean_payload.len());
154            out.resize(SFX_HEADER_SIZE, 0);
155            out[0..4].copy_from_slice(&SFX_MAGIC);
156            out.extend_from_slice(clean_payload);
157            out
158        }
159        (WavType::Vo, WavAudioFormat::Mp3) => {
160            if clean_payload.is_empty() {
161                return Err(WavError::EmptyMp3);
162            }
163            let mut out = Vec::with_capacity(MP3_IN_WAV_HEADER.len() + clean_payload.len());
164            out.extend_from_slice(&MP3_IN_WAV_HEADER);
165            out.extend_from_slice(clean_payload);
166            out
167        }
168        (form @ (WavType::Standard | WavType::Sfx), payload @ WavAudioFormat::Mp3)
169        | (form @ WavType::Vo, payload @ WavAudioFormat::Wave) => {
170            return Err(WavError::MismatchedPayload { form, payload });
171        }
172    })
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::wav::{read_wav_from_bytes, WavEncoding, WavEncodingCode, WavWaveMetadata};
179
180    /// The header as `wav.md` lays it out, row by row.
181    fn header_from_the_page() -> Vec<u8> {
182        let mut header = Vec::new();
183        header.extend_from_slice(b"RIFF");
184        header.extend_from_slice(&50_u32.to_le_bytes());
185        header.extend_from_slice(b"WAVE");
186        header.extend_from_slice(b"fmt ");
187        header.extend_from_slice(&18_u32.to_le_bytes());
188        // 8-bit mono PCM at 22,050 Hz, so one byte per frame.
189        header.extend_from_slice(&1_u16.to_le_bytes());
190        header.extend_from_slice(&1_u16.to_le_bytes());
191        header.extend_from_slice(&22_050_u32.to_le_bytes());
192        header.extend_from_slice(&22_050_u32.to_le_bytes());
193        header.extend_from_slice(&1_u16.to_le_bytes());
194        header.extend_from_slice(&8_u16.to_le_bytes());
195        header.extend_from_slice(&0_u16.to_le_bytes());
196        header.extend_from_slice(b"fact");
197        header.extend_from_slice(&4_u32.to_le_bytes());
198        header.extend_from_slice(&0_u32.to_le_bytes());
199        header.extend_from_slice(b"data");
200        header.extend_from_slice(&0_u32.to_le_bytes());
201        header
202    }
203
204    #[test]
205    fn mp3_in_wav_output_opens_with_the_header_vanilla_writes() {
206        let mp3 = vec![0xFF, 0xFB, 0x38, 0xC4, 0x00, 0x01];
207        let bytes = write_wav_to_vec(&Wav::new_mp3(WavType::Vo, mp3.clone())).expect("write");
208
209        let header = header_from_the_page();
210        assert_eq!(header.len(), 58, "the page's table spans bytes 0 to 0x3A");
211        assert_eq!(&bytes[..58], header.as_slice());
212        assert_eq!(&bytes[58..], mp3.as_slice());
213    }
214
215    #[test]
216    fn mp3_in_wav_output_reads_back_and_writes_the_same_bytes() {
217        let first = write_wav_to_vec(&Wav::new_mp3(WavType::Vo, vec![0xFF, 0xFB, 0x90, 0x40]))
218            .expect("first write");
219        let reread = read_wav_from_bytes(&first).expect("read");
220        assert_eq!(reread.audio_format, WavAudioFormat::Mp3);
221        let second = write_wav_to_vec(&reread).expect("second write");
222        assert_eq!(first, second);
223    }
224
225    #[test]
226    fn a_wave_payload_has_no_mp3_in_wav_form() {
227        let wave = Wav::new_wave(
228            WavType::Vo,
229            WavWaveMetadata {
230                encoding: WavEncodingCode::from(WavEncoding::Pcm),
231                channels: 1,
232                sample_rate: 22_050,
233                bytes_per_sec: 22_050,
234                block_align: 1,
235                bits_per_sample: 8,
236            },
237            vec![0x80; 4],
238        );
239        assert!(matches!(
240            write_wav_to_vec(&wave),
241            Err(WavError::MismatchedPayload {
242                form: WavType::Vo,
243                payload: WavAudioFormat::Wave,
244            })
245        ));
246    }
247
248    #[test]
249    fn an_mp3_payload_has_no_plain_or_prefixed_form() {
250        for form in [WavType::Standard, WavType::Sfx] {
251            let mp3 = Wav::new_mp3(form, vec![0xFF, 0xFB, 0x90, 0x40]);
252            assert!(
253                matches!(
254                    write_wav_to_vec(&mp3),
255                    Err(WavError::MismatchedPayload {
256                        payload: WavAudioFormat::Mp3,
257                        ..
258                    })
259                ),
260                "{form}"
261            );
262        }
263    }
264
265    #[test]
266    fn an_mp3_in_wav_with_no_mp3_is_refused() {
267        // The header alone would read back as a plain empty WAVE.
268        assert!(matches!(
269            write_wav_to_vec(&Wav::new_mp3(WavType::Vo, Vec::new())),
270            Err(WavError::EmptyMp3)
271        ));
272    }
273
274    #[test]
275    fn clean_mode_writes_any_pair() {
276        let options = WavWriteOptions {
277            mode: WavWriteMode::Clean,
278        };
279        let mp3 = Wav::new_mp3(WavType::Sfx, vec![0xFF, 0xFB]);
280        assert_eq!(
281            write_wav_to_vec_with_options(&mp3, options).expect("clean write"),
282            vec![0xFF, 0xFB]
283        );
284    }
285}