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