aria_kit/audio/capture/microphone/
mod.rs1mod resampling;
4mod stream;
5mod transfer;
6mod wake_word;
7mod worker;
8
9use std::path::PathBuf;
10use std::sync::mpsc::Sender;
11use std::time::Duration;
12
13use crate::audio::recording;
14use crate::audio::spool;
15use crate::audio::{CaptureError, RecordingReader, StopSignal, Stopper};
16use crate::primitives::private_fs;
17use crate::voice_activity::{SileroVad, VoiceActivityDetector};
18use crate::wake_word::WakeWordDetector;
19
20use super::{AudioCapture, MicrophoneError, RecordingMode};
21
22pub use stream::check_default_input_device;
23pub use wake_word::{
24 WakeWordControl, WakeWordEvent, WakeWordFailure, WakeWordMonitor, WakeWordSuspension,
25};
26
27const DEFAULT_MAXIMUM_DURATION: Duration = Duration::from_secs(60);
28
29type Result<T> = std::result::Result<T, CaptureError>;
30
31pub struct Microphone {
35 commands: Sender<worker::Command>,
36}
37
38impl Microphone {
39 pub fn open() -> Result<Self> {
40 Self::builder().open()
41 }
42
43 pub fn builder() -> MicrophoneBuilder {
44 MicrophoneBuilder {
45 spool_dir: None,
46 maximum_duration: DEFAULT_MAXIMUM_DURATION,
47 }
48 }
49
50 pub fn record(&mut self, mode: RecordingMode) -> Result<(RecordingReader, Stopper)> {
55 let stopper = Stopper::new();
56 let recording = self.start(mode, StopSignal::from(stopper.clone()))?;
57 Ok((recording, stopper))
58 }
59}
60
61pub struct MicrophoneBuilder {
62 spool_dir: Option<PathBuf>,
63 maximum_duration: Duration,
64}
65
66impl MicrophoneBuilder {
67 pub fn spool_dir(mut self, directory: impl Into<PathBuf>) -> Self {
71 self.spool_dir = Some(directory.into());
72 self
73 }
74
75 pub fn maximum_duration(mut self, maximum_duration: Duration) -> Self {
79 self.maximum_duration = maximum_duration;
80 self
81 }
82
83 pub fn open(self) -> Result<Microphone> {
84 self.load()?.open()
85 }
86
87 pub fn load(self) -> Result<PreparedMicrophone> {
88 self.prepare(|| {
89 SileroVad::load().map_err(|error| MicrophoneError::VoiceActivity(error).into())
90 })
91 }
92
93 pub fn load_with<V>(self, vad: V) -> Result<PreparedMicrophone<V>>
98 where
99 V: VoiceActivityDetector + Send + 'static,
100 {
101 self.prepare(|| Ok(vad))
102 }
103
104 fn prepare<V>(self, vad: impl FnOnce() -> Result<V>) -> Result<PreparedMicrophone<V>> {
106 spool::maximum_samples(self.maximum_duration)?;
107 let spool_dir = match self.spool_dir {
108 Some(directory) => directory,
111 None => private_spool_dir()?,
112 };
113 Ok(PreparedMicrophone {
114 spool_dir,
115 maximum_duration: self.maximum_duration,
116 vad: vad()?,
117 })
118 }
119}
120
121pub struct PreparedMicrophone<V = SileroVad> {
124 spool_dir: PathBuf,
125 maximum_duration: Duration,
126 vad: V,
127}
128
129impl<V: VoiceActivityDetector + Send + 'static> PreparedMicrophone<V> {
130 pub fn open(self) -> Result<Microphone> {
131 let commands = worker::spawn(self.spool_dir, self.maximum_duration, self.vad)?;
132 Ok(Microphone { commands })
133 }
134
135 pub fn open_with_wake_word<W>(self, detector: W) -> Result<(Microphone, WakeWordMonitor)>
136 where
137 W: WakeWordDetector + Send + 'static,
138 {
139 let (commands, events) = worker::spawn_with_wake_word(
140 self.spool_dir,
141 self.maximum_duration,
142 self.vad,
143 detector,
144 )?;
145 let monitor = WakeWordMonitor::new(commands.clone(), events);
146 Ok((Microphone { commands }, monitor))
147 }
148}
149
150fn private_spool_dir() -> Result<PathBuf> {
151 let directory =
152 std::env::temp_dir().join(format!("aria-{}", rustix::process::getuid().as_raw()));
153 private_fs::prepare_directory(&directory).map_err(MicrophoneError::SpoolDirectory)?;
154 Ok(directory)
155}
156
157impl AudioCapture for Microphone {
158 fn start(&self, mode: RecordingMode, stop: StopSignal) -> Result<RecordingReader> {
159 let (mut recording, writer) = recording::ends();
160 self.commands
161 .send(worker::Command::Record(worker::RecordCommand {
162 mode,
163 writer,
164 stop,
165 }))
166 .map_err(|_command| MicrophoneError::WorkerGone)?;
167 recording.wait_for_capture()?;
171 Ok(recording)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::audio::SpoolError;
179
180 #[test]
181 fn a_maximum_duration_over_the_limit_is_refused_before_opening() {
182 let outcome = Microphone::builder()
183 .maximum_duration(crate::audio::MAX_RECORDING_DURATION + Duration::from_secs(1))
184 .open();
185
186 assert!(matches!(
187 outcome,
188 Err(CaptureError::Spool(SpoolError::InvalidDuration { .. }))
189 ));
190 }
191}