//! Microphone capture. use std::time::Instant; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::{Sample, SampleFormat, SupportedStreamConfig}; use crossbeam_channel::Sender; use asist_core::config::AudioConfig; use asist_core::error::{Error, Result}; use crate::{describe, ASR_SAMPLE_RATE}; /// Format the device was actually opened with. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct InputFormat { pub sample_rate: usize, pub channels: usize, } /// A block as it comes out of the callback, with the timestamp that later /// lets us measure how far the pipeline lags behind the voice. #[derive(Debug)] pub struct CaptureBlock { pub samples: Vec, pub at: Instant, } pub struct Capture { stream: cpal::Stream, pub format: InputFormat, pub device_name: String, } impl Capture { /// Opens the input and starts pushing blocks through `tx`. /// /// It prefers 16 kHz mono because that is exactly what the model wants: if /// the device accepts it, there is no resampling anywhere on the path. pub fn open(config: &AudioConfig, tx: Sender) -> Result { let host = cpal::default_host(); let device = select_device(&host, &config.input_device)?; let device_name = describe(&device); let supported = preferred_config(&device)?; let format = InputFormat { sample_rate: supported.sample_rate() as usize, channels: supported.channels() as usize, }; let stream_config: cpal::StreamConfig = supported.clone().into(); let on_error = |err| tracing::error!(target: "audio", %err, "input stream"); // Inside the callback: convert to f32, send and leave. `send` on an // unbounded channel does not block, which is the only property that // matters here. macro_rules! build { ($sample:ty) => { device .build_input_stream( &stream_config, move |data: &[$sample], _: &_| { let samples = data.iter().map(|s| f32::from_sample(*s)).collect(); let _ = tx.send(CaptureBlock { samples, at: Instant::now(), }); }, on_error, None, ) .map_err(|e| Error::Audio(format!("could not open the input: {e}")))? }; } let stream = match supported.sample_format() { SampleFormat::F32 => build!(f32), SampleFormat::I16 => build!(i16), SampleFormat::U16 => build!(u16), other => { return Err(Error::Audio(format!( "unsupported sample format: {other:?}" ))) } }; stream .play() .map_err(|e| Error::Audio(format!("could not start the input: {e}")))?; tracing::info!( target: "audio", device = %device_name, hz = format.sample_rate, channels = format.channels, resampling = format.sample_rate != ASR_SAMPLE_RATE as usize || format.channels != 1, "entrada abierta" ); Ok(Self { stream, format, device_name, }) } /// Closes the device. Dropping the sender makes the chain of threads /// take itself apart from top to bottom. pub fn stop(self) { drop(self.stream); } } fn select_device(host: &cpal::Host, wanted: &str) -> Result { if wanted.is_empty() { return host .default_input_device() .ok_or_else(|| Error::Audio("no input device".into())); } let wanted_lower = wanted.to_lowercase(); let devices = host .input_devices() .map_err(|e| Error::Audio(format!("could not list the inputs: {e}")))?; let mut seen = Vec::new(); for device in devices { let name = describe(&device); if name.to_lowercase().contains(&wanted_lower) { return Ok(device); } seen.push(name); } Err(Error::Audio(format!( "no input matches «{wanted}». Available: {}", seen.join(", ") ))) } fn preferred_config(device: &cpal::Device) -> Result { let native = device .supported_input_configs() .map_err(|e| Error::Audio(format!("could not query the input: {e}")))? .filter(|range| range.channels() == 1) .filter(|range| { range.min_sample_rate() <= ASR_SAMPLE_RATE && ASR_SAMPLE_RATE <= range.max_sample_rate() }) .find(|range| range.sample_format() == SampleFormat::F32) .map(|range| range.with_sample_rate(ASR_SAMPLE_RATE)); match native { Some(config) => Ok(config), None => device .default_input_config() .map_err(|e| Error::Audio(format!("no input configuration: {e}"))), } }