1use 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#[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#[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#[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#[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 Ok(match (wav.wav_type, wav.audio_format) {
146 (WavType::Standard, WavAudioFormat::Wave) => clean_payload.to_vec(),
147 (WavType::Sfx, WavAudioFormat::Wave) => {
148 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 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 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 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}