use tokio::sync::mpsc;
use crate::types::AudioChunk;
#[derive(Debug, Clone)]
pub struct MicConfig {
pub sample_rate: u32,
pub channels: u16,
pub chunk_size: usize,
}
impl Default for MicConfig {
fn default() -> Self {
Self {
sample_rate: 16000,
channels: 1,
chunk_size: 1600, }
}
}
pub fn record(config: MicConfig) -> Result<(mpsc::Receiver<AudioChunk>, MicStopGuard), MicError> {
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or_else(|| MicError("no input device found".into()))?;
tracing::info!(device = ?device.name().unwrap_or_default(), "using input device");
let supported = device
.supported_input_configs()
.map_err(|e| MicError(format!("query configs: {e}")))?
.find(|c| {
c.channels() >= config.channels
&& c.min_sample_rate().0 <= config.sample_rate
&& c.max_sample_rate().0 >= config.sample_rate
})
.ok_or_else(|| MicError("no compatible audio config found".into()))?;
let stream_config: cpal::StreamConfig = supported
.with_sample_rate(cpal::SampleRate(config.sample_rate))
.into();
let (pipeline_tx, pipeline_rx) = mpsc::channel::<AudioChunk>(32);
let (sync_tx, sync_rx) = std::sync::mpsc::channel::<Vec<f32>>();
let chunk_size = config.chunk_size;
let sample_rate = config.sample_rate;
let channels = config.channels;
let input_channels = stream_config.channels as usize;
let stream = device
.build_input_stream(
&stream_config,
move |data: &[f32], _info: &cpal::InputCallbackInfo| {
let mono: Vec<f32> = if input_channels > 1 {
data.chunks(input_channels)
.map(|frame| frame.iter().sum::<f32>() / frame.len() as f32)
.collect()
} else {
data.to_vec()
};
let _ = sync_tx.send(mono);
},
|err| {
tracing::error!(error = %err, "audio input error");
},
None,
)
.map_err(|e| MicError(format!("build stream: {e}")))?;
stream
.play()
.map_err(|e| MicError(format!("start stream: {e}")))?;
let _bridge = std::thread::spawn(move || {
let mut buffer: Vec<f32> = Vec::with_capacity(chunk_size * 2);
while let Ok(samples) = sync_rx.recv() {
buffer.extend(samples);
while buffer.len() >= chunk_size {
let chunk_data: Vec<f32> = buffer.drain(..chunk_size).collect();
let chunk = AudioChunk {
samples: chunk_data,
sample_rate,
channels,
};
if pipeline_tx.blocking_send(chunk).is_err() {
return;
}
}
}
if !buffer.is_empty() {
let chunk = AudioChunk {
samples: buffer,
sample_rate,
channels,
};
let _ = pipeline_tx.blocking_send(chunk);
}
});
let guard = MicStopGuard {
_stream: stream,
_bridge: Some(_bridge),
};
Ok((pipeline_rx, guard))
}
pub struct MicStopGuard {
_stream: cpal::Stream,
_bridge: Option<std::thread::JoinHandle<()>>,
}
#[derive(Debug, Clone)]
pub struct MicError(pub String);
impl std::fmt::Display for MicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "mic error: {}", self.0)
}
}
impl std::error::Error for MicError {}