use std::ptr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use super::backend::{AudioBackend, AudioEngineHandle, BackendError, DeviceInfo};
fn suppress_stderr<F: FnOnce() -> T, T>(f: F) -> T {
use std::os::fd::AsRawFd;
let devnull = std::fs::File::open("/dev/null").ok();
let saved_fd = unsafe { nix_dup(2) };
if let Some(ref null) = devnull
&& saved_fd >= 0
{
unsafe { nix_dup2(null.as_raw_fd(), 2) };
}
let result = f();
if saved_fd >= 0 {
unsafe {
nix_dup2(saved_fd, 2);
nix_close(saved_fd);
}
}
result
}
unsafe fn nix_dup(fd: i32) -> i32 {
unsafe extern "C" {
safe fn dup(fd: i32) -> i32;
}
dup(fd)
}
unsafe fn nix_dup2(oldfd: i32, newfd: i32) -> i32 {
unsafe extern "C" {
safe fn dup2(oldfd: i32, newfd: i32) -> i32;
}
dup2(oldfd, newfd)
}
unsafe fn nix_close(fd: i32) -> i32 {
unsafe extern "C" {
safe fn close(fd: i32) -> i32;
}
close(fd)
}
pub struct CpalBackend {
host: cpal::Host,
}
impl Default for CpalBackend {
fn default() -> Self {
Self::new()
}
}
impl CpalBackend {
pub fn new() -> Self {
let host = suppress_stderr(cpal::default_host);
Self { host }
}
fn resolve_device(&self, info: &DeviceInfo) -> Result<cpal::Device, BackendError> {
let devices = self
.host
.output_devices()
.map_err(|e| BackendError::Platform(e.to_string()))?;
for dev in devices {
if let Ok(name) = dev.name()
&& name == info.name
{
return Ok(dev);
}
}
Err(BackendError::DeviceNotFound(info.name.clone()))
}
fn device_info_from_cpal(dev: &cpal::Device, index: u64) -> Option<DeviceInfo> {
let name = dev.name().ok()?;
let configs = dev.supported_output_configs().ok()?;
let mut rates: Vec<f64> = Vec::new();
for cfg in configs {
let min = cfg.min_sample_rate().0 as f64;
let max = cfg.max_sample_rate().0 as f64;
if !rates.contains(&min) {
rates.push(min);
}
if !rates.contains(&max) {
rates.push(max);
}
for &common in &[44100.0, 48000.0, 88200.0, 96000.0, 176400.0, 192000.0] {
if common >= min && common <= max && !rates.contains(&common) {
rates.push(common);
}
}
}
rates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
rates.dedup();
Some(DeviceInfo {
name,
sample_rates: rates,
platform_id: index,
})
}
}
impl AudioBackend for CpalBackend {
fn list_devices(&self) -> Result<Vec<DeviceInfo>, BackendError> {
let devices = suppress_stderr(|| self.host.output_devices())
.map_err(|e| BackendError::Platform(e.to_string()))?;
let mut result = Vec::new();
for (i, dev) in devices.enumerate() {
if let Some(info) = Self::device_info_from_cpal(&dev, i as u64) {
result.push(info);
}
}
Ok(result)
}
fn default_device(&self) -> Result<DeviceInfo, BackendError> {
let dev =
suppress_stderr(|| self.host.default_output_device()).ok_or(BackendError::NoDevices)?;
suppress_stderr(|| Self::device_info_from_cpal(&dev, 0)).ok_or(BackendError::NoDevices)
}
fn supported_sample_rates(&self, device: &DeviceInfo) -> Result<Vec<f64>, BackendError> {
Ok(device.sample_rates.clone())
}
fn get_device_sample_rate(&self, device: &DeviceInfo) -> Result<f64, BackendError> {
let dev = self.resolve_device(device)?;
let config = suppress_stderr(|| dev.default_output_config())
.map_err(|e| BackendError::Platform(e.to_string()))?;
Ok(config.sample_rate().0 as f64)
}
fn set_device_sample_rate(&self, _device: &DeviceInfo, _rate: f64) -> Result<(), BackendError> {
Ok(())
}
fn create_engine(
&self,
device: &DeviceInfo,
sample_rate: f64,
channels: u32,
consumer: rtrb::Consumer<f32>,
samples_played: Arc<AtomicU64>,
) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
let dev = self.resolve_device(device)?;
let config = cpal::StreamConfig {
channels: channels as u16,
sample_rate: cpal::SampleRate(sample_rate as u32),
buffer_size: cpal::BufferSize::Default,
};
let running = Arc::new(AtomicBool::new(false));
let running_cb = running.clone();
let consumer = std::sync::Mutex::new(consumer);
let stream = suppress_stderr(|| {
dev.build_output_stream(
&config,
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
if !running_cb.load(Ordering::Acquire) {
data.fill(0.0);
return;
}
let Ok(mut consumer) = consumer.lock() else {
data.fill(0.0);
return;
};
let total_samples = data.len();
let available = consumer.slots();
let to_read = available.min(total_samples);
if to_read > 0
&& let Ok(chunk) = consumer.read_chunk(to_read)
{
let (first, second) = chunk.as_slices();
let ring_total = first.len() + second.len();
let copy_total = ring_total.min(total_samples);
let first_copy = first.len().min(copy_total);
let second_copy = copy_total.saturating_sub(first_copy).min(second.len());
unsafe {
ptr::copy_nonoverlapping(first.as_ptr(), data.as_mut_ptr(), first_copy);
if second_copy > 0 {
ptr::copy_nonoverlapping(
second.as_ptr(),
data.as_mut_ptr().add(first_copy),
second_copy,
);
}
}
chunk.commit_all();
samples_played.fetch_add(copy_total as u64, Ordering::AcqRel);
}
if to_read < total_samples {
data[to_read..].fill(0.0);
}
},
|err| {
log::error!("cpal audio stream error: {}", err);
},
None,
)
.map_err(|e| BackendError::StreamCreation(e.to_string()))
})?;
Ok(Box::new(CpalEngineHandle { stream, running }))
}
}
struct CpalEngineHandle {
stream: cpal::Stream,
running: Arc<AtomicBool>,
}
unsafe impl Send for CpalEngineHandle {}
impl AudioEngineHandle for CpalEngineHandle {
fn start(&self) -> Result<(), BackendError> {
self.running.store(true, Ordering::Release);
self.stream
.play()
.map_err(|e| BackendError::Platform(e.to_string()))
}
fn stop(&self) -> Result<(), BackendError> {
self.running.store(false, Ordering::Release);
self.stream
.pause()
.map_err(|e| BackendError::Platform(e.to_string()))
}
fn is_running(&self) -> bool {
self.running.load(Ordering::Acquire)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpal_backend_constructs() {
let _backend = CpalBackend::new();
}
#[test]
fn cpal_list_devices_no_panic() {
let backend = CpalBackend::new();
let result = backend.list_devices();
assert!(result.is_ok());
}
#[test]
fn cpal_set_sample_rate_is_noop() {
let backend = CpalBackend::new();
let dummy = DeviceInfo {
name: "nonexistent".into(),
sample_rates: vec![44100.0],
platform_id: 0,
};
assert!(backend.set_device_sample_rate(&dummy, 96000.0).is_ok());
}
#[test]
fn cpal_device_info_has_sample_rates() {
let backend = CpalBackend::new();
if let Ok(devices) = backend.list_devices() {
for dev in &devices {
if !dev.sample_rates.is_empty() {
assert!(dev.sample_rates[0] > 0.0);
}
}
}
}
#[test]
fn cpal_resolve_nonexistent_device_fails() {
let backend = CpalBackend::new();
let dummy = DeviceInfo {
name: "this device does not exist xyzzy".into(),
sample_rates: vec![],
platform_id: 9999,
};
assert!(backend.resolve_device(&dummy).is_err());
}
}