1use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use super::recording::RecordedAudio;
8use super::{
9 CANONICAL_SAMPLE_RATE, CHANNELS, CaptureError, Clip, ClipError, MAX_RECORDING_DURATION,
10 RecordingReader, canonical_samples, pcm16_from_f32,
11};
12
13const BITS_PER_SAMPLE: u16 = 16;
14
15type Result<T> = std::result::Result<T, WavError>;
16
17pub(super) type WavFileReader = hound::WavReader<std::io::BufReader<std::fs::File>>;
18
19pub(super) fn wav_spec() -> hound::WavSpec {
20 mono_spec(CANONICAL_SAMPLE_RATE)
21}
22
23fn mono_spec(sample_rate: u32) -> hound::WavSpec {
24 hound::WavSpec {
25 channels: CHANNELS,
26 sample_rate,
27 bits_per_sample: BITS_PER_SAMPLE,
28 sample_format: hound::SampleFormat::Int,
29 }
30}
31
32impl RecordingReader {
33 pub fn from_wav_file(path: impl AsRef<Path>) -> std::result::Result<Self, CaptureError> {
40 let path = path.as_ref();
41 let file = std::fs::File::open(path).map_err(|source| WavError::Open {
42 path: path.to_path_buf(),
43 source: Box::new(source),
44 })?;
45 Self::from_open_wav(file, path)
46 }
47
48 pub fn from_open_wav(
51 file: std::fs::File,
52 path: impl AsRef<Path>,
53 ) -> std::result::Result<Self, CaptureError> {
54 let path = path.as_ref();
55 let reader = canonical_wav(file, path)?;
56 check_declared_length(&reader, path, MAX_RECORDING_DURATION)?;
57 let samples = collect_samples(reader, path)?;
58
59 let (recording, writer) = super::recording::ends();
60 writer.begin();
61 writer.push_pcm16(&samples);
62 let sample_count = samples.len() as u64;
63 writer.close(Ok(RecordedAudio::borrowed(
64 path.to_path_buf(),
65 sample_count,
66 )));
67 Ok(recording)
68 }
69}
70
71pub fn read_wav(path: impl AsRef<Path>) -> Result<Clip> {
75 let path = path.as_ref();
76 let reader = hound::WavReader::open(path).map_err(|source| WavError::Open {
77 path: path.to_path_buf(),
78 source: Box::new(source),
79 })?;
80 let spec = reader.spec();
81 let interleaved = decode(reader, path)?;
82 if interleaved.is_empty() {
83 return Err(WavError::Empty {
84 path: path.to_path_buf(),
85 });
86 }
87 let channels = usize::from(spec.channels);
88 let mono = match channels {
89 1 => interleaved,
90 _ => interleaved
91 .chunks_exact(channels)
92 .map(|frame| frame.iter().sum::<f32>() / channels as f32)
93 .collect(),
94 };
95 Clip::new(spec.sample_rate, mono).map_err(|source| WavError::SampleRate {
96 path: path.to_path_buf(),
97 source,
98 })
99}
100
101fn decode(reader: WavFileReader, path: &Path) -> Result<Vec<f32>> {
102 let spec = reader.spec();
103 let decoded: std::result::Result<Vec<f32>, hound::Error> =
104 match (spec.sample_format, spec.bits_per_sample) {
105 (hound::SampleFormat::Float, 32) => reader.into_samples::<f32>().collect(),
106 (hound::SampleFormat::Int, bits @ 8..=32) => {
107 let full_scale = 2_f32.powi(i32::from(bits) - 1);
108 reader
109 .into_samples::<i32>()
110 .map(|sample| sample.map(|value| value as f32 / full_scale))
111 .collect()
112 }
113 _ => {
114 return Err(WavError::UnsupportedEncoding {
115 path: path.to_path_buf(),
116 });
117 }
118 };
119 decoded.map_err(|source| WavError::Read {
120 path: path.to_path_buf(),
121 source: Box::new(source),
122 })
123}
124
125pub fn write_wav(path: impl AsRef<Path>, clip: &Clip) -> Result<()> {
128 let path = path.as_ref();
129 let failed = |source: hound::Error| WavError::Write {
130 path: path.to_path_buf(),
131 source: Box::new(source),
132 };
133 let mut writer =
134 hound::WavWriter::create(path, mono_spec(clip.sample_rate())).map_err(failed)?;
135 for &sample in clip.samples() {
136 writer
137 .write_sample(pcm16_from_f32(sample))
138 .map_err(failed)?;
139 }
140 writer.finalize().map_err(failed)
141}
142
143pub(super) fn open_canonical_wav(path: &Path) -> Result<WavFileReader> {
145 let file = std::fs::File::open(path).map_err(|source| WavError::Open {
146 path: path.to_path_buf(),
147 source: Box::new(source),
148 })?;
149 canonical_wav(file, path)
150}
151
152fn canonical_wav(file: std::fs::File, path: &Path) -> Result<WavFileReader> {
153 let reader =
154 hound::WavReader::new(std::io::BufReader::new(file)).map_err(|source| WavError::Open {
155 path: path.to_path_buf(),
156 source: Box::new(source),
157 })?;
158 if reader.spec() != wav_spec() {
159 return Err(WavError::UnsupportedFormat {
160 path: path.to_path_buf(),
161 });
162 }
163 Ok(reader)
164}
165
166pub(super) fn check_declared_length(
169 reader: &WavFileReader,
170 path: &Path,
171 maximum_duration: Duration,
172) -> Result<()> {
173 let declared_samples = u64::from(reader.len());
174 if declared_samples == 0 {
175 return Err(WavError::Empty {
176 path: path.to_path_buf(),
177 });
178 }
179 if declared_samples > canonical_samples(maximum_duration) {
180 return Err(WavError::TooLong {
181 path: path.to_path_buf(),
182 maximum_duration,
183 });
184 }
185 Ok(())
186}
187
188pub(super) fn collect_samples(mut reader: WavFileReader, path: &Path) -> Result<Vec<i16>> {
189 let samples: Vec<i16> = reader
190 .samples()
191 .collect::<std::result::Result<_, _>>()
192 .map_err(|source| WavError::Read {
193 path: path.to_path_buf(),
194 source: Box::new(source),
195 })?;
196 if samples.is_empty() {
197 return Err(WavError::Empty {
198 path: path.to_path_buf(),
199 });
200 }
201 Ok(samples)
202}
203
204#[derive(Debug, thiserror::Error)]
205pub enum WavError {
206 #[error("failed to open WAV file {}", path.display())]
207 Open {
208 path: PathBuf,
209 #[source]
210 source: Box<dyn std::error::Error + Send + Sync>,
211 },
212
213 #[error("WAV file must be 16 kHz mono 16-bit PCM: {}", path.display())]
214 UnsupportedFormat { path: PathBuf },
215
216 #[error("WAV file is not integer PCM up to 32 bits or 32-bit float: {}", path.display())]
217 UnsupportedEncoding { path: PathBuf },
218
219 #[error("WAV file {}: {source}", path.display())]
220 SampleRate {
221 path: PathBuf,
222 #[source]
223 source: ClipError,
224 },
225
226 #[error("failed to read WAV file {}", path.display())]
227 Read {
228 path: PathBuf,
229 #[source]
230 source: Box<dyn std::error::Error + Send + Sync>,
231 },
232
233 #[error("WAV file contains no samples: {}", path.display())]
234 Empty { path: PathBuf },
235
236 #[error("failed to write WAV file {}", path.display())]
237 Write {
238 path: PathBuf,
239 #[source]
240 source: Box<dyn std::error::Error + Send + Sync>,
241 },
242
243 #[error(
244 "WAV file is longer than the recording limit {maximum_duration:?}: {}",
245 path.display()
246 )]
247 TooLong {
248 path: PathBuf,
249 maximum_duration: Duration,
250 },
251}
252
253#[cfg(test)]
254mod tests {
255 use std::time::Duration;
256
257 use super::super::testing::{FIXTURE_SAMPLES, canonical_fixture, fixture_of, write_fixture};
258 use super::*;
259
260 #[test]
261 fn a_written_wav_reads_back() {
262 let directory = tempfile::tempdir().expect("tempdir must be creatable");
263 let path = directory.path().join("written.wav");
264 let clip = Clip::new(24_000, vec![0.0, 0.5, -0.5, 0.25]).expect("the rate is usable");
265
266 write_wav(&path, &clip).expect("the WAV is writable");
267
268 assert_eq!(read_wav(&path).expect("the WAV is readable"), clip);
269 }
270
271 #[test]
272 fn a_stereo_24_bit_wav_reads_as_mono_at_its_own_rate() {
273 let directory = tempfile::tempdir().expect("tempdir must be creatable");
274 let path = directory.path().join("stereo.wav");
275 let spec = hound::WavSpec {
276 channels: 2,
277 sample_rate: 44_100,
278 bits_per_sample: 24,
279 sample_format: hound::SampleFormat::Int,
280 };
281 let mut writer = hound::WavWriter::create(&path, spec).expect("the WAV is writable");
282 let half_scale = 1 << 22;
283 for sample in [half_scale, 0, -half_scale, 0] {
284 writer.write_sample(sample).expect("the sample is writable");
285 }
286 writer.finalize().expect("the WAV finalizes");
287
288 let clip = read_wav(&path).expect("the WAV is readable");
289
290 assert_eq!(clip.sample_rate(), 44_100);
291 assert_eq!(clip.samples(), [0.25, -0.25]);
292 }
293
294 #[test]
295 fn a_wav_declaring_an_absurd_rate_is_refused() {
296 let directory = tempfile::tempdir().expect("tempdir must be creatable");
297 let rate = 1_000_000_007;
298 let spec = hound::WavSpec {
299 channels: 1,
300 sample_rate: rate,
301 bits_per_sample: 16,
302 sample_format: hound::SampleFormat::Int,
303 };
304 let fixture = write_fixture(directory.path(), spec, 1);
305
306 let outcome = read_wav(&fixture);
307
308 assert!(matches!(
309 outcome,
310 Err(WavError::SampleRate {
311 source: ClipError::SampleRate(declared),
312 ..
313 }) if declared == rate
314 ));
315 }
316
317 #[test]
318 fn a_canonical_file_becomes_a_finished_recording() {
319 let directory = tempfile::tempdir().expect("tempdir must be creatable");
320 let fixture = canonical_fixture(directory.path());
321
322 let mut recording =
323 RecordingReader::from_wav_file(&fixture).expect("the fixture is canonical");
324
325 recording.fill().expect("the recording already ended");
326 assert_eq!(recording.samples().len(), FIXTURE_SAMPLES);
327 let audio = recording
328 .finished_audio()
329 .expect("the recording already ended");
330 assert_eq!(audio.path(), Some(fixture.as_path()));
331 assert_eq!(audio.duration(), Duration::from_secs(1));
332 }
333
334 #[test]
335 fn an_open_wav_reads_its_descriptor_not_the_path() {
336 let directory = tempfile::tempdir().expect("tempdir must be creatable");
337 let fixture = canonical_fixture(directory.path());
338 let opened = std::fs::File::open(&fixture).expect("the fixture opens");
339 std::fs::remove_file(&fixture).expect("the path is removable");
340 fixture_of(directory.path(), 10);
341
342 let mut recording =
343 RecordingReader::from_open_wav(opened, &fixture).expect("the descriptor still reads");
344
345 recording.fill().expect("the recording already ended");
346 assert_eq!(recording.samples().len(), FIXTURE_SAMPLES);
347 }
348
349 #[test]
350 fn an_empty_file_is_refused() {
351 let directory = tempfile::tempdir().expect("tempdir must be creatable");
352 let fixture = fixture_of(directory.path(), 0);
353
354 let outcome = RecordingReader::from_wav_file(&fixture);
355
356 assert!(matches!(
357 outcome,
358 Err(CaptureError::Wav(WavError::Empty { .. }))
359 ));
360 }
361
362 #[test]
365 fn a_file_over_the_recording_limit_is_refused() {
366 let directory = tempfile::tempdir().expect("tempdir must be creatable");
367 let path = directory.path().join("huge.wav");
368 let over_limit_samples =
369 u32::try_from(crate::audio::MAX_RECORDING_SAMPLES + 1).expect("fits a header");
370 let data_bytes = over_limit_samples * 2;
371 let mut header = Vec::new();
372 header.extend_from_slice(b"RIFF");
373 header.extend_from_slice(&u32::MAX.to_le_bytes());
376 header.extend_from_slice(b"WAVEfmt ");
377 header.extend_from_slice(&16_u32.to_le_bytes());
378 header.extend_from_slice(&1_u16.to_le_bytes()); header.extend_from_slice(&1_u16.to_le_bytes()); header.extend_from_slice(&16_000_u32.to_le_bytes());
381 header.extend_from_slice(&32_000_u32.to_le_bytes()); header.extend_from_slice(&2_u16.to_le_bytes()); header.extend_from_slice(&16_u16.to_le_bytes()); header.extend_from_slice(b"data");
385 header.extend_from_slice(&data_bytes.to_le_bytes());
386 std::fs::write(&path, header).expect("the header must be writable");
387
388 let outcome = RecordingReader::from_wav_file(&path);
389
390 assert!(matches!(
391 outcome,
392 Err(CaptureError::Wav(WavError::TooLong { .. }))
393 ));
394 }
395
396 #[test]
397 fn a_non_canonical_file_is_refused() {
398 let directory = tempfile::tempdir().expect("tempdir must be creatable");
399 let spec = hound::WavSpec {
400 channels: 2,
401 sample_rate: 44_100,
402 bits_per_sample: 16,
403 sample_format: hound::SampleFormat::Int,
404 };
405 let fixture = write_fixture(directory.path(), spec, 100);
406
407 let outcome = RecordingReader::from_wav_file(&fixture);
408
409 assert!(matches!(
410 outcome,
411 Err(CaptureError::Wav(WavError::UnsupportedFormat { .. }))
412 ));
413 }
414}