use std::sync::mpsc;
use audio_core_bsd::AudioFrame;
use crate::backend::{AudioBackend, InputSource, OutputSink};
use crate::device::{BufferSize, DeviceDirection, DeviceInfo, StreamParams};
use crate::error::{IoError, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
const RING_CAPACITY: usize = 1 << 16;
pub struct CpalBackend {
host: cpal::Host,
}
impl CpalBackend {
pub fn new() -> Result<Self> {
Ok(Self {
host: cpal::default_host(),
})
}
#[must_use]
pub fn with_host(host: cpal::Host) -> Self {
Self { host }
}
#[must_use]
pub fn host(&self) -> &cpal::Host {
&self.host
}
}
fn stream_config(params: StreamParams) -> cpal::StreamConfig {
cpal::StreamConfig {
channels: params.channels,
sample_rate: cpal::SampleRate(params.sample_rate),
buffer_size: match params.buffer_size {
BufferSize::Fixed(n) => cpal::BufferSize::Fixed(u32::try_from(n).unwrap_or(u32::MAX)),
BufferSize::Default => cpal::BufferSize::Default,
},
}
}
fn find_device(host: &cpal::Host, dev: &str, output: bool) -> Result<cpal::Device> {
let mut devices = if output {
host.output_devices()
.map_err(|e| IoError::Backend(e.to_string()))?
} else {
host.input_devices()
.map_err(|e| IoError::Backend(e.to_string()))?
};
devices
.find(|d| d.name().is_ok_and(|n| n == dev))
.ok_or_else(|| IoError::DeviceNotFound(dev.into()))
}
fn check_supported(device: &cpal::Device, params: StreamParams, output: bool) -> Result<()> {
let target = cpal::SampleRate(params.sample_rate);
let matches = |c: cpal::SupportedStreamConfigRange| {
c.channels() == params.channels
&& c.min_sample_rate() <= target
&& target <= c.max_sample_rate()
};
let ok = if output {
device
.supported_output_configs()
.map_err(|e| IoError::UnsupportedConfig(e.to_string()))?
.any(matches)
} else {
device
.supported_input_configs()
.map_err(|e| IoError::UnsupportedConfig(e.to_string()))?
.any(matches)
};
if ok {
Ok(())
} else {
Err(IoError::UnsupportedConfig(format!(
"no {} config for {}ch @ {} Hz",
if output { "output" } else { "input" },
params.channels,
params.sample_rate
)))
}
}
fn device_info(device: &cpal::Device, direction: DeviceDirection, is_default: bool) -> DeviceInfo {
let name = device.name().unwrap_or_else(|_| "<unknown>".into());
let (channels, rates) = device
.supported_output_configs()
.ok()
.and_then(|mut c| c.next())
.map_or((2, vec![48_000]), |first| {
(
first.channels(),
vec![first.min_sample_rate().0, first.max_sample_rate().0],
)
});
DeviceInfo::new(name, direction, channels, rates, is_default)
}
impl AudioBackend for CpalBackend {
fn enumerate_devices(&self) -> Vec<DeviceInfo> {
let default_out = self.host.default_output_device();
let default_in = self.host.default_input_device();
let mut out = Vec::new();
if let Ok(devs) = self.host.output_devices() {
for d in devs {
let is_default = default_out
.as_ref()
.is_some_and(|def| d.name().is_ok_and(|n| def.name().is_ok_and(|m| n == m)));
out.push(device_info(&d, DeviceDirection::Output, is_default));
}
}
if let Ok(devs) = self.host.input_devices() {
for d in devs {
let is_default = default_in
.as_ref()
.is_some_and(|def| d.name().is_ok_and(|n| def.name().is_ok_and(|m| n == m)));
out.push(device_info(&d, DeviceDirection::Input, is_default));
}
}
out
}
fn default_output(&self) -> Option<DeviceInfo> {
self.host
.default_output_device()
.as_ref()
.map(|d| device_info(d, DeviceDirection::Output, true))
}
fn default_input(&self) -> Option<DeviceInfo> {
self.host
.default_input_device()
.as_ref()
.map(|d| device_info(d, DeviceDirection::Input, true))
}
fn open_output(&self, dev: &str, params: StreamParams) -> Result<Box<dyn OutputSink>> {
params.validate()?;
let device = find_device(&self.host, dev, true)?;
check_supported(&device, params, true)?;
let cfg = stream_config(params);
let channels = cfg.channels;
let (producer, consumer) = rtrb::RingBuffer::<f32>::new(RING_CAPACITY);
let (drop_tx, drop_rx) = mpsc::channel::<()>();
let (ready_tx, ready_rx) = mpsc::channel::<Result<()>>();
std::thread::Builder::new()
.name("audio-io-output".into())
.spawn(move || {
let mut consumer = consumer;
let stream = device.build_output_stream::<f32, _, _>(
&cfg,
move |data: &mut [f32], _info| {
for slot in data.iter_mut() {
*slot = consumer.pop().unwrap_or(0.0);
}
},
|_err| {
},
None,
);
let stream = match stream {
Ok(s) => s,
Err(e) => {
let _ = ready_tx.send(Err(IoError::StreamSetup(e.to_string())));
return;
}
};
if let Err(e) = stream.play() {
let _ = ready_tx.send(Err(IoError::StreamSetup(e.to_string())));
return;
}
let _ = ready_tx.send(Ok(()));
let _ = drop_rx.recv();
})
.map_err(|e| IoError::Backend(format!("spawn output thread: {e}")))?;
ready_rx
.recv()
.map_err(|e| IoError::StreamSetup(format!("output thread panicked: {e}")))??;
Ok(Box::new(CpalSink {
producer,
scratch: Vec::with_capacity(4 * channels as usize),
channels,
drop_tx: Some(drop_tx),
}))
}
fn open_input(&self, dev: &str, params: StreamParams) -> Result<Box<dyn InputSource>> {
params.validate()?;
let device = find_device(&self.host, dev, false)?;
check_supported(&device, params, false)?;
let cfg = stream_config(params);
let channels = cfg.channels;
let sample_rate = cfg.sample_rate.0;
let (producer, consumer) = rtrb::RingBuffer::<f32>::new(RING_CAPACITY);
let (drop_tx, drop_rx) = mpsc::channel::<()>();
let (ready_tx, ready_rx) = mpsc::channel::<Result<()>>();
std::thread::Builder::new()
.name("audio-io-input".into())
.spawn(move || {
let mut producer = producer;
let stream = device.build_input_stream::<f32, _, _>(
&cfg,
move |data: &[f32], _info| {
for &s in data {
let _ = producer.push(s);
}
},
|_err| {},
None,
);
let stream = match stream {
Ok(s) => s,
Err(e) => {
let _ = ready_tx.send(Err(IoError::StreamSetup(e.to_string())));
return;
}
};
if let Err(e) = stream.play() {
let _ = ready_tx.send(Err(IoError::StreamSetup(e.to_string())));
return;
}
let _ = ready_tx.send(Ok(()));
let _ = drop_rx.recv();
})
.map_err(|e| IoError::Backend(format!("spawn input thread: {e}")))?;
ready_rx
.recv()
.map_err(|e| IoError::StreamSetup(format!("input thread panicked: {e}")))??;
Ok(Box::new(CpalSource {
consumer,
scratch: Vec::with_capacity(4 * channels as usize),
channels,
sample_rate,
drop_tx: Some(drop_tx),
}))
}
}
pub struct CpalSink {
producer: rtrb::Producer<f32>,
scratch: Vec<f32>,
channels: cpal::ChannelCount,
drop_tx: Option<mpsc::Sender<()>>,
}
impl OutputSink for CpalSink {
fn write(&mut self, frame: &AudioFrame) -> Result<()> {
let n = frame.num_frames();
let ch = (self.channels.min(frame.channels)) as usize;
if ch == 0 {
return Ok(());
}
self.scratch.clear();
self.scratch.reserve(n * ch);
for i in 0..n {
for c in 0..ch {
let v = frame.channel_slice(c).get(i).copied().unwrap_or(0.0);
self.scratch.push(v);
}
}
for &s in &self.scratch {
if self.producer.push(s).is_err() {
return Err(IoError::StreamSetup(
"output ring full (back-pressure)".into(),
));
}
}
Ok(())
}
}
impl Drop for CpalSink {
fn drop(&mut self) {
self.drop_tx.take();
}
}
pub struct CpalSource {
consumer: rtrb::Consumer<f32>,
scratch: Vec<f32>,
channels: cpal::ChannelCount,
sample_rate: u32,
drop_tx: Option<mpsc::Sender<()>>,
}
impl InputSource for CpalSource {
fn read(&mut self) -> Result<AudioFrame> {
self.scratch.clear();
while let Ok(s) = self.consumer.pop() {
self.scratch.push(s);
}
let ch = self.channels as usize;
let n = self.scratch.len().checked_div(ch).unwrap_or(0);
let mut planar = vec![0.0_f32; ch * n];
for (i, &v) in self.scratch.iter().enumerate().take(ch * n) {
let frame = i / ch;
let channel = i % ch;
planar[channel * n + frame] = v;
}
Ok(AudioFrame::from_planar(
self.channels,
self.sample_rate,
planar,
))
}
}
impl Drop for CpalSource {
fn drop(&mut self) {
self.drop_tx.take();
}
}