Skip to main content

aria_kit/audio/capture/
mod.rs

1//! Audio sources behind [`AudioCapture`]: the default microphone and WAV replay.
2
3#[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
26/// A source of canonical 16 kHz mono audio. Each implementation bounds how
27/// long a recording runs.
28pub trait AudioCapture: Send + Sync + 'static {
29    /// Starts capturing and returns a reader for the new recording. Returns
30    /// once capture is live or the recording has ended, and fails if it
31    /// ended in error.
32    ///
33    /// A live source writes from its own thread and polls `stop` there, so
34    /// the caller can transcribe while audio arrives.
35    fn start(&self, mode: RecordingMode, stop: StopSignal)
36    -> Result<RecordingReader, CaptureError>;
37}
38
39/// One backend serves every operation that records, so the operations share
40/// it rather than each opening a microphone of their own.
41impl<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    /// Records until stopped, or until `auto_stop` passes without speech.
58    /// Silence before the first word counts, and all audio is kept.
59    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}