1use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::thread;
10use std::time::{Duration, Instant};
11
12use crate::audio::StopSignal;
13use crate::audio::recording;
14use crate::audio::spool::{WavSpool, maximum_samples};
15use crate::audio::wav::{
16 WavFileReader, check_declared_length, collect_samples, open_canonical_wav,
17};
18use crate::audio::{RecordedAudio, RecordingReader, canonical_samples};
19use crate::primitives::file_inspection::{InspectedFile, InspectionError};
20
21use super::RecordingMode;
22use super::{AudioCapture, CaptureError};
23
24type Result<T> = std::result::Result<T, WavReplayError>;
25
26const PACED_CHUNK: Duration = Duration::from_millis(100);
28const CHUNK_SAMPLES: usize = canonical_samples(PACED_CHUNK) as usize;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ReplayPace {
35 Immediate,
36 Realtime,
37}
38
39#[derive(Debug)]
40pub struct WavReplayCapture {
41 spool_dir: PathBuf,
42 maximum_duration: Duration,
43 pace: ReplayPace,
44 samples: Arc<Vec<i16>>,
45}
46
47impl WavReplayCapture {
48 pub fn new(
49 spool_dir: impl Into<PathBuf>,
50 maximum_duration: Duration,
51 wav_path: &Path,
52 pace: ReplayPace,
53 ) -> std::result::Result<Self, CaptureError> {
54 let samples = read_replay_samples(wav_path, maximum_duration)?;
55 Ok(Self {
56 spool_dir: spool_dir.into(),
57 maximum_duration,
58 pace,
59 samples: Arc::new(samples),
60 })
61 }
62}
63
64impl AudioCapture for WavReplayCapture {
65 fn start(
69 &self,
70 _mode: RecordingMode,
71 stop: StopSignal,
72 ) -> std::result::Result<RecordingReader, CaptureError> {
73 let mut recording = match self.pace {
74 ReplayPace::Immediate => {
77 RecordingReader::from_pcm16_in(&self.spool_dir, &self.samples)?
78 }
79 ReplayPace::Realtime => {
80 let (recording, writer) = recording::ends();
81 let spool = WavSpool::create(&self.spool_dir, self.maximum_duration, &writer)?;
82 let samples = Arc::clone(&self.samples);
83 writer.begin();
84 thread::Builder::new()
85 .name("aria-wav-replay".to_owned())
86 .spawn(move || {
87 let result = replay_paced(spool, &samples, &stop);
88 drop(stop);
91 writer.close(result);
92 })
93 .map_err(WavReplayError::SpawnReplay)?;
94 recording
95 }
96 };
97 recording.wait_for_capture()?;
98 Ok(recording)
99 }
100}
101
102impl WavReplayCapture {
103 pub fn check(
106 wav_path: &Path,
107 maximum_duration: Duration,
108 ) -> std::result::Result<PathBuf, CaptureError> {
109 checked_reader(wav_path, maximum_duration).map(|(path, _unread)| path)
110 }
111}
112
113fn replay_paced(
117 mut spool: WavSpool,
118 samples: &[i16],
119 stop: &StopSignal,
120) -> std::result::Result<RecordedAudio, CaptureError> {
121 let started = Instant::now();
122 for (index, batch) in samples.chunks(CHUNK_SAMPLES).enumerate() {
123 let due = started + PACED_CHUNK * index as u32;
124 if let Some(wait) = due.checked_duration_since(Instant::now()) {
125 thread::sleep(wait);
126 }
127 spool.write_samples(batch)?;
128 if stop.should_stop() {
129 break;
130 }
131 }
132 Ok(spool.finish()?)
133}
134
135#[derive(Debug, thiserror::Error)]
136pub enum WavReplayError {
137 #[error("the capture WAV file cannot be used")]
138 UnusableFile(#[source] InspectionError),
139
140 #[error("failed to start the paced replay thread")]
141 SpawnReplay(#[source] std::io::Error),
142}
143
144fn validate_replay_file(path: &Path) -> Result<PathBuf> {
147 InspectedFile::inspect(path.to_path_buf())
148 .and_then(InspectedFile::require_regular_file)
149 .and_then(InspectedFile::require_trusted)
150 .map_err(WavReplayError::UnusableFile)
151}
152
153fn read_replay_samples(
154 path: &Path,
155 maximum_duration: Duration,
156) -> std::result::Result<Vec<i16>, CaptureError> {
157 let (path, reader) = checked_reader(path, maximum_duration)?;
158 Ok(collect_samples(reader, &path)?)
159}
160
161fn checked_reader(
165 path: &Path,
166 maximum_duration: Duration,
167) -> std::result::Result<(PathBuf, WavFileReader), CaptureError> {
168 maximum_samples(maximum_duration)?;
169 let path = validate_replay_file(path)?;
170 let reader = open_canonical_wav(&path)?;
171 check_declared_length(&reader, &path, maximum_duration)?;
172 Ok((path, reader))
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::audio::WavError;
179 use crate::audio::testing::{FIXTURE_SAMPLES, canonical_fixture, write_fixture};
180 use crate::audio::wav::wav_spec;
181
182 const MAXIMUM_DURATION: Duration = Duration::from_secs(10);
183 const PACE: ReplayPace = ReplayPace::Immediate;
184
185 #[test]
186 fn replays_the_fixture_as_recorded_audio() {
187 let directory = tempfile::tempdir().expect("tempdir must be creatable");
188 let fixture = canonical_fixture(directory.path());
189 let capture = WavReplayCapture::new(directory.path(), MAXIMUM_DURATION, &fixture, PACE)
190 .expect("fixture must load");
191 let mut recording = capture
192 .start(RecordingMode::UntilStopped, StopSignal::never())
193 .expect("replay must succeed");
194 let audio = recording
195 .finished_audio()
196 .expect("the replay already ended");
197 assert_eq!(audio.duration(), Duration::from_secs(1));
198 }
199
200 #[test]
203 fn a_paced_replay_arrives_while_the_reader_waits() {
204 let directory = tempfile::tempdir().expect("tempdir must be creatable");
205 let fixture = canonical_fixture(directory.path());
206 let capture = WavReplayCapture::new(
207 directory.path(),
208 MAXIMUM_DURATION,
209 &fixture,
210 ReplayPace::Realtime,
211 )
212 .expect("fixture must load");
213
214 let mut recording = capture
215 .start(RecordingMode::UntilStopped, StopSignal::never())
216 .expect("replay must start");
217 recording
218 .fill_through(CHUNK_SAMPLES as u64)
219 .expect("the first chunk arrives");
220 let arriving = recording.reached_end();
221 recording.fill().expect("the rest arrives");
222
223 assert!(!arriving, "the replay must not be over after one chunk");
224 assert_eq!(recording.samples().len(), FIXTURE_SAMPLES);
225 }
226
227 #[test]
228 fn rejects_a_non_canonical_sample_rate() {
229 let directory = tempfile::tempdir().expect("tempdir must be creatable");
230 let mut spec = wav_spec();
231 spec.sample_rate = 44_100;
232 let fixture = write_fixture(directory.path(), spec, FIXTURE_SAMPLES);
233 let error = WavReplayCapture::new(directory.path(), MAXIMUM_DURATION, &fixture, PACE)
234 .expect_err("a 44.1 kHz fixture must be rejected");
235 assert!(matches!(
236 error,
237 CaptureError::Wav(WavError::UnsupportedFormat { .. })
238 ));
239 }
240
241 #[test]
242 fn rejects_a_fixture_writable_by_other_users() {
243 use std::os::unix::fs::PermissionsExt;
244
245 let directory = tempfile::tempdir().expect("tempdir must be creatable");
246 let fixture = canonical_fixture(directory.path());
247 std::fs::set_permissions(&fixture, std::fs::Permissions::from_mode(0o666))
248 .expect("fixture permissions must be settable");
249 let error = WavReplayCapture::new(directory.path(), MAXIMUM_DURATION, &fixture, PACE)
250 .expect_err("a group/other-writable fixture must be rejected");
251 assert!(matches!(
252 error,
253 CaptureError::WavReplay(WavReplayError::UnusableFile(
254 InspectionError::WritableByOthers { .. }
255 ))
256 ));
257 }
258
259 #[test]
260 fn rejects_a_relative_path() {
261 let error = WavReplayCapture::new(
262 "recordings",
263 MAXIMUM_DURATION,
264 Path::new("fixture.wav"),
265 PACE,
266 )
267 .expect_err("a relative path must be rejected");
268 assert!(matches!(
269 error,
270 CaptureError::WavReplay(WavReplayError::UnusableFile(
271 InspectionError::NotAbsolute { .. }
272 ))
273 ));
274 }
275
276 #[test]
277 fn rejects_a_fixture_longer_than_the_recording_timeout() {
278 let directory = tempfile::tempdir().expect("tempdir must be creatable");
279 let fixture = write_fixture(directory.path(), wav_spec(), 2 * FIXTURE_SAMPLES);
280 let error = WavReplayCapture::new(directory.path(), Duration::from_secs(1), &fixture, PACE)
281 .expect_err("an over-long fixture must be rejected");
282 assert!(matches!(error, CaptureError::Wav(WavError::TooLong { .. })));
283 }
284}