Skip to main content

aria_kit/audio/
wav.rs

1//! The one WAV format this crate reads and writes: 16 kHz mono 16-bit PCM.
2
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6use super::recording::RecordedAudio;
7use super::{
8    CANONICAL_SAMPLE_RATE, CHANNELS, CaptureError, MAX_RECORDING_DURATION, RecordingReader,
9    canonical_samples, f32_from_pcm16, pcm16_from_f32,
10};
11
12const BITS_PER_SAMPLE: u16 = 16;
13
14type Result<T> = std::result::Result<T, WavError>;
15
16pub(super) type WavFileReader = hound::WavReader<std::io::BufReader<std::fs::File>>;
17
18pub(super) fn wav_spec() -> hound::WavSpec {
19    hound::WavSpec {
20        channels: CHANNELS,
21        sample_rate: CANONICAL_SAMPLE_RATE,
22        bits_per_sample: BITS_PER_SAMPLE,
23        sample_format: hound::SampleFormat::Int,
24    }
25}
26
27impl RecordingReader {
28    /// Opens a caller-owned canonical 16 kHz mono WAV as a finished recording.
29    ///
30    /// The file is validated for format, not trust — trust policy is for
31    /// artifacts a daemon loads on a user's behalf, and this file is the
32    /// caller's own. The file is borrowed, never deleted.
33    pub fn from_wav_file(path: impl AsRef<Path>) -> std::result::Result<Self, CaptureError> {
34        let path = path.as_ref();
35        let reader = open_canonical_wav(path)?;
36        check_declared_length(&reader, path, MAX_RECORDING_DURATION)?;
37        let samples = collect_samples(reader, path)?;
38
39        let (recording, writer) = super::recording::ends();
40        writer.begin();
41        writer.push_pcm16(&samples);
42        let sample_count = samples.len() as u64;
43        writer.close(Ok(RecordedAudio::borrowed(
44            path.to_path_buf(),
45            sample_count,
46        )));
47        Ok(recording)
48    }
49}
50
51/// Reads every sample of a canonical WAV. Unlike
52/// [`RecordingReader::from_wav_file`], it sets no length limit.
53pub fn read_wav(path: impl AsRef<Path>) -> Result<Vec<f32>> {
54    let path = path.as_ref();
55    let samples = collect_samples(open_canonical_wav(path)?, path)?;
56    Ok(samples.into_iter().map(f32_from_pcm16).collect())
57}
58
59/// Writes `samples` to `path` as a canonical WAV, replacing any file there.
60pub fn write_wav(path: impl AsRef<Path>, samples: &[f32]) -> Result<()> {
61    let path = path.as_ref();
62    let failed = |source: hound::Error| WavError::Write {
63        path: path.to_path_buf(),
64        source: Box::new(source),
65    };
66    let mut writer = hound::WavWriter::create(path, wav_spec()).map_err(failed)?;
67    for &sample in samples {
68        writer
69            .write_sample(pcm16_from_f32(sample))
70            .map_err(failed)?;
71    }
72    writer.finalize().map_err(failed)
73}
74
75/// Opens the WAV and rejects anything but the canonical 16 kHz mono format.
76pub(super) fn open_canonical_wav(path: &Path) -> Result<WavFileReader> {
77    let reader = hound::WavReader::open(path).map_err(|source| WavError::Open {
78        path: path.to_path_buf(),
79        source: Box::new(source),
80    })?;
81    if reader.spec() != wav_spec() {
82        return Err(WavError::UnsupportedFormat {
83            path: path.to_path_buf(),
84        });
85    }
86    Ok(reader)
87}
88
89/// Rejects an empty or over-long file from its header, before reading any
90/// samples.
91pub(super) fn check_declared_length(
92    reader: &WavFileReader,
93    path: &Path,
94    maximum_duration: Duration,
95) -> Result<()> {
96    let declared_samples = u64::from(reader.len());
97    if declared_samples == 0 {
98        return Err(WavError::Empty {
99            path: path.to_path_buf(),
100        });
101    }
102    if declared_samples > canonical_samples(maximum_duration) {
103        return Err(WavError::TooLong {
104            path: path.to_path_buf(),
105            maximum_duration,
106        });
107    }
108    Ok(())
109}
110
111pub(super) fn collect_samples(mut reader: WavFileReader, path: &Path) -> Result<Vec<i16>> {
112    let samples: Vec<i16> = reader
113        .samples()
114        .collect::<std::result::Result<_, _>>()
115        .map_err(|source| WavError::Read {
116            path: path.to_path_buf(),
117            source: Box::new(source),
118        })?;
119    if samples.is_empty() {
120        return Err(WavError::Empty {
121            path: path.to_path_buf(),
122        });
123    }
124    Ok(samples)
125}
126
127#[derive(Debug, thiserror::Error)]
128pub enum WavError {
129    #[error("failed to open WAV file {}", path.display())]
130    Open {
131        path: PathBuf,
132        #[source]
133        source: Box<dyn std::error::Error + Send + Sync>,
134    },
135
136    #[error("WAV file must be 16 kHz mono 16-bit PCM: {}", path.display())]
137    UnsupportedFormat { path: PathBuf },
138
139    #[error("failed to read WAV file {}", path.display())]
140    Read {
141        path: PathBuf,
142        #[source]
143        source: Box<dyn std::error::Error + Send + Sync>,
144    },
145
146    #[error("WAV file contains no samples: {}", path.display())]
147    Empty { path: PathBuf },
148
149    #[error("failed to write WAV file {}", path.display())]
150    Write {
151        path: PathBuf,
152        #[source]
153        source: Box<dyn std::error::Error + Send + Sync>,
154    },
155
156    #[error(
157        "WAV file is longer than the recording limit {maximum_duration:?}: {}",
158        path.display()
159    )]
160    TooLong {
161        path: PathBuf,
162        maximum_duration: Duration,
163    },
164}
165
166#[cfg(test)]
167mod tests {
168    use std::time::Duration;
169
170    use super::super::testing::{FIXTURE_SAMPLES, canonical_fixture, fixture_of, write_fixture};
171    use super::*;
172
173    #[test]
174    fn a_written_wav_reads_back() {
175        let directory = tempfile::tempdir().expect("tempdir must be creatable");
176        let path = directory.path().join("written.wav");
177        let samples = [0.0, 0.5, -0.5, 0.25];
178
179        write_wav(&path, &samples).expect("the WAV is writable");
180
181        assert_eq!(read_wav(&path).expect("the WAV is readable"), samples);
182    }
183
184    #[test]
185    fn a_canonical_file_becomes_a_finished_recording() {
186        let directory = tempfile::tempdir().expect("tempdir must be creatable");
187        let fixture = canonical_fixture(directory.path());
188
189        let mut recording =
190            RecordingReader::from_wav_file(&fixture).expect("the fixture is canonical");
191
192        recording.fill().expect("the recording already ended");
193        assert_eq!(recording.samples().len(), FIXTURE_SAMPLES);
194        let audio = recording
195            .finished_audio()
196            .expect("the recording already ended");
197        assert_eq!(audio.path(), Some(fixture.as_path()));
198        assert_eq!(audio.duration(), Duration::from_secs(1));
199    }
200
201    #[test]
202    fn an_empty_file_is_refused() {
203        let directory = tempfile::tempdir().expect("tempdir must be creatable");
204        let fixture = fixture_of(directory.path(), 0);
205
206        let outcome = RecordingReader::from_wav_file(&fixture);
207
208        assert!(matches!(
209            outcome,
210            Err(CaptureError::Wav(WavError::Empty { .. }))
211        ));
212    }
213
214    /// The header alone declares the length, so a file over the limit is
215    /// refused before any sample is read.
216    #[test]
217    fn a_file_over_the_recording_limit_is_refused() {
218        let directory = tempfile::tempdir().expect("tempdir must be creatable");
219        let path = directory.path().join("huge.wav");
220        let over_limit_samples: u32 = 15 * 60 * 16_000 + 1;
221        let data_bytes = over_limit_samples * 2;
222        let mut header = Vec::new();
223        header.extend_from_slice(b"RIFF");
224        header.extend_from_slice(&(36 + data_bytes).to_le_bytes());
225        header.extend_from_slice(b"WAVEfmt ");
226        header.extend_from_slice(&16_u32.to_le_bytes());
227        header.extend_from_slice(&1_u16.to_le_bytes()); // PCM
228        header.extend_from_slice(&1_u16.to_le_bytes()); // mono
229        header.extend_from_slice(&16_000_u32.to_le_bytes());
230        header.extend_from_slice(&32_000_u32.to_le_bytes()); // byte rate
231        header.extend_from_slice(&2_u16.to_le_bytes()); // block align
232        header.extend_from_slice(&16_u16.to_le_bytes()); // bits
233        header.extend_from_slice(b"data");
234        header.extend_from_slice(&data_bytes.to_le_bytes());
235        std::fs::write(&path, header).expect("the header must be writable");
236
237        let outcome = RecordingReader::from_wav_file(&path);
238
239        assert!(matches!(
240            outcome,
241            Err(CaptureError::Wav(WavError::TooLong { .. }))
242        ));
243    }
244
245    #[test]
246    fn a_non_canonical_file_is_refused() {
247        let directory = tempfile::tempdir().expect("tempdir must be creatable");
248        let spec = hound::WavSpec {
249            channels: 2,
250            sample_rate: 44_100,
251            bits_per_sample: 16,
252            sample_format: hound::SampleFormat::Int,
253        };
254        let fixture = write_fixture(directory.path(), spec, 100);
255
256        let outcome = RecordingReader::from_wav_file(&fixture);
257
258        assert!(matches!(
259            outcome,
260            Err(CaptureError::Wav(WavError::UnsupportedFormat { .. }))
261        ));
262    }
263}