Skip to main content

aria_kit/audio/
wav.rs

1//! WAV files. Recordings read only the canonical 16 kHz mono 16-bit format;
2//! clips keep the file's own rate on read and write.
3
4use 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    /// Opens a caller-owned canonical 16 kHz mono WAV as a finished recording.
34    ///
35    /// The file is validated for format, not trust — trust policy is for
36    /// artifacts a daemon loads on a user's behalf, and this file is the
37    /// caller's own. The file is borrowed, never deleted, and read whole into
38    /// memory: about 8 bytes per sample, near 460 MB per hour of audio.
39    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    /// [`RecordingReader::from_wav_file`] over a `file` already open at
49    /// `path`, so checks made on that descriptor cover what is read.
50    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
71/// Reads a PCM WAV, 8- to 32-bit integer or 32-bit float, at any rate a
72/// [`Clip`] accepts, as mono audio at the file's rate. Channels are averaged.
73/// Unlike [`RecordingReader::from_wav_file`], it sets no length limit.
74pub 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
125/// Writes `clip` to `path` as mono 16-bit PCM at its own rate, replacing any
126/// file there.
127pub 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
143/// Opens the WAV and rejects anything but the canonical 16 kHz mono format.
144pub(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
166/// Rejects an empty or over-long file from its header, before reading any
167/// samples.
168pub(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    /// The header alone declares the length, so a file over the limit is
363    /// refused before any sample is read.
364    #[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        // The true RIFF size, 36 plus the data bytes, overflows a `u32` here,
374        // so the largest one stands in.
375        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()); // PCM
379        header.extend_from_slice(&1_u16.to_le_bytes()); // mono
380        header.extend_from_slice(&16_000_u32.to_le_bytes());
381        header.extend_from_slice(&32_000_u32.to_le_bytes()); // byte rate
382        header.extend_from_slice(&2_u16.to_le_bytes()); // block align
383        header.extend_from_slice(&16_u16.to_le_bytes()); // bits
384        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}