aria_kit/audio/capture/
mod.rs1#[cfg(feature = "microphone")]
4mod microphone;
5mod wav_replay;
6
7use std::time::Duration;
8
9use crate::audio::{CaptureError, RecordingReader, ResampleError, StopSignal};
10use crate::voice_activity::VoiceActivityError;
11
12#[cfg(feature = "microphone")]
13pub use microphone::{
14 Microphone, MicrophoneBuilder, PreparedMicrophone, WakeWordControl, WakeWordEvent,
15 WakeWordFailure, WakeWordMonitor, WakeWordSuspension, check_default_input_device,
16};
17pub use wav_replay::{ReplayPace, WavReplayCapture, WavReplayError};
18
19const MIN_SILENCE_DURATION: Duration = Duration::from_millis(100);
20const MAX_SILENCE_DURATION: Duration = Duration::from_secs(10);
21const DEFAULT_SILENCE_DURATION: Duration = Duration::from_millis(1_500);
22const MIN_AUTO_STOP: Duration = Duration::from_secs(3);
23const MAX_AUTO_STOP: Duration = Duration::from_secs(60);
24const DEFAULT_AUTO_STOP: Duration = Duration::from_secs(10);
25
26pub trait AudioCapture: Send + Sync + 'static {
29 fn start(&self, mode: RecordingMode, stop: StopSignal)
36 -> Result<RecordingReader, CaptureError>;
37}
38
39impl<C: AudioCapture> AudioCapture for std::sync::Arc<C> {
42 fn start(
43 &self,
44 mode: RecordingMode,
45 stop: StopSignal,
46 ) -> Result<RecordingReader, CaptureError> {
47 (**self).start(mode, stop)
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum RecordingMode {
53 StopOnSilence {
54 trailing_silence: SilenceDuration,
55 },
56 UntilStopped,
57 UntilQuiet {
60 auto_stop: AutoStop,
61 },
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct SilenceDuration(Duration);
66
67impl SilenceDuration {
68 pub fn new(duration: Duration) -> Result<Self, SilenceDurationError> {
69 if !(MIN_SILENCE_DURATION..=MAX_SILENCE_DURATION).contains(&duration) {
70 return Err(SilenceDurationError::OutOfRange {
71 actual: duration,
72 minimum: MIN_SILENCE_DURATION,
73 maximum: MAX_SILENCE_DURATION,
74 });
75 }
76 if !duration.subsec_nanos().is_multiple_of(1_000_000) {
77 return Err(SilenceDurationError::SubmillisecondPrecision { actual: duration });
78 }
79 Ok(Self(duration))
80 }
81
82 pub fn get(self) -> Duration {
83 self.0
84 }
85}
86
87impl Default for SilenceDuration {
88 fn default() -> Self {
89 Self(DEFAULT_SILENCE_DURATION)
90 }
91}
92
93#[derive(Debug, thiserror::Error)]
94pub enum SilenceDurationError {
95 #[error("silence duration must be between {minimum:?} and {maximum:?}; got {actual:?}")]
96 OutOfRange {
97 actual: Duration,
98 minimum: Duration,
99 maximum: Duration,
100 },
101
102 #[error("silence duration must use whole-millisecond precision; got {actual:?}")]
103 SubmillisecondPrecision { actual: Duration },
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct AutoStop(Duration);
108
109impl AutoStop {
110 pub fn new(duration: Duration) -> Result<Self, AutoStopError> {
111 if !(MIN_AUTO_STOP..=MAX_AUTO_STOP).contains(&duration) {
112 return Err(AutoStopError::OutOfRange {
113 actual: duration,
114 minimum: MIN_AUTO_STOP,
115 maximum: MAX_AUTO_STOP,
116 });
117 }
118 if !duration.subsec_nanos().is_multiple_of(1_000_000) {
119 return Err(AutoStopError::SubmillisecondPrecision { actual: duration });
120 }
121 Ok(Self(duration))
122 }
123
124 pub fn get(self) -> Duration {
125 self.0
126 }
127}
128
129impl Default for AutoStop {
130 fn default() -> Self {
131 Self(DEFAULT_AUTO_STOP)
132 }
133}
134
135#[derive(Debug, thiserror::Error)]
136pub enum AutoStopError {
137 #[error("auto-stop must be between {minimum:?} and {maximum:?}; got {actual:?}")]
138 OutOfRange {
139 actual: Duration,
140 minimum: Duration,
141 maximum: Duration,
142 },
143
144 #[error("auto-stop must use whole-millisecond precision; got {actual:?}")]
145 SubmillisecondPrecision { actual: Duration },
146}
147
148#[derive(Debug, thiserror::Error)]
149pub enum MicrophoneError {
150 #[error("failed to prepare the recording spool directory")]
151 SpoolDirectory(#[source] crate::primitives::private_fs::PrivateDirectoryError),
152
153 #[error("no default microphone is available")]
154 NoInputDevice,
155
156 #[error("failed to read the default microphone configuration")]
157 DefaultConfiguration(#[source] Box<dyn std::error::Error + Send + Sync>),
158
159 #[error("failed to read the default microphone description")]
160 DeviceDescription(#[source] Box<dyn std::error::Error + Send + Sync>),
161
162 #[error("microphone input configuration has no channels")]
163 NoInputChannels,
164
165 #[error("microphone input configuration has a zero sample rate")]
166 ZeroInputSampleRate,
167
168 #[error(
169 "microphone input sample rate {actual} Hz exceeds the supported maximum of {maximum} Hz"
170 )]
171 InputSampleRateTooHigh { actual: u32, maximum: u32 },
172
173 #[error("microphone sample format {0} is not supported")]
174 UnsupportedSampleFormat(String),
175
176 #[error("failed to build the microphone input stream")]
177 BuildStream(#[source] Box<dyn std::error::Error + Send + Sync>),
178
179 #[error("failed to start the microphone input stream")]
180 StartStream(#[source] Box<dyn std::error::Error + Send + Sync>),
181
182 #[error("the microphone input stream failed")]
183 Stream(#[source] Box<dyn std::error::Error + Send + Sync>),
184
185 #[error("the microphone input stream ended unexpectedly")]
186 StreamEnded,
187
188 #[error("the microphone callback supplied an incomplete audio frame")]
189 IncompleteFrame,
190
191 #[error("the microphone worker fell behind and lost audio")]
192 Overrun,
193
194 #[error("failed to convert microphone audio")]
195 Resample(#[from] ResampleError),
196
197 #[error("microphone sample count exceeds the supported recording bound")]
198 SampleCountOverflow,
199
200 #[error("failed to start the microphone worker thread")]
201 SpawnWorker(#[source] std::io::Error),
202
203 #[error("the microphone worker thread is not running")]
204 WorkerGone,
205
206 #[error("the microphone worker recovered from an internal panic")]
207 WorkerPanicked,
208
209 #[error("voice-activity detection failed")]
210 VoiceActivity(#[source] VoiceActivityError),
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn auto_stop_accepts_its_bounds_and_refuses_beyond_them() {
219 assert!(AutoStop::new(MIN_AUTO_STOP).is_ok());
220 assert!(AutoStop::new(MAX_AUTO_STOP).is_ok());
221 assert!(matches!(
222 AutoStop::new(MIN_AUTO_STOP - Duration::from_millis(1)),
223 Err(AutoStopError::OutOfRange { .. })
224 ));
225 assert!(matches!(
226 AutoStop::new(MAX_AUTO_STOP + Duration::from_millis(1)),
227 Err(AutoStopError::OutOfRange { .. })
228 ));
229 }
230}