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, RIFF_MAGIC, SFX_HEADER_SIZE, SFX_MAGIC, VO_HEADER_SIZE, 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. The payload is built before anything is written, so these leave
31/// the writer untouched. [`WavError::Io`] when the writer fails.
32#[cfg_attr(
33    feature = "tracing",
34    tracing::instrument(level = "debug", skip(writer, wav, options))
35)]
36pub fn write_wav_with_options<W: Write>(
37    writer: &mut W,
38    wav: &Wav,
39    options: WavWriteOptions,
40) -> Result<(), WavError> {
41    let clean_payload = match wav.audio_format {
42        WavAudioFormat::Wave => build_clean_wave_payload(wav)?,
43        WavAudioFormat::Mp3 => wav.data.clone(),
44    };
45
46    let out = match options.mode {
47        WavWriteMode::Clean => clean_payload,
48        WavWriteMode::Game => obfuscate_for_game(&clean_payload, wav.wav_type),
49    };
50    writer.write_all(&out)?;
51    Ok(())
52}
53
54/// Serializes WAV data to a byte vector using [`WavWriteMode::Game`].
55///
56/// # Errors
57///
58/// The same as [`write_wav_to_vec_with_options`] under that mode.
59#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(wav)))]
60pub fn write_wav_to_vec(wav: &Wav) -> Result<Vec<u8>, WavError> {
61    write_wav_to_vec_with_options(wav, WavWriteOptions::default())
62}
63
64/// Serializes WAV data to a byte vector with explicit options.
65///
66/// # Errors
67///
68/// [`WavError::InvalidChunk`] on the terms [`write_wav_with_options`] gives.
69/// The `Vec` target has no I/O to fail at.
70#[cfg_attr(
71    feature = "tracing",
72    tracing::instrument(level = "debug", skip(wav, options))
73)]
74pub fn write_wav_to_vec_with_options(
75    wav: &Wav,
76    options: WavWriteOptions,
77) -> Result<Vec<u8>, WavError> {
78    let mut out = Vec::new();
79    write_wav_with_options(&mut out, wav, options)?;
80    Ok(out)
81}
82
83fn build_clean_wave_payload(wav: &Wav) -> Result<Vec<u8>, WavError> {
84    let data_size = u32::try_from(wav.data.len())
85        .map_err(|_| WavError::InvalidChunk("audio payload exceeds 4GiB RIFF limit".into()))?;
86
87    let block_align = if wav.block_align != 0 {
88        wav.block_align
89    } else {
90        default_block_align(wav.channels, wav.bits_per_sample)?
91    };
92    let bytes_per_sec = if wav.bytes_per_sec != 0 {
93        wav.bytes_per_sec
94    } else {
95        wav.sample_rate
96            .checked_mul(u32::from(block_align))
97            .ok_or_else(|| {
98                WavError::InvalidChunk("sample_rate * block_align overflows u32".into())
99            })?
100    };
101
102    let fmt_chunk_size: u32 = 16;
103    let riff_size = 4_u32
104        .checked_add(8)
105        .and_then(|value| value.checked_add(fmt_chunk_size))
106        .and_then(|value| value.checked_add(8))
107        .and_then(|value| value.checked_add(data_size))
108        .ok_or_else(|| WavError::InvalidChunk("RIFF size overflows u32".into()))?;
109
110    let mut out = Vec::with_capacity(
111        usize::try_from(8_u64 + u64::from(riff_size))
112            .unwrap_or(0)
113            .max(usize::from(44_u16)),
114    );
115    out.extend_from_slice(&RIFF_MAGIC);
116    out.extend_from_slice(&riff_size.to_le_bytes());
117    out.extend_from_slice(&WAVE_MAGIC);
118    out.extend_from_slice(&FMT_CHUNK_ID);
119    out.extend_from_slice(&fmt_chunk_size.to_le_bytes());
120    out.extend_from_slice(&wav.encoding.raw().to_le_bytes());
121    out.extend_from_slice(&wav.channels.to_le_bytes());
122    out.extend_from_slice(&wav.sample_rate.to_le_bytes());
123    out.extend_from_slice(&bytes_per_sec.to_le_bytes());
124    out.extend_from_slice(&block_align.to_le_bytes());
125    out.extend_from_slice(&wav.bits_per_sample.to_le_bytes());
126    out.extend_from_slice(&DATA_CHUNK_ID);
127    out.extend_from_slice(&data_size.to_le_bytes());
128    out.extend_from_slice(&wav.data);
129
130    Ok(out)
131}
132
133fn default_block_align(channels: u16, bits_per_sample: u16) -> Result<u16, WavError> {
134    let bytes_per_sample = bits_per_sample / 8;
135    channels
136        .checked_mul(bytes_per_sample)
137        .ok_or_else(|| WavError::InvalidChunk("channels * bytes_per_sample overflows u16".into()))
138}
139
140fn obfuscate_for_game(clean_payload: &[u8], wav_type: WavType) -> Vec<u8> {
141    // TODO(wav-decode): Keep codec-level decode/resample helpers out of this container module;
142    // add optional PCM/ADPCM decoding in higher-level playback/tooling APIs.
143    match wav_type {
144        WavType::Standard => clean_payload.to_vec(),
145        WavType::Sfx => {
146            // Deliberately asymmetric with the reader, which does not
147            // require this prefix because the engine does not either. It is
148            // emitted anyway: every retail SFX carries it, so writing it
149            // keeps this output comparable with shipped files, and other
150            // tools do key on it even though the game does not.
151            let mut out = Vec::with_capacity(SFX_HEADER_SIZE + clean_payload.len());
152            out.resize(SFX_HEADER_SIZE, 0);
153            out[0..4].copy_from_slice(&SFX_MAGIC);
154            out.extend_from_slice(clean_payload);
155            out
156        }
157        WavType::Vo => {
158            let mut out = Vec::with_capacity(VO_HEADER_SIZE + clean_payload.len());
159            out.resize(VO_HEADER_SIZE, 0);
160            out[0..4].copy_from_slice(&RIFF_MAGIC);
161            out.extend_from_slice(clean_payload);
162            out
163        }
164    }
165}