#[cfg(feature = "microphone")]
mod microphone;
mod wav_replay;
use std::time::Duration;
use crate::audio::{CaptureError, RecordingReader, ResampleError, StopSignal};
use crate::voice_activity::VoiceActivityError;
#[cfg(feature = "microphone")]
pub use microphone::{
Microphone, MicrophoneBuilder, PreparedMicrophone, WakeWordControl, WakeWordEvent,
WakeWordFailure, WakeWordMonitor, WakeWordSuspension, check_default_input_device,
};
pub use wav_replay::{ReplayPace, WavReplayCapture, WavReplayError};
const MIN_SILENCE_DURATION: Duration = Duration::from_millis(100);
const MAX_SILENCE_DURATION: Duration = Duration::from_secs(10);
const DEFAULT_SILENCE_DURATION: Duration = Duration::from_millis(1_500);
const MIN_AUTO_STOP: Duration = Duration::from_secs(3);
const MAX_AUTO_STOP: Duration = Duration::from_secs(60);
const DEFAULT_AUTO_STOP: Duration = Duration::from_secs(10);
pub trait AudioCapture: Send + Sync + 'static {
fn start(&self, mode: RecordingMode, stop: StopSignal)
-> Result<RecordingReader, CaptureError>;
}
impl<C: AudioCapture> AudioCapture for std::sync::Arc<C> {
fn start(
&self,
mode: RecordingMode,
stop: StopSignal,
) -> Result<RecordingReader, CaptureError> {
(**self).start(mode, stop)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordingMode {
StopOnSilence {
trailing_silence: SilenceDuration,
},
UntilStopped,
UntilQuiet {
auto_stop: AutoStop,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SilenceDuration(Duration);
impl SilenceDuration {
pub fn new(duration: Duration) -> Result<Self, SilenceDurationError> {
if !(MIN_SILENCE_DURATION..=MAX_SILENCE_DURATION).contains(&duration) {
return Err(SilenceDurationError::OutOfRange {
actual: duration,
minimum: MIN_SILENCE_DURATION,
maximum: MAX_SILENCE_DURATION,
});
}
if !duration.subsec_nanos().is_multiple_of(1_000_000) {
return Err(SilenceDurationError::SubmillisecondPrecision { actual: duration });
}
Ok(Self(duration))
}
pub fn get(self) -> Duration {
self.0
}
}
impl Default for SilenceDuration {
fn default() -> Self {
Self(DEFAULT_SILENCE_DURATION)
}
}
#[derive(Debug, thiserror::Error)]
pub enum SilenceDurationError {
#[error("silence duration must be between {minimum:?} and {maximum:?}; got {actual:?}")]
OutOfRange {
actual: Duration,
minimum: Duration,
maximum: Duration,
},
#[error("silence duration must use whole-millisecond precision; got {actual:?}")]
SubmillisecondPrecision { actual: Duration },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AutoStop(Duration);
impl AutoStop {
pub fn new(duration: Duration) -> Result<Self, AutoStopError> {
if !(MIN_AUTO_STOP..=MAX_AUTO_STOP).contains(&duration) {
return Err(AutoStopError::OutOfRange {
actual: duration,
minimum: MIN_AUTO_STOP,
maximum: MAX_AUTO_STOP,
});
}
if !duration.subsec_nanos().is_multiple_of(1_000_000) {
return Err(AutoStopError::SubmillisecondPrecision { actual: duration });
}
Ok(Self(duration))
}
pub fn get(self) -> Duration {
self.0
}
}
impl Default for AutoStop {
fn default() -> Self {
Self(DEFAULT_AUTO_STOP)
}
}
#[derive(Debug, thiserror::Error)]
pub enum AutoStopError {
#[error("auto-stop must be between {minimum:?} and {maximum:?}; got {actual:?}")]
OutOfRange {
actual: Duration,
minimum: Duration,
maximum: Duration,
},
#[error("auto-stop must use whole-millisecond precision; got {actual:?}")]
SubmillisecondPrecision { actual: Duration },
}
#[derive(Debug, thiserror::Error)]
pub enum MicrophoneError {
#[error("failed to prepare the recording spool directory")]
SpoolDirectory(#[source] crate::primitives::private_fs::PrivateDirectoryError),
#[error("no default microphone is available")]
NoInputDevice,
#[error("failed to read the default microphone configuration")]
DefaultConfiguration(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("failed to read the default microphone description")]
DeviceDescription(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("microphone input configuration has no channels")]
NoInputChannels,
#[error("microphone input configuration has a zero sample rate")]
ZeroInputSampleRate,
#[error(
"microphone input sample rate {actual} Hz exceeds the supported maximum of {maximum} Hz"
)]
InputSampleRateTooHigh { actual: u32, maximum: u32 },
#[error("microphone sample format {0} is not supported")]
UnsupportedSampleFormat(String),
#[error("failed to build the microphone input stream")]
BuildStream(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("failed to start the microphone input stream")]
StartStream(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("the microphone input stream failed")]
Stream(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("the microphone input stream ended unexpectedly")]
StreamEnded,
#[error("the microphone callback supplied an incomplete audio frame")]
IncompleteFrame,
#[error("the microphone worker fell behind and lost audio")]
Overrun,
#[error("failed to convert microphone audio")]
Resample(#[from] ResampleError),
#[error("microphone sample count exceeds the supported recording bound")]
SampleCountOverflow,
#[error("failed to start the microphone worker thread")]
SpawnWorker(#[source] std::io::Error),
#[error("the microphone worker thread is not running")]
WorkerGone,
#[error("the microphone worker recovered from an internal panic")]
WorkerPanicked,
#[error("voice-activity detection failed")]
VoiceActivity(#[source] VoiceActivityError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auto_stop_accepts_its_bounds_and_refuses_beyond_them() {
assert!(AutoStop::new(MIN_AUTO_STOP).is_ok());
assert!(AutoStop::new(MAX_AUTO_STOP).is_ok());
assert!(matches!(
AutoStop::new(MIN_AUTO_STOP - Duration::from_millis(1)),
Err(AutoStopError::OutOfRange { .. })
));
assert!(matches!(
AutoStop::new(MAX_AUTO_STOP + Duration::from_millis(1)),
Err(AutoStopError::OutOfRange { .. })
));
}
}