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);
22
23/// A source of canonical 16 kHz mono audio. Each implementation bounds how
24/// long a recording runs.
25pub trait AudioCapture: Send + Sync + 'static {
26    /// Starts capturing and returns a reader for the new recording. Returns
27    /// once capture is live or the recording has ended, and fails if it
28    /// ended in error.
29    ///
30    /// A live source writes from its own thread and polls `stop` there, so
31    /// the caller can transcribe while audio arrives.
32    fn start(&self, mode: RecordingMode, stop: StopSignal)
33    -> Result<RecordingReader, CaptureError>;
34}
35
36/// One backend serves every operation that records, so the operations share
37/// it rather than each opening a microphone of their own.
38impl<C: AudioCapture> AudioCapture for std::sync::Arc<C> {
39    fn start(
40        &self,
41        mode: RecordingMode,
42        stop: StopSignal,
43    ) -> Result<RecordingReader, CaptureError> {
44        (**self).start(mode, stop)
45    }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum RecordingMode {
50    StopOnSilence { trailing_silence: SilenceDuration },
51    UntilStopped,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct SilenceDuration(Duration);
56
57impl SilenceDuration {
58    pub fn new(duration: Duration) -> Result<Self, SilenceDurationError> {
59        if !(MIN_SILENCE_DURATION..=MAX_SILENCE_DURATION).contains(&duration) {
60            return Err(SilenceDurationError::OutOfRange {
61                actual: duration,
62                minimum: MIN_SILENCE_DURATION,
63                maximum: MAX_SILENCE_DURATION,
64            });
65        }
66        if !duration.subsec_nanos().is_multiple_of(1_000_000) {
67            return Err(SilenceDurationError::SubmillisecondPrecision { actual: duration });
68        }
69        Ok(Self(duration))
70    }
71
72    pub fn get(self) -> Duration {
73        self.0
74    }
75}
76
77impl Default for SilenceDuration {
78    fn default() -> Self {
79        Self(DEFAULT_SILENCE_DURATION)
80    }
81}
82
83#[derive(Debug, thiserror::Error)]
84pub enum SilenceDurationError {
85    #[error("silence duration must be between {minimum:?} and {maximum:?}; got {actual:?}")]
86    OutOfRange {
87        actual: Duration,
88        minimum: Duration,
89        maximum: Duration,
90    },
91
92    #[error("silence duration must use whole-millisecond precision; got {actual:?}")]
93    SubmillisecondPrecision { actual: Duration },
94}
95
96#[derive(Debug, thiserror::Error)]
97pub enum MicrophoneError {
98    #[error("failed to prepare the recording spool directory")]
99    SpoolDirectory(#[source] crate::primitives::private_fs::PrivateDirectoryError),
100
101    #[error("no default microphone is available")]
102    NoInputDevice,
103
104    #[error("failed to read the default microphone configuration")]
105    DefaultConfiguration(#[source] Box<dyn std::error::Error + Send + Sync>),
106
107    #[error("failed to read the default microphone description")]
108    DeviceDescription(#[source] Box<dyn std::error::Error + Send + Sync>),
109
110    #[error("microphone input configuration has no channels")]
111    NoInputChannels,
112
113    #[error("microphone input configuration has a zero sample rate")]
114    ZeroInputSampleRate,
115
116    #[error(
117        "microphone input sample rate {actual} Hz exceeds the supported maximum of {maximum} Hz"
118    )]
119    InputSampleRateTooHigh { actual: u32, maximum: u32 },
120
121    #[error("microphone sample format {0} is not supported")]
122    UnsupportedSampleFormat(String),
123
124    #[error("failed to build the microphone input stream")]
125    BuildStream(#[source] Box<dyn std::error::Error + Send + Sync>),
126
127    #[error("failed to start the microphone input stream")]
128    StartStream(#[source] Box<dyn std::error::Error + Send + Sync>),
129
130    #[error("the microphone input stream failed")]
131    Stream(#[source] Box<dyn std::error::Error + Send + Sync>),
132
133    #[error("the microphone input stream ended unexpectedly")]
134    StreamEnded,
135
136    #[error("the microphone callback supplied an incomplete audio frame")]
137    IncompleteFrame,
138
139    #[error("the microphone worker fell behind and lost audio")]
140    Overrun,
141
142    #[error("failed to convert microphone audio")]
143    Resample(#[from] ResampleError),
144
145    #[error("microphone sample count exceeds the supported recording bound")]
146    SampleCountOverflow,
147
148    #[error("failed to start the microphone worker thread")]
149    SpawnWorker(#[source] std::io::Error),
150
151    #[error("the microphone worker thread is not running")]
152    WorkerGone,
153
154    #[error("the microphone worker recovered from an internal panic")]
155    WorkerPanicked,
156
157    #[error("voice-activity detection failed")]
158    VoiceActivity(#[source] VoiceActivityError),
159}