use std::time::Duration;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::Error;
mod channel;
mod permission;
#[cfg(target_os = "macos")]
mod screencapture;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Source {
Microphone(Option<String>),
System,
}
impl Default for Source {
fn default() -> Self {
Self::Microphone(None)
}
}
const FIRST_BUFFER_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct Config {
pub source: Source,
pub sample_rate: Option<u32>,
pub channels: Option<u32>,
#[cfg(feature = "aec")]
pub aec: Option<crate::aec::Canceller>,
}
pub(crate) struct Samples {
pub data: Vec<f32>,
pub gap: bool,
}
pub(crate) enum Stream {
Microphone(Microphone),
#[cfg(target_os = "macos")]
System(screencapture::SystemAudio),
}
impl Stream {
pub(crate) async fn read(&mut self) -> Option<Samples> {
match self {
Self::Microphone(mic) => mic.read().await,
#[cfg(target_os = "macos")]
Self::System(system) => system.read().await,
}
}
}
pub(crate) async fn format(config: &Config) -> Result<(u32, u32), Error> {
match &config.source {
Source::Microphone(device) => {
let (device, config) = (device.clone(), config.clone());
blocking(move || {
let (_, _, stream_config) = resolve(device.as_deref(), &config)?;
Ok((stream_config.sample_rate, stream_config.channels as u32))
})
.await
}
#[cfg(target_os = "macos")]
Source::System => Ok(screencapture::SystemAudio::format(config.sample_rate, config.channels)),
#[cfg(not(target_os = "macos"))]
Source::System => Err(Error::Unsupported(
"system audio capture is only supported on macOS".into(),
)),
}
}
pub(crate) async fn open(config: &Config) -> Result<Stream, Error> {
match &config.source {
Source::Microphone(device) => Ok(Stream::Microphone(Microphone::open(device.as_deref(), config).await?)),
#[cfg(target_os = "macos")]
Source::System => Ok(Stream::System(
screencapture::SystemAudio::open(config.sample_rate, config.channels).await?,
)),
#[cfg(not(target_os = "macos"))]
Source::System => Err(Error::Unsupported(
"system audio capture is only supported on macOS".into(),
)),
}
}
pub(crate) struct Microphone {
_stream: cpal::Stream,
rx: channel::Receiver<Vec<f32>>,
pending: Option<Vec<f32>>,
}
impl Microphone {
async fn open(selector: Option<&str>, config: &Config) -> Result<Self, Error> {
permission::ensure_microphone_access().await?;
let (device, sample_format, stream_config) = resolve(selector, config)?;
let sample_rate = stream_config.sample_rate;
let channels = stream_config.channels as u32;
#[cfg(feature = "aec")]
if let Some(aec) = &config.aec {
aec.open(sample_rate, channels)?;
}
let (tx, mut rx) = channel::bounded::<Vec<f32>>();
let deliver = {
#[cfg(feature = "aec")]
let aec = config.aec.clone();
move |#[allow(unused_mut)] mut pcm: Vec<f32>| {
#[cfg(feature = "aec")]
if let Some(aec) = &aec {
aec.process(&mut pcm);
}
tx.push(pcm);
}
};
let stream = match sample_format {
cpal::SampleFormat::F32 => device.build_input_stream(
stream_config,
move |data: &[f32], _: &_| deliver(data.to_vec()),
stream_err,
None,
),
cpal::SampleFormat::I16 => device.build_input_stream(
stream_config,
move |data: &[i16], _: &_| deliver(data.iter().map(|&s| s as f32 / 32768.0).collect()),
stream_err,
None,
),
cpal::SampleFormat::U16 => device.build_input_stream(
stream_config,
move |data: &[u16], _: &_| deliver(data.iter().map(|&s| (s as f32 - 32768.0) / 32768.0).collect()),
stream_err,
None,
),
other => {
return Err(Error::Unsupported(format!("unsupported input sample format {other:?}")));
}
}
.map_err(capture_err)?;
stream.play().map_err(capture_err)?;
let pending = match tokio::time::timeout(FIRST_BUFFER_TIMEOUT, rx.recv()).await {
Ok(Some(samples)) => samples,
Ok(None) => {
return Err(Error::Capture(format!(
"microphone {device} stopped before any samples"
)));
}
Err(_) => {
return Err(Error::Capture(format!(
"no samples from microphone {device} within {FIRST_BUFFER_TIMEOUT:?} (permission denied?)"
)));
}
};
tracing::info!(device = %device, sample_rate, channels, "opened microphone");
Ok(Self {
_stream: stream,
rx,
pending: Some(pending),
})
}
async fn read(&mut self) -> Option<Samples> {
if let Some(data) = self.pending.take() {
return Some(Samples { data, gap: false });
}
let data = self.rx.recv().await?; Some(Samples {
data,
gap: self.rx.gap(),
})
}
}
#[derive(Clone, Debug)]
pub struct Device {
pub id: String,
pub name: String,
pub default: bool,
}
impl Device {
pub fn source(&self) -> Source {
Source::Microphone(Some(self.id.clone()))
}
}
pub async fn devices() -> Result<Vec<Device>, Error> {
blocking(list).await
}
fn list() -> Result<Vec<Device>, Error> {
let host = cpal::default_host();
let default = host.default_input_device().map(|d| d.to_string());
Ok(host
.input_devices()
.map_err(capture_err)?
.map(|device| {
let name = device.to_string();
Device {
default: Some(&name) == default.as_ref(),
id: name.clone(),
name,
}
})
.collect())
}
async fn blocking<T, F>(f: F) -> Result<T, Error>
where
F: FnOnce() -> Result<T, Error> + Send + 'static,
T: Send + 'static,
{
tokio::task::spawn_blocking(f)
.await
.map_err(|err| Error::Capture(format!("audio host thread failed: {err}")))?
}
fn resolve(
selector: Option<&str>,
config: &Config,
) -> Result<(cpal::Device, cpal::SampleFormat, cpal::StreamConfig), Error> {
let host = cpal::default_host();
let device = match selector {
Some(name) => host
.input_devices()
.map_err(capture_err)?
.find(|d| d.to_string() == name)
.ok_or_else(|| Error::Device(format!("input device {name:?} not found")))?,
None => host
.default_input_device()
.ok_or_else(|| Error::Device("no default input device".into()))?,
};
let supported = device.default_input_config().map_err(capture_err)?;
let sample_format = supported.sample_format();
let mut stream_config = supported.config();
if let Some(rate) = config.sample_rate {
stream_config.sample_rate = rate;
}
if let Some(channels) = config.channels {
stream_config.channels = channels as u16;
}
Ok((device, sample_format, stream_config))
}
fn stream_err(err: cpal::Error) {
tracing::error!(error = %err, "microphone stream error");
}
fn capture_err(err: impl std::fmt::Display) -> Error {
Error::Capture(err.to_string())
}