1use 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 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
51pub 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
59pub 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
75pub(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
89pub(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 #[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()); header.extend_from_slice(&1_u16.to_le_bytes()); header.extend_from_slice(&16_000_u32.to_le_bytes());
230 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");
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}