use std::task::Poll;
use std::time::Duration;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use ringbuf::traits::Producer;
use crate::Error;
mod buffer;
#[cfg(target_os = "macos")]
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,
recycle: Option<ringbuf::HeapProd<Vec<f32>>>,
}
impl Samples {
#[cfg(any(test, target_os = "macos"))]
pub(crate) fn plain(data: Vec<f32>, gap: bool) -> Self {
Self {
data,
gap,
recycle: None,
}
}
fn pooled(data: Vec<f32>, gap: bool, recycle: ringbuf::HeapProd<Vec<f32>>) -> Self {
Self {
data,
gap,
recycle: Some(recycle),
}
}
pub(crate) fn replace(&mut self, data: Vec<f32>) {
self.recycle();
self.data = data;
}
fn recycle(&mut self) {
let Some(mut recycle) = self.recycle.take() else {
return;
};
let mut data = std::mem::take(&mut self.data);
data.clear();
if recycle.try_push(data).is_err() {
}
}
}
impl Drop for Samples {
fn drop(&mut self) {
self.recycle();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Layout {
pub sample_rate: u32,
pub channels: u32,
}
#[derive(Debug)]
pub(crate) enum Failure {
Retry(Error),
Fatal(Error),
}
impl Failure {
pub(crate) fn retry(error: Error) -> Self {
Self::Retry(error)
}
pub(crate) fn fatal(error: Error) -> Self {
Self::Fatal(error)
}
fn cpal(error: cpal::Error) -> Self {
let retryable = retryable(error.kind());
let error = capture_err(error);
if retryable {
Self::Retry(error)
} else {
Self::Fatal(error)
}
}
pub(crate) fn is_retryable(&self) -> bool {
matches!(self, Self::Retry(_))
}
pub(crate) fn into_error(self) -> Error {
match self {
Self::Retry(error) | Self::Fatal(error) => error,
}
}
}
pub(crate) enum Stream {
Microphone(Microphone),
#[cfg(target_os = "macos")]
System(screencapture::SystemAudio),
}
impl Stream {
pub(crate) fn layout(&self) -> Layout {
match self {
Self::Microphone(mic) => mic.layout,
#[cfg(target_os = "macos")]
Self::System(system) => system.layout(),
}
}
pub(crate) async fn read(&mut self) -> Result<Option<Samples>, Failure> {
match self {
Self::Microphone(mic) => mic.read().await,
#[cfg(target_os = "macos")]
Self::System(system) => Ok(system.read().await),
}
}
}
pub(crate) async fn format(config: &Config) -> Result<Layout, Failure> {
match &config.source {
Source::Microphone(device) => {
let (device, config) = (device.clone(), config.clone());
tokio::task::spawn_blocking(move || {
let (_, _, stream_config) = resolve(device.as_deref(), &config)?;
Ok(Layout {
sample_rate: stream_config.sample_rate,
channels: stream_config.channels as u32,
})
})
.await
.map_err(|err| Failure::fatal(Error::Capture(format!("audio host thread failed: {err}"))))?
}
#[cfg(target_os = "macos")]
Source::System => Ok(screencapture::SystemAudio::format(config.sample_rate, config.channels)),
#[cfg(not(target_os = "macos"))]
Source::System => Err(Failure::fatal(Error::Unsupported(
"system audio capture is only supported on macOS".into(),
))),
}
}
pub(crate) async fn open(config: &Config) -> Result<Stream, Failure> {
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
.map_err(Failure::fatal)?,
)),
#[cfg(not(target_os = "macos"))]
Source::System => Err(Failure::fatal(Error::Unsupported(
"system audio capture is only supported on macOS".into(),
))),
}
}
pub(crate) struct Microphone {
_stream: cpal::Stream,
reader: MicrophoneReader,
pending: Option<Samples>,
layout: Layout,
}
struct MicrophoneReader {
rx: buffer::Reader,
errors: kio::Consumer<Option<cpal::Error>>,
}
impl MicrophoneReader {
async fn pending(&mut self, samples: Samples) -> Result<Option<Samples>, Failure> {
tokio::select! {
biased;
Some(err) = failure(&self.errors) => Err(err),
_ = std::future::ready(()) => Ok(Some(samples)),
}
}
async fn read(&mut self) -> Result<Option<Samples>, Failure> {
let data = tokio::select! {
biased;
Some(err) = failure(&self.errors) => return Err(err),
data = self.rx.recv() => data,
};
Ok(data)
}
}
async fn failure(errors: &kio::Consumer<Option<cpal::Error>>) -> Option<Failure> {
errors
.wait(|error| match error.as_ref() {
Some(error) => Poll::Ready(error.clone()),
None => Poll::Pending,
})
.await
.ok()
.map(Failure::cpal)
}
impl Microphone {
async fn open(selector: Option<&str>, config: &Config) -> Result<Self, Failure> {
permission::ensure_microphone_access().await.map_err(Failure::fatal)?;
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).map_err(Failure::fatal)?;
}
let (mut writer, rx) = buffer::channel(
channels as usize,
#[cfg(feature = "aec")]
config.aec.clone(),
);
let error_tx = kio::Producer::new(None);
let errors = error_tx.consume();
let mut reader = MicrophoneReader { rx, errors };
let stream = match sample_format {
cpal::SampleFormat::F32 => {
let errors = error_tx.clone();
device.build_input_stream(
stream_config,
move |data: &[f32], _: &_| writer.write_f32(data),
move |err| stream_err(&errors, err),
None,
)
}
cpal::SampleFormat::I16 => {
let errors = error_tx.clone();
device.build_input_stream(
stream_config,
move |data: &[i16], _: &_| writer.write_i16(data),
move |err| stream_err(&errors, err),
None,
)
}
cpal::SampleFormat::U16 => {
let errors = error_tx.clone();
device.build_input_stream(
stream_config,
move |data: &[u16], _: &_| writer.write_u16(data),
move |err| stream_err(&errors, err),
None,
)
}
other => {
return Err(Failure::fatal(Error::Unsupported(format!(
"unsupported input sample format {other:?}"
))));
}
}
.map_err(Failure::cpal)?;
stream.play().map_err(Failure::cpal)?;
let pending = match tokio::time::timeout(FIRST_BUFFER_TIMEOUT, reader.read()).await {
Ok(Ok(Some(samples))) => samples,
Ok(Ok(None)) => {
return Err(Failure::retry(Error::Capture(format!(
"microphone {device} stopped before any samples"
))));
}
Ok(Err(err)) => return Err(err),
Err(_) => {
return Err(Failure::fatal(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,
reader,
pending: Some(pending),
layout: Layout { sample_rate, channels },
})
}
async fn read(&mut self) -> Result<Option<Samples>, Failure> {
if let Some(samples) = self.pending.take() {
return self.reader.pending(samples).await;
}
self.reader.read().await
}
}
#[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), Failure> {
let host = cpal::default_host();
let device = match selector {
Some(name) => host
.input_devices()
.map_err(Failure::cpal)?
.find(|d| d.to_string() == name)
.ok_or_else(|| Failure::retry(Error::Device(format!("input device {name:?} not found"))))?,
None => host
.default_input_device()
.ok_or_else(|| Failure::retry(Error::Device("no default input device".into())))?,
};
let supported = device.default_input_config().map_err(Failure::cpal)?;
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(errors: &kio::Producer<Option<cpal::Error>>, err: cpal::Error) {
if survivable(err.kind()) {
tracing::warn!(error = %err, "microphone stream error does not require a restart");
return;
}
tracing::error!(error = %err, "microphone stream error");
let Ok(mut failure) = errors.write() else { return };
if failure.is_some() {
return;
}
*failure = Some(err);
failure.close();
}
fn survivable(kind: cpal::ErrorKind) -> bool {
matches!(
kind,
cpal::ErrorKind::DeviceChanged | cpal::ErrorKind::RealtimeDenied | cpal::ErrorKind::Xrun
)
}
fn retryable(kind: cpal::ErrorKind) -> bool {
matches!(
kind,
cpal::ErrorKind::DeviceBusy
| cpal::ErrorKind::DeviceNotAvailable
| cpal::ErrorKind::HostUnavailable
| cpal::ErrorKind::ResourceExhausted
| cpal::ErrorKind::StreamInvalidated
)
}
fn capture_err(err: impl std::fmt::Display) -> Error {
Error::Capture(err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn reader() -> (buffer::Writer, kio::Producer<Option<cpal::Error>>, MicrophoneReader) {
let (tx, rx) = buffer::channel(
1,
#[cfg(feature = "aec")]
None,
);
let failures = kio::Producer::new(None);
let errors = failures.consume();
(tx, failures, MicrophoneReader { rx, errors })
}
fn fail(errors: &kio::Producer<Option<cpal::Error>>, message: &'static str) {
stream_err(
errors,
cpal::Error::with_message(cpal::ErrorKind::DeviceNotAvailable, message),
);
}
#[tokio::test]
async fn stream_error_wakes_a_reader_without_samples() {
let (_samples, errors, mut reader) = reader();
fail(&errors, "device lost");
let err = match reader.read().await {
Err(err) => err.into_error(),
Ok(_) => panic!("the reader ignored its stream error"),
};
assert!(matches!(err, Error::Capture(message) if message == "device lost"));
}
#[tokio::test]
async fn replaced_stream_cannot_fail_its_replacement() {
let (_old_samples, old_errors, old_reader) = reader();
let (mut new_samples, _new_errors, mut new_reader) = reader();
drop(old_reader);
fail(&old_errors, "stale");
new_samples.write_f32(&[1.0]);
let samples = new_reader.read().await.unwrap().unwrap();
assert_eq!(samples.data, vec![1.0]);
}
#[tokio::test]
async fn stream_error_wins_over_the_buffer_saved_during_open() {
let (_samples, errors, mut reader) = reader();
fail(&errors, "device lost");
let result = reader.pending(Samples::plain(vec![1.0], false)).await;
let err = match result {
Err(err) => err.into_error(),
Ok(_) => panic!("the pending sample hid a stream error"),
};
assert!(matches!(err, Error::Capture(message) if message == "device lost"));
}
#[test]
fn survivable_errors_do_not_end_the_stream() {
let (_samples, errors, _reader) = reader();
stream_err(&errors, cpal::Error::new(cpal::ErrorKind::DeviceChanged));
assert!(errors.read().is_none());
}
#[test]
fn permission_errors_are_not_retryable() {
let failure = Failure::cpal(cpal::Error::new(cpal::ErrorKind::PermissionDenied));
assert!(!failure.is_retryable());
}
#[test]
fn opaque_backend_errors_are_not_retryable() {
let failure = Failure::cpal(cpal::Error::new(cpal::ErrorKind::BackendError));
assert!(!failure.is_retryable());
}
}