mod resampling;
mod stream;
mod transfer;
mod wake_word;
mod worker;
use std::path::PathBuf;
use std::sync::mpsc::Sender;
use std::time::Duration;
use crate::audio::recording;
use crate::audio::spool;
use crate::audio::{CaptureError, RecordingReader, StopSignal, Stopper};
use crate::primitives::private_fs;
use crate::voice_activity::{SileroVad, VoiceActivityDetector};
use crate::wake_word::WakeWordDetector;
use super::{AudioCapture, MicrophoneError, RecordingMode};
pub use stream::check_default_input_device;
pub use wake_word::{
WakeWordControl, WakeWordEvent, WakeWordFailure, WakeWordMonitor, WakeWordSuspension,
};
const DEFAULT_MAXIMUM_DURATION: Duration = Duration::from_secs(60);
type Result<T> = std::result::Result<T, CaptureError>;
pub struct Microphone {
commands: Sender<worker::Command>,
}
impl Microphone {
pub fn open() -> Result<Self> {
Self::builder().open()
}
pub fn builder() -> MicrophoneBuilder {
MicrophoneBuilder {
spool_dir: None,
maximum_duration: DEFAULT_MAXIMUM_DURATION,
}
}
pub fn record(&mut self, mode: RecordingMode) -> Result<(RecordingReader, Stopper)> {
let stopper = Stopper::new();
let recording = self.start(mode, StopSignal::from(stopper.clone()))?;
Ok((recording, stopper))
}
}
pub struct MicrophoneBuilder {
spool_dir: Option<PathBuf>,
maximum_duration: Duration,
}
impl MicrophoneBuilder {
pub fn spool_dir(mut self, directory: impl Into<PathBuf>) -> Self {
self.spool_dir = Some(directory.into());
self
}
pub fn maximum_duration(mut self, maximum_duration: Duration) -> Self {
self.maximum_duration = maximum_duration;
self
}
pub fn open(self) -> Result<Microphone> {
self.load()?.open()
}
pub fn load(self) -> Result<PreparedMicrophone> {
self.prepare(|| {
SileroVad::load().map_err(|error| MicrophoneError::VoiceActivity(error).into())
})
}
pub fn load_with<V>(self, vad: V) -> Result<PreparedMicrophone<V>>
where
V: VoiceActivityDetector + Send + 'static,
{
self.prepare(|| Ok(vad))
}
fn prepare<V>(self, vad: impl FnOnce() -> Result<V>) -> Result<PreparedMicrophone<V>> {
spool::maximum_samples(self.maximum_duration)?;
let spool_dir = match self.spool_dir {
Some(directory) => directory,
None => private_spool_dir()?,
};
Ok(PreparedMicrophone {
spool_dir,
maximum_duration: self.maximum_duration,
vad: vad()?,
})
}
}
pub struct PreparedMicrophone<V = SileroVad> {
spool_dir: PathBuf,
maximum_duration: Duration,
vad: V,
}
impl<V: VoiceActivityDetector + Send + 'static> PreparedMicrophone<V> {
pub fn open(self) -> Result<Microphone> {
let commands = worker::spawn(self.spool_dir, self.maximum_duration, self.vad)?;
Ok(Microphone { commands })
}
pub fn open_with_wake_word<W>(self, detector: W) -> Result<(Microphone, WakeWordMonitor)>
where
W: WakeWordDetector + Send + 'static,
{
let (commands, events) = worker::spawn_with_wake_word(
self.spool_dir,
self.maximum_duration,
self.vad,
detector,
)?;
let monitor = WakeWordMonitor::new(commands.clone(), events);
Ok((Microphone { commands }, monitor))
}
}
fn private_spool_dir() -> Result<PathBuf> {
let directory =
std::env::temp_dir().join(format!("aria-{}", rustix::process::getuid().as_raw()));
private_fs::prepare_directory(&directory).map_err(MicrophoneError::SpoolDirectory)?;
Ok(directory)
}
impl AudioCapture for Microphone {
fn start(&self, mode: RecordingMode, stop: StopSignal) -> Result<RecordingReader> {
let (mut recording, writer) = recording::ends();
self.commands
.send(worker::Command::Record(worker::RecordCommand {
mode,
writer,
stop,
}))
.map_err(|_command| MicrophoneError::WorkerGone)?;
recording.wait_for_capture()?;
Ok(recording)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::SpoolError;
#[test]
fn a_maximum_duration_over_the_limit_is_refused_before_opening() {
let outcome = Microphone::builder()
.maximum_duration(crate::audio::MAX_RECORDING_DURATION + Duration::from_secs(1))
.open();
assert!(matches!(
outcome,
Err(CaptureError::Spool(SpoolError::InvalidDuration { .. }))
));
}
}