1use std::io::{Read, Write};
9
10#[derive(Debug, thiserror::Error)]
12#[non_exhaustive]
13pub enum WavError {
14 #[error("io: {0}")]
16 Io(#[from] std::io::Error),
17 #[error("not a WAVE file")]
19 NotWave,
20 #[error("unsupported: {0}")]
22 Unsupported(String),
23 #[error("truncated")]
25 Truncated,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Wav {
31 pub sample_rate: u32,
33 pub samples: Vec<i16>,
35}
36
37impl Wav {
38 #[must_use]
40 pub fn narrowband(samples: Vec<i16>) -> Self {
41 Self {
42 sample_rate: 8000,
43 samples,
44 }
45 }
46
47 #[must_use]
49 pub fn duration(&self) -> std::time::Duration {
50 if self.sample_rate == 0 {
51 return std::time::Duration::ZERO;
52 }
53 std::time::Duration::from_nanos(
57 (self.samples.len() as u64).saturating_mul(1_000_000_000) / u64::from(self.sample_rate),
58 )
59 }
60}
61
62fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
63 Some(u32::from_le_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
64}
65
66fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
67 Some(u16::from_le_bytes(bytes.get(at..at + 2)?.try_into().ok()?))
68}
69
70pub fn read_wav(mut source: impl Read) -> Result<Wav, WavError> {
72 let mut bytes = Vec::new();
73 source.read_to_end(&mut bytes)?;
74
75 if bytes.get(..4) != Some(b"RIFF") || bytes.get(8..12) != Some(b"WAVE") {
76 return Err(WavError::NotWave);
77 }
78
79 let mut sample_rate = None;
80 let mut samples = None;
81 let mut offset = 12usize;
82
83 while offset + 8 <= bytes.len() {
87 let id = bytes.get(offset..offset + 4).ok_or(WavError::Truncated)?;
88 let size = u32_at(&bytes, offset + 4).ok_or(WavError::Truncated)? as usize;
89 let body_at = offset + 8;
90 let body = bytes
91 .get(body_at..body_at + size)
92 .ok_or(WavError::Truncated)?;
93
94 match id {
95 b"fmt " => {
96 let format = u16_at(body, 0).ok_or(WavError::Truncated)?;
97 let channels = u16_at(body, 2).ok_or(WavError::Truncated)?;
98 let rate = u32_at(body, 4).ok_or(WavError::Truncated)?;
99 let bits = u16_at(body, 14).ok_or(WavError::Truncated)?;
100
101 if format != 1 {
102 return Err(WavError::Unsupported(format!(
103 "format tag {format}; only uncompressed PCM is handled"
104 )));
105 }
106 if channels != 1 {
107 return Err(WavError::Unsupported(format!(
108 "{channels} channels; mono only"
109 )));
110 }
111 if bits != 16 {
112 return Err(WavError::Unsupported(format!("{bits}-bit; 16-bit only")));
113 }
114 sample_rate = Some(rate);
115 }
116 b"data" => {
117 samples = Some(
118 body.chunks_exact(2)
119 .map(|pair| {
120 i16::from_le_bytes([
121 pair.first().copied().unwrap_or(0),
122 pair.get(1).copied().unwrap_or(0),
123 ])
124 })
125 .collect::<Vec<i16>>(),
126 );
127 }
128 _ => {}
129 }
130
131 offset = body_at + size + (size % 2);
134 }
135
136 Ok(Wav {
137 sample_rate: sample_rate.ok_or(WavError::NotWave)?,
138 samples: samples.ok_or(WavError::NotWave)?,
139 })
140}
141
142pub fn write_wav(mut sink: impl Write, wav: &Wav) -> Result<(), WavError> {
144 let data_len = u32::try_from(wav.samples.len() * 2).unwrap_or(u32::MAX);
145 let byte_rate = wav.sample_rate * 2;
146
147 sink.write_all(b"RIFF")?;
148 sink.write_all(&(36 + data_len).to_le_bytes())?;
149 sink.write_all(b"WAVE")?;
150
151 sink.write_all(b"fmt ")?;
152 sink.write_all(&16u32.to_le_bytes())?;
153 sink.write_all(&1u16.to_le_bytes())?; sink.write_all(&1u16.to_le_bytes())?; sink.write_all(&wav.sample_rate.to_le_bytes())?;
156 sink.write_all(&byte_rate.to_le_bytes())?;
157 sink.write_all(&2u16.to_le_bytes())?; sink.write_all(&16u16.to_le_bytes())?; sink.write_all(b"data")?;
161 sink.write_all(&data_len.to_le_bytes())?;
162 for sample in &wav.samples {
163 sink.write_all(&sample.to_le_bytes())?;
164 }
165 Ok(())
166}
167
168#[cfg(test)]
169#[allow(
170 clippy::cast_possible_truncation,
171 clippy::unwrap_used,
172 clippy::expect_used,
173 clippy::panic,
174 clippy::indexing_slicing
175)]
176mod tests {
177 use super::*;
178
179 fn tone(samples: usize) -> Wav {
180 Wav::narrowband(
181 (0..samples)
182 .map(|i| {
183 let phase = f64::from(u32::try_from(i).unwrap_or(0))
184 * 2.0
185 * std::f64::consts::PI
186 * 440.0
187 / 8000.0;
188 let value = (phase.sin() * 16000.0).round();
189 i16::try_from(value as i32).unwrap_or(0)
190 })
191 .collect(),
192 )
193 }
194
195 #[test]
196 fn a_clip_survives_a_round_trip_exactly() {
197 let original = tone(800);
198 let mut buffer = Vec::new();
199 write_wav(&mut buffer, &original).expect("writes");
200 let read = read_wav(buffer.as_slice()).expect("reads");
201 assert_eq!(read, original);
202 }
203
204 #[test]
205 fn the_duration_follows_the_sample_count_and_rate() {
206 assert_eq!(tone(8000).duration(), std::time::Duration::from_secs(1));
207 assert_eq!(tone(4000).duration(), std::time::Duration::from_millis(500));
208 }
209
210 #[test]
213 fn chunks_between_fmt_and_data_are_skipped() {
214 let clip = Wav::narrowband(vec![1, -1, 2, -2]);
215 let mut buffer = Vec::new();
216 write_wav(&mut buffer, &clip).expect("writes");
217
218 let data_at = buffer
220 .windows(4)
221 .position(|w| w == b"data")
222 .expect("a data chunk");
223 let mut spliced = buffer[..data_at].to_vec();
224 spliced.extend_from_slice(b"LIST");
225 spliced.extend_from_slice(&8u32.to_le_bytes());
226 spliced.extend_from_slice(b"INFOxxxx");
227 spliced.extend_from_slice(&buffer[data_at..]);
228 let total = u32::try_from(spliced.len() - 8).expect("fits");
230 spliced[4..8].copy_from_slice(&total.to_le_bytes());
231
232 assert_eq!(read_wav(spliced.as_slice()).expect("reads"), clip);
233 }
234
235 #[test]
238 fn an_odd_sized_chunk_is_padded_to_a_word_boundary() {
239 let clip = Wav::narrowband(vec![7, -7]);
240 let mut buffer = Vec::new();
241 write_wav(&mut buffer, &clip).expect("writes");
242 let data_at = buffer
243 .windows(4)
244 .position(|w| w == b"data")
245 .expect("a data chunk");
246
247 let mut spliced = buffer[..data_at].to_vec();
248 spliced.extend_from_slice(b"ODDC");
249 spliced.extend_from_slice(&3u32.to_le_bytes());
250 spliced.extend_from_slice(b"abc\0"); spliced.extend_from_slice(&buffer[data_at..]);
252 let total = u32::try_from(spliced.len() - 8).expect("fits");
253 spliced[4..8].copy_from_slice(&total.to_le_bytes());
254
255 assert_eq!(read_wav(spliced.as_slice()).expect("reads"), clip);
256 }
257
258 #[test]
261 fn an_unsupported_format_is_refused_rather_than_misread() {
262 let mut stereo = Vec::new();
263 write_wav(&mut stereo, &Wav::narrowband(vec![1, 2, 3, 4])).expect("writes");
264 let fmt_at = stereo
266 .windows(4)
267 .position(|w| w == b"fmt ")
268 .expect("a fmt chunk");
269 stereo[fmt_at + 10..fmt_at + 12].copy_from_slice(&2u16.to_le_bytes());
270
271 match read_wav(stereo.as_slice()) {
272 Err(WavError::Unsupported(message)) => assert!(message.contains("channels")),
273 other => panic!("expected a refusal, got {other:?}"),
274 }
275 }
276
277 #[test]
278 fn a_file_that_is_not_a_wave_is_refused() {
279 assert!(matches!(
280 read_wav(b"this is not a wav file at all".as_slice()),
281 Err(WavError::NotWave)
282 ));
283 }
284
285 #[test]
286 fn a_truncated_chunk_is_an_error_not_a_partial_read() {
287 let clip = Wav::narrowband(vec![1, 2, 3, 4]);
288 let mut buffer = Vec::new();
289 write_wav(&mut buffer, &clip).expect("writes");
290 buffer.truncate(buffer.len() - 4);
291 assert!(matches!(
292 read_wav(buffer.as_slice()),
293 Err(WavError::Truncated)
294 ));
295 }
296
297 #[test]
298 fn an_empty_clip_round_trips() {
299 let clip = Wav::narrowband(Vec::new());
300 let mut buffer = Vec::new();
301 write_wav(&mut buffer, &clip).expect("writes");
302 assert_eq!(read_wav(buffer.as_slice()).expect("reads"), clip);
303 }
304}