1use crate::{Result, error::Error};
2use std::io::{Read, Seek, SeekFrom};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum AudioType {
7 Mono8,
8 Mono16,
9 Stereo8,
10 Stereo16,
11}
12
13impl AudioType {
14 #[must_use]
15 pub(crate) const fn from_type_id(id: u32) -> Option<Self> {
16 match id {
17 0 => Some(Self::Mono8),
18 1 => Some(Self::Mono16),
19 2 => Some(Self::Stereo8),
20 3 => Some(Self::Stereo16),
21 _ => None,
22 }
23 }
24
25 #[must_use]
27 pub const fn channels(&self) -> u16 {
28 match self {
29 Self::Mono8 | Self::Mono16 => 1,
30 Self::Stereo8 | Self::Stereo16 => 2,
31 }
32 }
33
34 #[must_use]
36 pub const fn bits_per_sample(&self) -> u16 {
37 match self {
38 Self::Mono8 | Self::Stereo8 => 8,
39 Self::Mono16 | Self::Stereo16 => 16,
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct SfxEffect {
47 pub audio_type: AudioType,
48 pub sample_rate: u32,
49 pub base_volume: u8,
51 pub loop_flag: i8,
52 pub loop_offset: u32,
53 pub loop_end: u32,
54 pub pcm_data: Vec<u8>,
55}
56
57impl SfxEffect {
58 #[must_use]
60 pub fn duration_secs(&self) -> f64 {
61 let bytes_per_sample = (u32::from(self.audio_type.bits_per_sample()) / 8)
62 * u32::from(self.audio_type.channels());
63 if self.sample_rate == 0 || bytes_per_sample == 0 {
64 return 0.0;
65 }
66 let pcm_len = u32::try_from(self.pcm_data.len()).unwrap_or(u32::MAX);
67 f64::from(pcm_len) / (f64::from(self.sample_rate) * f64::from(bytes_per_sample))
68 }
69}
70
71#[derive(Debug, Clone)]
72pub struct SfxFile {
74 pub description: String,
75 pub effects: Vec<SfxEffect>,
76}
77
78fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
79 let bytes: [u8; 4] = data.get(offset..offset + 4)?.try_into().ok()?;
80 Some(u32::from_le_bytes(bytes))
81}
82
83fn read_u32_be(data: &[u8], offset: usize) -> Option<u32> {
84 let bytes: [u8; 4] = data.get(offset..offset + 4)?.try_into().ok()?;
85 Some(u32::from_be_bytes(bytes))
86}
87
88pub struct SfxIndex {
90 pub description: String,
91 pub effects: Vec<SfxEffect>,
92 ranges: Vec<(u64, usize)>,
93}
94impl SfxIndex {
95 pub fn read(reader: &mut (impl Read + Seek)) -> Result<Self> {
97 let length = reader.seek(SeekFrom::End(0))?;
98 reader.seek(SeekFrom::Start(0))?;
99 let mut input = [0; 52];
100 reader.read_exact(&mut input)?;
101 let input = &input[..];
102 if input.len() < 52 {
103 return Err(Error::Parse("SFX file too small for FXHD section".into()));
104 }
105
106 if &input[0..4] != b"FXHD" {
108 return Err(Error::Parse(format!(
109 "expected FXHD tag, got {:?}",
110 String::from_utf8_lossy(&input[0..4])
111 )));
112 }
113
114 let _section_size = read_u32_be(input, 4)
115 .ok_or_else(|| Error::Parse("failed to read FXHD section_size".into()))?;
116
117 let description = String::from_utf8_lossy(&input[8..40])
118 .trim_matches('\0')
119 .to_string();
120
121 let effect_count = usize::try_from(
122 read_u32_le(input, 40)
123 .ok_or_else(|| Error::Parse("failed to read FXHD effect_count".into()))?,
124 )
125 .map_err(|_| Error::Parse("FXHD effect_count does not fit usize".to_string()))?;
126
127 let fxdt_offset = 44;
129 if input.len() < fxdt_offset + 8 {
130 return Err(Error::Parse("SFX file too small for FXDT header".into()));
131 }
132 if &input[fxdt_offset..fxdt_offset + 4] != b"FXDT" {
133 return Err(Error::Parse(format!(
134 "expected FXDT tag, got {:?}",
135 String::from_utf8_lossy(&input[fxdt_offset..fxdt_offset + 4])
136 )));
137 }
138 let _fxdt_size = read_u32_be(input, fxdt_offset + 4)
139 .ok_or_else(|| Error::Parse("failed to read FXDT section_size".into()))?;
140
141 if effect_count as u64 > length.saturating_sub(52) / 27 {
142 return Err(Error::Parse("SFX effect count exceeds file length".into()));
143 }
144 let mut position = 52;
145 let mut effects = Vec::new();
146 let mut ranges = Vec::new();
147 for i in 0..effect_count {
148 reader.seek(SeekFrom::Start(position))?;
149 let mut bytes = [0; 27];
150 reader.read_exact(&mut bytes)?;
151 let input = &bytes[..];
152 let cursor = 0;
153 let type_id = read_u32_le(input, cursor)
154 .ok_or_else(|| Error::Parse(format!("effect {i}: failed to read type_id")))?;
155 let _bit_depth = read_u32_le(input, cursor + 4);
156 let sample_rate = read_u32_le(input, cursor + 8)
157 .ok_or_else(|| Error::Parse(format!("effect {i}: failed to read sample_rate")))?;
158 #[allow(clippy::cast_possible_wrap)]
159 let loop_flag = input[cursor + 13] as i8;
161 let base_volume = input[cursor + 12];
162 let loop_offset = read_u32_le(input, cursor + 14)
163 .ok_or_else(|| Error::Parse(format!("effect {i}: failed to read loop_offset")))?;
164 let loop_end = read_u32_le(input, cursor + 18)
165 .ok_or_else(|| Error::Parse(format!("effect {i}: failed to read loop_end")))?;
166 let data_length =
167 usize::try_from(read_u32_le(input, cursor + 22).ok_or_else(|| {
168 Error::Parse(format!("effect {i}: failed to read data_length"))
169 })?)
170 .map_err(|_| Error::Parse(format!("effect {i}: data_length does not fit usize")))?;
171
172 let audio_type = AudioType::from_type_id(type_id).ok_or_else(|| {
173 Error::Parse(format!("effect {i}: unknown audio type_id {type_id}"))
174 })?;
175
176 position += 27;
177 if data_length as u64 > length.saturating_sub(position) {
178 return Err(Error::Parse(format!(
179 "SFX effect {i}: PCM payload exceeds file length"
180 )));
181 }
182 ranges.push((position, data_length));
183 position += data_length as u64;
184 effects.push(SfxEffect {
185 audio_type,
186 sample_rate,
187 base_volume,
188 loop_flag,
189 loop_offset,
190 loop_end,
191 pcm_data: Vec::new(),
192 });
193 }
194 Ok(Self {
195 description,
196 effects,
197 ranges,
198 })
199 }
200 pub fn read_effect(&self, reader: &mut (impl Read + Seek), index: usize) -> Result<SfxEffect> {
202 let mut effect = self
203 .effects
204 .get(index)
205 .ok_or_else(|| Error::Parse(format!("SFX effect {index} missing")))?
206 .clone();
207 let (offset, length) = self.ranges[index];
208 reader.seek(SeekFrom::Start(offset))?;
209 effect.pcm_data.resize(length, 0);
210 reader.read_exact(&mut effect.pcm_data)?;
211 Ok(effect)
212 }
213}
214
215pub fn parse_sfx_file(input: &[u8]) -> Result<SfxFile> {
217 let mut reader = std::io::Cursor::new(input);
218 let index = SfxIndex::read(&mut reader)?;
219 let effects = (0..index.effects.len())
220 .map(|i| index.read_effect(&mut reader, i))
221 .collect::<Result<Vec<_>>>()?;
222 Ok(SfxFile {
223 description: index.description,
224 effects,
225 })
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 fn make_minimal_sfx(effect_count: u32, effects_data: &[u8]) -> Vec<u8> {
233 let mut buf = Vec::new();
234 buf.extend_from_slice(b"FXHD");
235 let fxhd_payload_size: u32 = 36;
236 buf.extend_from_slice(&fxhd_payload_size.to_be_bytes());
237 buf.extend_from_slice(&[0u8; 32]);
238 buf.extend_from_slice(&effect_count.to_le_bytes());
239 buf.extend_from_slice(b"FXDT");
240 let fxdt_size = u32::try_from(effects_data.len()).expect("effects data must fit u32");
241 buf.extend_from_slice(&fxdt_size.to_be_bytes());
242 buf.extend_from_slice(effects_data);
243 buf.extend_from_slice(b"END ");
244 buf
245 }
246
247 fn make_effect_bytes(type_id: u32, sample_rate: u32, pcm_len: u32) -> Vec<u8> {
248 let mut buf = Vec::new();
249 buf.extend_from_slice(&type_id.to_le_bytes());
250 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&sample_rate.to_le_bytes());
252 buf.push(64); buf.push(0); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); buf.extend_from_slice(&pcm_len.to_le_bytes());
257 buf.push(0); let pcm_size = usize::try_from(pcm_len).expect("pcm length must fit usize");
259 buf.extend_from_slice(&vec![0x80u8; pcm_size]);
260 buf
261 }
262
263 #[test]
264 fn parse_single_effect() {
265 let effect = make_effect_bytes(1, 22050, 100);
266 let sfx = make_minimal_sfx(1, &effect);
267 let file = parse_sfx_file(&sfx).unwrap();
268 assert_eq!(file.effects.len(), 1);
269 assert_eq!(file.effects[0].audio_type, AudioType::Mono16);
270 assert_eq!(file.effects[0].sample_rate, 22050);
271 assert_eq!(file.effects[0].base_volume, 64);
272 assert_eq!(file.effects[0].pcm_data.len(), 100);
273 }
274
275 #[test]
276 fn parse_multiple_effects() {
277 let mut data = Vec::new();
278 data.extend_from_slice(&make_effect_bytes(0, 11025, 50));
279 data.extend_from_slice(&make_effect_bytes(3, 22050, 200));
280 let sfx = make_minimal_sfx(2, &data);
281 let file = parse_sfx_file(&sfx).unwrap();
282 assert_eq!(file.effects.len(), 2);
283 assert_eq!(file.effects[0].audio_type, AudioType::Mono8);
284 assert_eq!(file.effects[1].audio_type, AudioType::Stereo16);
285 }
286
287 #[test]
288 fn rejects_truncated_file() {
289 let sfx = vec![0u8; 10];
290 assert!(parse_sfx_file(&sfx).is_err());
291 }
292}