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