use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, SizedSample, StreamConfig};
use ringbuf::traits::{Observer, Producer, Split};
use ringbuf::{HeapCons, HeapProd, HeapRb};
use crate::error::{AudioError, Result};
use super::resample::LinearResampler;
use super::{config_ranges, device_name, negotiate_config, pick_device};
pub const CAPTURE_RATE: u32 = 16_000;
const MIC_RING_CAPACITY: usize = 480_000;
const CALLBACK_CHUNK_FRAMES: usize = 1024;
fn out_scratch_capacity(device_rate: u32) -> usize {
CALLBACK_CHUNK_FRAMES * CAPTURE_RATE as usize / device_rate.max(1) as usize + 8
}
#[derive(Debug, Clone, Default)]
pub struct AudioInputConfig {
pub device_name: Option<String>,
}
pub struct MicCapture {
stream: cpal::Stream,
dropped_samples: Arc<AtomicU64>,
}
impl MicCapture {
pub fn start(cfg: &AudioInputConfig) -> Result<(MicCapture, HeapCons<f32>)> {
let host = cpal::default_host();
let device = pick_device(
cfg.device_name.as_deref(),
host.input_devices(),
host.default_input_device(),
)?;
tracing::info!(
device = device_name(&device).as_deref().unwrap_or("<unknown>"),
"capturing from input device"
);
let ranges = config_ranges("input", device.supported_input_configs())?;
let negotiated = negotiate_config(&ranges, CAPTURE_RATE).ok_or_else(|| {
AudioError::StreamConfig("device offers no f32/i16/u16 input configuration".to_string())
})?;
let sample_format = negotiated.sample_format();
let config = negotiated.config();
let channels = config.channels as usize;
let device_rate = config.sample_rate;
tracing::debug!(
?sample_format,
channels,
device_rate,
"negotiated input stream config"
);
let (prod, cons) = HeapRb::new(MIC_RING_CAPACITY).split();
let dropped = Arc::new(AtomicU64::new(0));
let state = CallbackState {
prod,
resampler: LinearResampler::new(device_rate, CAPTURE_RATE),
mono: Vec::with_capacity(CALLBACK_CHUNK_FRAMES),
out: Vec::with_capacity(out_scratch_capacity(device_rate)),
channels,
dropped: Arc::clone(&dropped),
};
let stream = match sample_format {
SampleFormat::F32 => build_input::<f32>(&device, config, state, |s| s),
SampleFormat::I16 => {
build_input::<i16>(&device, config, state, |s| f32::from(s) / 32768.0)
}
SampleFormat::U16 => build_input::<u16>(&device, config, state, |s| {
(f32::from(s) - 32768.0) / 32768.0
}),
other => {
return Err(AudioError::StreamConfig(format!(
"unsupported input sample format {other:?}"
))
.into());
}
}?;
stream
.play()
.map_err(|err| AudioError::StreamBuild(err.to_string()))?;
Ok((
MicCapture {
stream,
dropped_samples: dropped,
},
cons,
))
}
pub fn dropped_samples(&self) -> u64 {
self.dropped_samples.load(Ordering::Relaxed)
}
pub fn stop(self) {
drop(self.stream);
}
}
pub fn push_block_drop_count(prod: &mut HeapProd<f32>, block: &[f32], dropped: &AtomicU64) {
if block.is_empty() {
return;
}
if prod.vacant_len() < block.len() {
dropped.fetch_add(block.len() as u64, Ordering::Relaxed);
return;
}
let pushed = prod.push_slice(block);
debug_assert_eq!(pushed, block.len());
}
pub fn list_devices() -> Result<Vec<String>> {
let host = cpal::default_host();
let mut names = Vec::new();
if let Ok(devices) = host.input_devices() {
for device in devices {
names.push(format!(
"input: {}",
device_name(&device).unwrap_or_else(|| "<unknown>".to_string())
));
}
}
if let Ok(devices) = host.output_devices() {
for device in devices {
names.push(format!(
"output: {}",
device_name(&device).unwrap_or_else(|| "<unknown>".to_string())
));
}
}
Ok(names)
}
struct CallbackState {
prod: HeapProd<f32>,
resampler: LinearResampler,
mono: Vec<f32>,
out: Vec<f32>,
channels: usize,
dropped: Arc<AtomicU64>,
}
fn build_input<T>(
device: &cpal::Device,
config: StreamConfig,
mut state: CallbackState,
convert: fn(T) -> f32,
) -> Result<cpal::Stream>
where
T: SizedSample + 'static,
{
device
.build_input_stream(
config,
move |data: &[T], _: &cpal::InputCallbackInfo| {
process_input_block(&mut state, data, convert);
},
move |err| {
tracing::warn!(%err, "mic input stream error");
},
None,
)
.map_err(|err| AudioError::StreamBuild(err.to_string()).into())
}
fn process_input_block<T: Copy>(state: &mut CallbackState, data: &[T], convert: fn(T) -> f32) {
let channels = state.channels.max(1);
for chunk in data.chunks(channels * CALLBACK_CHUNK_FRAMES) {
state.mono.clear();
let frames = chunk.len() / channels;
debug_assert!(frames <= CALLBACK_CHUNK_FRAMES);
if channels == 1 {
state.mono.extend(chunk.iter().map(|&s| convert(s)));
} else {
state.mono.extend((0..frames).map(|frame| {
let mut acc = 0.0f32;
for &sample in &chunk[frame * channels..(frame + 1) * channels] {
acc += convert(sample);
}
acc / channels as f32
}));
}
state.resampler.process(&state.mono, &mut state.out);
push_block_drop_count(&mut state.prod, &state.out, &state.dropped);
}
}
#[cfg(test)]
mod tests {
use super::*;
use ringbuf::traits::Observer;
fn test_state(device_rate: u32, channels: usize) -> CallbackState {
let (prod, _cons) = HeapRb::<f32>::new(MIC_RING_CAPACITY).split();
CallbackState {
prod,
resampler: LinearResampler::new(device_rate, CAPTURE_RATE),
mono: Vec::with_capacity(CALLBACK_CHUNK_FRAMES),
out: Vec::with_capacity(out_scratch_capacity(device_rate)),
channels,
dropped: Arc::new(AtomicU64::new(0)),
}
}
#[test]
fn huge_callback_block_does_not_grow_scratch() {
let mut state = test_state(48_000, 2);
let mono_cap = state.mono.capacity();
let out_cap = state.out.capacity();
let block = vec![0.1f32; 100_000]; process_input_block(&mut state, &block, |s| s * 2.0);
process_input_block(&mut state, &block, |s| s * 2.0);
assert_eq!(state.mono.capacity(), mono_cap, "mono scratch grew");
assert_eq!(state.out.capacity(), out_cap, "resampler scratch grew");
let pushed = state.prod.occupied_len();
assert!(
(pushed as isize - 33_333).abs() <= 8,
"expected ≈33 333 ringed samples, got {pushed}"
);
assert_eq!(state.dropped.load(Ordering::Relaxed), 0);
}
#[test]
fn huge_mono_block_does_not_grow_scratch() {
let mut state = test_state(16_000, 1);
let mono_cap = state.mono.capacity();
let out_cap = state.out.capacity();
let block = vec![0.25f32; 100_001]; process_input_block(&mut state, &block, |s| s);
assert_eq!(state.mono.capacity(), mono_cap);
assert_eq!(state.out.capacity(), out_cap);
assert_eq!(state.prod.occupied_len(), 100_001);
}
}