use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use crate::audio::StopSignal;
use crate::audio::recording;
use crate::audio::spool::{WavSpool, maximum_samples};
use crate::audio::wav::{
WavFileReader, check_declared_length, collect_samples, open_canonical_wav,
};
use crate::audio::{RecordedAudio, RecordingReader, canonical_samples};
use crate::primitives::file_inspection::{InspectedFile, InspectionError};
use super::RecordingMode;
use super::{AudioCapture, CaptureError};
type Result<T> = std::result::Result<T, WavReplayError>;
const PACED_CHUNK: Duration = Duration::from_millis(100);
const CHUNK_SAMPLES: usize = canonical_samples(PACED_CHUNK) as usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayPace {
Immediate,
Realtime,
}
#[derive(Debug)]
pub struct WavReplayCapture {
spool_dir: PathBuf,
maximum_duration: Duration,
pace: ReplayPace,
samples: Arc<Vec<i16>>,
}
impl WavReplayCapture {
pub fn new(
spool_dir: impl Into<PathBuf>,
maximum_duration: Duration,
wav_path: &Path,
pace: ReplayPace,
) -> std::result::Result<Self, CaptureError> {
let samples = read_replay_samples(wav_path, maximum_duration)?;
Ok(Self {
spool_dir: spool_dir.into(),
maximum_duration,
pace,
samples: Arc::new(samples),
})
}
}
impl AudioCapture for WavReplayCapture {
fn start(
&self,
_mode: RecordingMode,
stop: StopSignal,
) -> std::result::Result<RecordingReader, CaptureError> {
let mut recording = match self.pace {
ReplayPace::Immediate => {
RecordingReader::from_pcm16_in(&self.spool_dir, &self.samples)?
}
ReplayPace::Realtime => {
let (recording, writer) = recording::ends();
let spool = WavSpool::create(&self.spool_dir, self.maximum_duration, &writer)?;
let samples = Arc::clone(&self.samples);
writer.begin();
thread::Builder::new()
.name("aria-wav-replay".to_owned())
.spawn(move || {
let result = replay_paced(spool, &samples, &stop);
drop(stop);
writer.close(result);
})
.map_err(WavReplayError::SpawnReplay)?;
recording
}
};
recording.wait_for_capture()?;
Ok(recording)
}
}
impl WavReplayCapture {
pub fn check(
wav_path: &Path,
maximum_duration: Duration,
) -> std::result::Result<PathBuf, CaptureError> {
checked_reader(wav_path, maximum_duration).map(|(path, _unread)| path)
}
}
fn replay_paced(
mut spool: WavSpool,
samples: &[i16],
stop: &StopSignal,
) -> std::result::Result<RecordedAudio, CaptureError> {
let started = Instant::now();
for (index, batch) in samples.chunks(CHUNK_SAMPLES).enumerate() {
let due = started + PACED_CHUNK * index as u32;
if let Some(wait) = due.checked_duration_since(Instant::now()) {
thread::sleep(wait);
}
spool.write_samples(batch)?;
if stop.should_stop() {
break;
}
}
Ok(spool.finish()?)
}
#[derive(Debug, thiserror::Error)]
pub enum WavReplayError {
#[error("the capture WAV file cannot be used")]
UnusableFile(#[source] InspectionError),
#[error("failed to start the paced replay thread")]
SpawnReplay(#[source] std::io::Error),
}
fn validate_replay_file(path: &Path) -> Result<PathBuf> {
InspectedFile::inspect(path.to_path_buf())
.and_then(InspectedFile::require_regular_file)
.and_then(InspectedFile::require_trusted)
.map_err(WavReplayError::UnusableFile)
}
fn read_replay_samples(
path: &Path,
maximum_duration: Duration,
) -> std::result::Result<Vec<i16>, CaptureError> {
let (path, reader) = checked_reader(path, maximum_duration)?;
Ok(collect_samples(reader, &path)?)
}
fn checked_reader(
path: &Path,
maximum_duration: Duration,
) -> std::result::Result<(PathBuf, WavFileReader), CaptureError> {
maximum_samples(maximum_duration)?;
let path = validate_replay_file(path)?;
let reader = open_canonical_wav(&path)?;
check_declared_length(&reader, &path, maximum_duration)?;
Ok((path, reader))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::WavError;
use crate::audio::testing::{FIXTURE_SAMPLES, canonical_fixture, write_fixture};
use crate::audio::wav::wav_spec;
const MAXIMUM_DURATION: Duration = Duration::from_secs(10);
const PACE: ReplayPace = ReplayPace::Immediate;
#[test]
fn replays_the_fixture_as_recorded_audio() {
let directory = tempfile::tempdir().expect("tempdir must be creatable");
let fixture = canonical_fixture(directory.path());
let capture = WavReplayCapture::new(directory.path(), MAXIMUM_DURATION, &fixture, PACE)
.expect("fixture must load");
let mut recording = capture
.start(RecordingMode::UntilStopped, StopSignal::never())
.expect("replay must succeed");
let audio = recording
.finished_audio()
.expect("the replay already ended");
assert_eq!(audio.duration(), Duration::from_secs(1));
}
#[test]
fn a_paced_replay_arrives_while_the_reader_waits() {
let directory = tempfile::tempdir().expect("tempdir must be creatable");
let fixture = canonical_fixture(directory.path());
let capture = WavReplayCapture::new(
directory.path(),
MAXIMUM_DURATION,
&fixture,
ReplayPace::Realtime,
)
.expect("fixture must load");
let mut recording = capture
.start(RecordingMode::UntilStopped, StopSignal::never())
.expect("replay must start");
recording
.fill_through(CHUNK_SAMPLES as u64)
.expect("the first chunk arrives");
let arriving = recording.reached_end();
recording.fill().expect("the rest arrives");
assert!(!arriving, "the replay must not be over after one chunk");
assert_eq!(recording.samples().len(), FIXTURE_SAMPLES);
}
#[test]
fn rejects_a_non_canonical_sample_rate() {
let directory = tempfile::tempdir().expect("tempdir must be creatable");
let mut spec = wav_spec();
spec.sample_rate = 44_100;
let fixture = write_fixture(directory.path(), spec, FIXTURE_SAMPLES);
let error = WavReplayCapture::new(directory.path(), MAXIMUM_DURATION, &fixture, PACE)
.expect_err("a 44.1 kHz fixture must be rejected");
assert!(matches!(
error,
CaptureError::Wav(WavError::UnsupportedFormat { .. })
));
}
#[test]
fn rejects_a_fixture_writable_by_other_users() {
use std::os::unix::fs::PermissionsExt;
let directory = tempfile::tempdir().expect("tempdir must be creatable");
let fixture = canonical_fixture(directory.path());
std::fs::set_permissions(&fixture, std::fs::Permissions::from_mode(0o666))
.expect("fixture permissions must be settable");
let error = WavReplayCapture::new(directory.path(), MAXIMUM_DURATION, &fixture, PACE)
.expect_err("a group/other-writable fixture must be rejected");
assert!(matches!(
error,
CaptureError::WavReplay(WavReplayError::UnusableFile(
InspectionError::WritableByOthers { .. }
))
));
}
#[test]
fn rejects_a_relative_path() {
let error = WavReplayCapture::new(
"recordings",
MAXIMUM_DURATION,
Path::new("fixture.wav"),
PACE,
)
.expect_err("a relative path must be rejected");
assert!(matches!(
error,
CaptureError::WavReplay(WavReplayError::UnusableFile(
InspectionError::NotAbsolute { .. }
))
));
}
#[test]
fn rejects_a_fixture_longer_than_the_recording_timeout() {
let directory = tempfile::tempdir().expect("tempdir must be creatable");
let fixture = write_fixture(directory.path(), wav_spec(), 2 * FIXTURE_SAMPLES);
let error = WavReplayCapture::new(directory.path(), Duration::from_secs(1), &fixture, PACE)
.expect_err("an over-long fixture must be rejected");
assert!(matches!(error, CaptureError::Wav(WavError::TooLong { .. })));
}
}