use crate::Error;
use cpal::traits::{DeviceTrait, HostTrait};
use ringbuffer::{AllocRingBuffer, RingBuffer};
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Mutex};
const PREFERRED_BUFFER_FRAMES: u32 = 256;
pub(crate) struct AudioBuffer {
samples: AllocRingBuffer<f32>,
total: u64,
}
impl AudioBuffer {
pub(crate) fn new(capacity: usize) -> Self {
let mut samples = AllocRingBuffer::new(capacity);
samples.fill(0.0);
Self {
samples,
total: capacity as u64,
}
}
pub(crate) fn snapshot(&self) -> (Vec<f32>, u64) {
(self.samples.to_vec(), self.total)
}
fn extend(&mut self, samples: impl ExactSizeIterator<Item = f32>) {
self.total += samples.len() as u64;
self.samples.extend(samples);
}
}
pub struct AudioInput {
dev: cpal::Device,
cfg: cpal::StreamConfig,
}
impl AudioInput {
pub fn default_device() -> Result<Self, Error> {
let host = cpal::default_host();
let dev = host.default_input_device().ok_or_else(|| {
Error::Audio(format!(
"no default audio input device found for host {}",
host.id().name()
))
})?;
Self::from_device(dev)
}
pub fn from_device(dev: cpal::Device) -> Result<Self, Error> {
let default = dev
.default_input_config()
.map_err(|e| Error::Audio(format!("no default input config: {e}")))?;
let cfg = mono_config(&dev, &default).unwrap_or_else(|| default.config());
Ok(Self { dev, cfg })
}
#[must_use]
pub const fn new(dev: cpal::Device, cfg: cpal::StreamConfig) -> Self {
Self { dev, cfg }
}
pub fn devices() -> Result<Vec<(String, cpal::Device)>, Error> {
let host = cpal::default_host();
let mut devs: Vec<(String, cpal::Device)> = host
.input_devices()
.map_err(|e| Error::Audio(format!("can't enumerate input devices: {e}")))?
.map(|dev| (dev.to_string(), dev))
.collect();
devs.sort_by(|(n1, _), (n2, _)| n1.cmp(n2));
Ok(devs)
}
#[must_use]
pub const fn device(&self) -> &cpal::Device {
&self.dev
}
#[must_use]
pub const fn config(&self) -> &cpal::StreamConfig {
&self.cfg
}
pub(crate) fn build_stream(
&self,
latest_audio_data: Arc<Mutex<AudioBuffer>>,
) -> Result<cpal::Stream, Error> {
let channels = self.cfg.channels;
if channels != 1 && channels != 2 {
return Err(Error::Audio(format!(
"only mono or stereo input is supported, device has {channels} channels"
)));
}
let is_mono = channels == 1;
let build = |cfg: cpal::StreamConfig| {
let latest_audio_data = latest_audio_data.clone();
self.dev.build_input_stream(
cfg,
move |data: &[f32], _info| {
let mut audio_buf = latest_audio_data.lock().unwrap();
if is_mono {
audio_buf.extend(data.iter().copied());
} else {
let (pairs, _) = data.as_chunks::<2>();
audio_buf.extend(pairs.iter().map(|[l, r]| (l + r) / 2.0));
}
},
|err| eprintln!("audio stream error: {err:#?}"),
None,
)
};
if matches!(self.cfg.buffer_size, cpal::BufferSize::Default) {
let preferred = cpal::StreamConfig {
buffer_size: cpal::BufferSize::Fixed(PREFERRED_BUFFER_FRAMES),
..self.cfg
};
if let Ok(stream) = build(preferred) {
return Ok(stream);
}
}
build(self.cfg).map_err(|e| Error::Audio(format!("can't build input stream: {e}")))
}
}
fn mono_config(
dev: &cpal::Device,
default: &cpal::SupportedStreamConfig,
) -> Option<cpal::StreamConfig> {
let sample_rate = default.sample_rate();
dev.supported_input_configs()
.ok()?
.find(|cfg| {
cfg.channels() == 1
&& cfg.sample_format() == cpal::SampleFormat::F32
&& (cfg.min_sample_rate()..=cfg.max_sample_rate()).contains(&sample_rate)
})
.map(|cfg| cfg.with_sample_rate(sample_rate).config())
}
impl Debug for AudioInput {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AudioInput")
.field("dev", &self.dev.to_string())
.field("cfg", &self.cfg)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_input_devs() {
dbg!(
AudioInput::devices()
.unwrap()
.iter()
.map(|(n, d)| (n, d.default_input_config()))
.collect::<Vec<_>>()
);
}
}