Skip to main content

aria_kit/audio/capture/microphone/
stream.rs

1//! The default input device's stream, and the ring that carries its samples
2//! off the real-time callback.
3
4use std::collections::VecDeque;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
8use std::time::Duration;
9
10use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
11use cpal::{FromSample, Sample, SampleFormat, SizedSample, SupportedStreamConfig};
12use rtrb::{Consumer, Producer, RingBuffer};
13
14use crate::audio::samples_at;
15
16use super::resampling::SampleRateConverter;
17use super::worker::IdleObservation;
18use super::{MicrophoneError, Result};
19
20/// Rides out a page-in stall on the draining thread.
21const SAMPLE_RING_DURATION: Duration = Duration::from_secs(5);
22const MAX_INPUT_SAMPLE_RATE: u32 = 384_000;
23pub(super) const DRAIN_BUFFER_SAMPLES: usize = 1_024;
24const STREAM_INITIALIZATION_TIMEOUT: Duration = Duration::from_secs(2);
25
26/// A playing microphone stream and the state that decodes it.
27pub(super) struct LiveStream {
28    /// Held only to keep the input stream playing.
29    _stream: cpal::Stream,
30    consumer: Consumer<i16>,
31    status: Arc<CaptureStatus>,
32    converter: SampleRateConverter,
33    errors: Receiver<cpal::Error>,
34}
35
36/// Checks the default microphone as `LiveStream::open` does, without opening
37/// a stream, because that triggers the permission prompt. Returns the device
38/// name.
39pub fn check_default_input_device() -> Result<String> {
40    let device = cpal::default_host()
41        .default_input_device()
42        .ok_or(MicrophoneError::NoInputDevice)?;
43    // Not `Display`: it panics if the description fails, which happens when
44    // the device was unplugged after it was chosen.
45    let description = device
46        .description()
47        .map_err(|source| MicrophoneError::DeviceDescription(Box::new(source)))?;
48    select_input_config(&device)?;
49    Ok(description.name().to_owned())
50}
51
52impl LiveStream {
53    pub(super) fn open() -> Result<Self> {
54        let host = cpal::default_host();
55        let device = host
56            .default_input_device()
57            .ok_or(MicrophoneError::NoInputDevice)?;
58        let config = select_input_config(&device)?;
59        let sample_rate = config.sample_rate();
60        let (producer, consumer, status) = sample_ring(sample_rate, config.channels())?;
61        let converter = SampleRateConverter::new(sample_rate)?;
62        let (error_sender, errors) = sync_channel(1);
63        let stream = build_stream(&device, config, producer, error_sender)?;
64
65        stream
66            .play()
67            .map_err(|source| MicrophoneError::StartStream(Box::new(source)))?;
68        Ok(Self {
69            _stream: stream,
70            consumer,
71            status,
72            converter,
73            errors,
74        })
75    }
76
77    pub(super) fn pop<'samples>(
78        &mut self,
79        destination: &'samples mut [i16],
80    ) -> Result<&'samples mut [i16]> {
81        Ok(self.converter.pop(&mut self.consumer, destination)?)
82    }
83
84    pub(super) fn check_state(&self) -> Result<()> {
85        self.check_failures()?;
86        if self.consumer.is_abandoned() {
87            return Err(MicrophoneError::StreamEnded.into());
88        }
89        Ok(())
90    }
91
92    pub(super) fn check_failures(&self) -> Result<()> {
93        if self.status.incomplete_frame.load(Ordering::Acquire) {
94            return Err(MicrophoneError::IncompleteFrame.into());
95        }
96        if self.status.overrun.load(Ordering::Acquire) {
97            return Err(MicrophoneError::Overrun.into());
98        }
99        if let Ok(error) = self.errors.try_recv() {
100            return Err(MicrophoneError::Stream(Box::new(error)).into());
101        }
102        Ok(())
103    }
104
105    /// Moves pending samples into the bounded pre-press buffer, keeping its
106    /// newest `capacity` samples.
107    pub(super) fn drain_into(
108        &mut self,
109        pre_press: &mut VecDeque<i16>,
110        capacity: usize,
111        mut observe: impl FnMut(&[i16]) -> IdleObservation,
112    ) -> Result<()> {
113        let mut scratch = [0_i16; DRAIN_BUFFER_SAMPLES];
114        loop {
115            self.check_state()?;
116            let samples = self.pop(&mut scratch)?;
117            if samples.is_empty() {
118                return Ok(());
119            }
120            let kept = match observe(samples) {
121                IdleObservation::Continue => &samples[..],
122                // The wake word stays out of the recording; the command that
123                // follows it in the same chunk does not.
124                IdleObservation::Detected { command_start } => {
125                    pre_press.clear();
126                    &samples[command_start..]
127                }
128            };
129            pre_press.extend(kept.iter().copied());
130            if pre_press.len() > capacity {
131                pre_press.drain(..pre_press.len() - capacity);
132            }
133        }
134    }
135}
136
137struct CaptureProducer {
138    samples: Producer<i16>,
139    channels: u16,
140    status: Arc<CaptureStatus>,
141}
142
143impl CaptureProducer {
144    fn push<T>(&mut self, input: &[T])
145    where
146        T: Sample,
147        i16: FromSample<T>,
148    {
149        let mut frames = input.chunks_exact(usize::from(self.channels));
150        if !frames.remainder().is_empty() {
151            self.status.incomplete_frame.store(true, Ordering::Release);
152            return;
153        }
154
155        let frame_count = frames.len();
156        if self.samples.slots() < frame_count {
157            self.status.overrun.store(true, Ordering::Release);
158            return;
159        }
160
161        let mut converted = [0_i16; DRAIN_BUFFER_SAMPLES];
162        let mut converted_count = 0;
163        for frame in &mut frames {
164            converted[converted_count] = downmix(frame, self.channels);
165            converted_count += 1;
166
167            if converted_count == converted.len() {
168                self.push_converted(&converted);
169                converted_count = 0;
170            }
171        }
172        self.push_converted(&converted[..converted_count]);
173    }
174
175    fn push_converted(&mut self, samples: &[i16]) {
176        if self.samples.push_entire_slice(samples).is_err() {
177            self.status.overrun.store(true, Ordering::Release);
178        }
179    }
180}
181
182#[derive(Default)]
183struct CaptureStatus {
184    incomplete_frame: AtomicBool,
185    overrun: AtomicBool,
186}
187
188fn sample_ring(
189    sample_rate: u32,
190    channels: u16,
191) -> Result<(CaptureProducer, Consumer<i16>, Arc<CaptureStatus>)> {
192    let capacity = duration_sample_count(SAMPLE_RING_DURATION, sample_rate)?;
193    let (producer, consumer) = RingBuffer::new(capacity);
194    let status = Arc::new(CaptureStatus::default());
195
196    Ok((
197        CaptureProducer {
198            samples: producer,
199            channels,
200            status: Arc::clone(&status),
201        },
202        consumer,
203        status,
204    ))
205}
206
207fn select_input_config(device: &cpal::Device) -> Result<SupportedStreamConfig> {
208    let config = device
209        .default_input_config()
210        .map_err(|source| MicrophoneError::DefaultConfiguration(Box::new(source)))?;
211    if config.channels() == 0 {
212        return Err(MicrophoneError::NoInputChannels.into());
213    }
214    let sample_rate = config.sample_rate();
215    if sample_rate == 0 {
216        return Err(MicrophoneError::ZeroInputSampleRate.into());
217    }
218    if sample_rate > MAX_INPUT_SAMPLE_RATE {
219        return Err(MicrophoneError::InputSampleRateTooHigh {
220            actual: sample_rate,
221            maximum: MAX_INPUT_SAMPLE_RATE,
222        }
223        .into());
224    }
225    if !supported_sample_format(config.sample_format()) {
226        return Err(
227            MicrophoneError::UnsupportedSampleFormat(config.sample_format().to_string()).into(),
228        );
229    }
230    tracing::debug!(
231        sample_rate,
232        channels = config.channels(),
233        sample_format = ?config.sample_format(),
234        "selected native microphone configuration"
235    );
236    Ok(config)
237}
238
239fn duration_sample_count(duration: Duration, sample_rate: u32) -> Result<usize> {
240    usize::try_from(samples_at(duration, sample_rate))
241        .map_err(|_error| MicrophoneError::SampleCountOverflow.into())
242}
243
244fn supported_sample_format(format: SampleFormat) -> bool {
245    matches!(
246        format,
247        SampleFormat::I16 | SampleFormat::F32 | SampleFormat::U16
248    )
249}
250
251fn build_stream(
252    device: &cpal::Device,
253    config: SupportedStreamConfig,
254    producer: CaptureProducer,
255    errors: SyncSender<cpal::Error>,
256) -> Result<cpal::Stream> {
257    match config.sample_format() {
258        SampleFormat::I16 => build_typed_stream::<i16>(device, config, producer, errors),
259        SampleFormat::F32 => build_typed_stream::<f32>(device, config, producer, errors),
260        SampleFormat::U16 => build_typed_stream::<u16>(device, config, producer, errors),
261        format => Err(MicrophoneError::UnsupportedSampleFormat(format.to_string()).into()),
262    }
263}
264
265fn build_typed_stream<T>(
266    device: &cpal::Device,
267    config: SupportedStreamConfig,
268    mut producer: CaptureProducer,
269    errors: SyncSender<cpal::Error>,
270) -> Result<cpal::Stream>
271where
272    T: Sample + SizedSample,
273    i16: FromSample<T>,
274{
275    device
276        .build_input_stream(
277            config.config(),
278            move |input: &[T], _info| producer.push(input),
279            move |error| {
280                errors.try_send(error).ok();
281            },
282            Some(STREAM_INITIALIZATION_TIMEOUT),
283        )
284        .map_err(|source| MicrophoneError::BuildStream(Box::new(source)).into())
285}
286
287fn downmix<T>(frame: &[T], channels: u16) -> i16
288where
289    T: Sample,
290    i16: FromSample<T>,
291{
292    let sum = frame
293        .iter()
294        .map(|&sample| i64::from(i16::from_sample(sample)))
295        .sum::<i64>();
296    let average = sum / i64::from(channels);
297    match i16::try_from(average) {
298        Ok(sample) => sample,
299        Err(_error) if average.is_negative() => i16::MIN,
300        Err(_error) => i16::MAX,
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn downmix_averages_the_channels() {
310        assert_eq!(downmix(&[1_000_i16, 3_000], 2), 2_000);
311        assert_eq!(downmix(&[-400_i16, 0, 400, -800], 4), -200);
312    }
313}