use std::sync::Mutex;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::device::{compute_is_default, ComputedRow, DeviceSelector, MicrophoneInfo, SpeakerInfo};
use crate::error::DecibriError;
pub(crate) struct BackendDevice {
inner: cpal::Device,
}
pub(crate) struct BackendStream {
inner: Mutex<Option<cpal::Stream>>,
}
impl BackendStream {
pub(crate) fn stop(&self) {
if let Ok(mut guard) = self.inner.lock() {
*guard = None;
}
}
#[cfg(test)]
pub(crate) fn is_active(&self) -> bool {
self.inner.lock().map(|g| g.is_some()).unwrap_or(false)
}
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self {
inner: Mutex::new(None),
}
}
}
pub(crate) struct StreamParams {
pub channels: u16,
pub sample_rate: u32,
pub frames_per_buffer: Option<u32>,
}
#[cfg(feature = "capture")]
pub(crate) type InputDataCallback = Box<dyn FnMut(&[f32]) + Send + 'static>;
#[cfg(feature = "playback")]
pub(crate) type OutputDataCallback = Box<dyn FnMut(&mut [f32]) + Send + 'static>;
pub(crate) type StreamErrorCallback = Box<dyn FnMut(DecibriError) + Send + 'static>;
pub(crate) trait AudioBackend: Send + Sync {
fn input_devices(&self) -> Result<Vec<MicrophoneInfo>, DecibriError>;
fn output_devices(&self) -> Result<Vec<SpeakerInfo>, DecibriError>;
#[cfg(feature = "capture")]
fn resolve_input_device(
&self,
selector: &DeviceSelector,
) -> Result<BackendDevice, DecibriError>;
#[cfg(feature = "capture")]
fn native_input_rate(&self, device: &BackendDevice) -> Result<u32, DecibriError>;
#[cfg(feature = "playback")]
fn resolve_output_device(
&self,
selector: &DeviceSelector,
) -> Result<BackendDevice, DecibriError>;
#[cfg(feature = "capture")]
fn open_input_stream(
&self,
device: &BackendDevice,
params: &StreamParams,
on_data: InputDataCallback,
on_error: StreamErrorCallback,
) -> Result<BackendStream, DecibriError>;
#[cfg(feature = "playback")]
fn open_output_stream(
&self,
device: &BackendDevice,
params: &StreamParams,
on_data: OutputDataCallback,
on_error: StreamErrorCallback,
) -> Result<BackendStream, DecibriError>;
}
pub(crate) struct CpalBackend;
impl AudioBackend for CpalBackend {
fn input_devices(&self) -> Result<Vec<MicrophoneInfo>, DecibriError> {
enumerate_devices::<Input>()
}
fn output_devices(&self) -> Result<Vec<SpeakerInfo>, DecibriError> {
enumerate_devices::<Output>()
}
#[cfg(feature = "capture")]
fn resolve_input_device(
&self,
selector: &DeviceSelector,
) -> Result<BackendDevice, DecibriError> {
Ok(BackendDevice {
inner: resolve_device_generic::<Input>(selector)?,
})
}
#[cfg(feature = "capture")]
fn native_input_rate(&self, device: &BackendDevice) -> Result<u32, DecibriError> {
device
.inner
.default_input_config()
.map(|config| config.sample_rate())
.map_err(|e| DecibriError::DeviceEnumerationFailed(e.to_string()))
}
#[cfg(feature = "playback")]
fn resolve_output_device(
&self,
selector: &DeviceSelector,
) -> Result<BackendDevice, DecibriError> {
Ok(BackendDevice {
inner: resolve_device_generic::<Output>(selector)?,
})
}
#[cfg(feature = "capture")]
fn open_input_stream(
&self,
device: &BackendDevice,
params: &StreamParams,
mut on_data: InputDataCallback,
mut on_error: StreamErrorCallback,
) -> Result<BackendStream, DecibriError> {
let stream = device
.inner
.build_input_stream(
&stream_config(params),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
on_data(data);
},
move |err| {
on_error(DecibriError::DeviceFailed {
source: Box::new(err),
});
},
None,
)
.map_err(|e| DecibriError::StreamOpenFailed(e.to_string()))?;
stream
.play()
.map_err(|e| DecibriError::StreamStartFailed(e.to_string()))?;
Ok(BackendStream {
inner: Mutex::new(Some(stream)),
})
}
#[cfg(feature = "playback")]
fn open_output_stream(
&self,
device: &BackendDevice,
params: &StreamParams,
mut on_data: OutputDataCallback,
mut on_error: StreamErrorCallback,
) -> Result<BackendStream, DecibriError> {
let stream = device
.inner
.build_output_stream(
&stream_config(params),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
on_data(data);
},
move |err| {
on_error(DecibriError::DeviceFailed {
source: Box::new(err),
});
},
None,
)
.map_err(|e| DecibriError::StreamOpenFailed(e.to_string()))?;
stream
.play()
.map_err(|e| DecibriError::StreamStartFailed(e.to_string()))?;
Ok(BackendStream {
inner: Mutex::new(Some(stream)),
})
}
}
fn stream_config(params: &StreamParams) -> cpal::StreamConfig {
cpal::StreamConfig {
channels: params.channels,
sample_rate: params.sample_rate,
buffer_size: match params.frames_per_buffer {
Some(frames) => cpal::BufferSize::Fixed(frames),
None => cpal::BufferSize::Default,
},
}
}
trait DeviceDirection {
type Info;
fn default_device(host: &cpal::Host) -> Option<cpal::Device>;
fn list_devices(host: &cpal::Host) -> Result<Vec<cpal::Device>, DecibriError>;
fn default_config(device: &cpal::Device) -> (u16, u32);
fn build_info(row: ComputedRow) -> Self::Info;
fn no_device_error() -> DecibriError;
fn not_found_error(query: String) -> DecibriError;
}
struct Input;
struct Output;
impl DeviceDirection for Input {
type Info = MicrophoneInfo;
fn default_device(host: &cpal::Host) -> Option<cpal::Device> {
host.default_input_device()
}
fn list_devices(host: &cpal::Host) -> Result<Vec<cpal::Device>, DecibriError> {
host.input_devices()
.map(|it| it.collect())
.map_err(|e| DecibriError::DeviceEnumerationFailed(e.to_string()))
}
fn default_config(device: &cpal::Device) -> (u16, u32) {
match device.default_input_config() {
Ok(config) => (config.channels(), config.sample_rate()),
Err(_) => (0, 0),
}
}
fn build_info(row: ComputedRow) -> Self::Info {
MicrophoneInfo {
index: row.index,
name: row.name,
id: row.id,
max_input_channels: row.channels,
default_sample_rate: row.sample_rate,
is_default: row.is_default,
}
}
fn no_device_error() -> DecibriError {
DecibriError::NoMicrophoneFound
}
fn not_found_error(query: String) -> DecibriError {
DecibriError::MicrophoneNotFound(query)
}
}
impl DeviceDirection for Output {
type Info = SpeakerInfo;
fn default_device(host: &cpal::Host) -> Option<cpal::Device> {
host.default_output_device()
}
fn list_devices(host: &cpal::Host) -> Result<Vec<cpal::Device>, DecibriError> {
host.output_devices()
.map(|it| it.collect())
.map_err(|e| DecibriError::DeviceEnumerationFailed(e.to_string()))
}
fn default_config(device: &cpal::Device) -> (u16, u32) {
match device.default_output_config() {
Ok(config) => (config.channels(), config.sample_rate()),
Err(_) => (0, 0),
}
}
fn build_info(row: ComputedRow) -> Self::Info {
SpeakerInfo {
index: row.index,
name: row.name,
id: row.id,
max_output_channels: row.channels,
default_sample_rate: row.sample_rate,
is_default: row.is_default,
}
}
fn no_device_error() -> DecibriError {
DecibriError::NoSpeakerFound
}
fn not_found_error(query: String) -> DecibriError {
DecibriError::SpeakerNotFound(query)
}
}
fn enumerate_devices<D: DeviceDirection>() -> Result<Vec<D::Info>, DecibriError> {
let host = cpal::default_host();
let default_id = D::default_device(&host).and_then(|d| d.id().ok());
let devices = D::list_devices(&host)?;
let rows: Vec<(Option<cpal::DeviceId>, String, u16, u32)> = devices
.into_iter()
.enumerate()
.map(|(index, device)| {
let id = device.id().ok();
let name = device
.description()
.map(|d| d.name().to_string())
.unwrap_or_else(|_| format!("Unknown Device {index}"));
let (channels, sample_rate) = D::default_config(&device);
(id, name, channels, sample_rate)
})
.collect();
Ok(compute_is_default(rows, default_id)
.into_iter()
.map(D::build_info)
.collect())
}
fn resolve_device_generic<D: DeviceDirection>(
selector: &DeviceSelector,
) -> Result<cpal::Device, DecibriError> {
let host = cpal::default_host();
match selector {
DeviceSelector::Default => D::default_device(&host).ok_or_else(D::no_device_error),
DeviceSelector::Index(idx) => D::list_devices(&host)?
.into_iter()
.nth(*idx)
.ok_or(DecibriError::DeviceIndexOutOfRange),
DeviceSelector::Name(query) => {
let query_lower = query.to_lowercase();
let devices = D::list_devices(&host)?;
let mut matches: Vec<(usize, String, cpal::Device)> = Vec::new();
for (index, device) in devices.into_iter().enumerate() {
let name = device
.description()
.map(|d| d.name().to_string())
.unwrap_or_default();
if name.to_lowercase().contains(&query_lower) {
matches.push((index, name, device));
}
}
match matches.len() {
0 => Err(D::not_found_error(query.clone())),
1 => Ok(matches.into_iter().next().unwrap().2),
_ => {
let match_list = matches
.iter()
.map(|(idx, name, _)| format!(" [{idx}] {name}"))
.collect::<Vec<_>>()
.join("\n");
Err(DecibriError::MultipleDevicesMatch {
name: query.clone(),
matches: format!(
"{match_list}\nUse a more specific name or pass the device index directly."
),
})
}
}
}
DeviceSelector::Id(query) => {
let devices = D::list_devices(&host)?;
for device in devices {
if let Ok(id) = device.id() {
if id.to_string() == *query {
return Ok(device);
}
}
}
Err(D::not_found_error(query.clone()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpal_backend_satisfies_the_seam() {
fn assert_backend<T: AudioBackend>() {}
fn assert_send_sync<T: Send + Sync>() {}
assert_backend::<CpalBackend>();
assert_send_sync::<CpalBackend>();
}
#[test]
fn backend_stream_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BackendStream>();
}
#[test]
fn backend_stream_stop_empties_and_is_idempotent() {
let stream = BackendStream::empty();
assert!(!stream.is_active(), "an empty handle holds no stream");
stream.stop();
assert!(!stream.is_active(), "stop() leaves the slot empty");
stream.stop(); }
#[test]
fn backend_enumeration_runs() {
let _ = CpalBackend.input_devices();
let _ = CpalBackend.output_devices();
}
#[test]
fn not_found_error_produces_direction_correct_variant() {
let input_err = Input::not_found_error("nonexistent-input".to_string());
let output_err = Output::not_found_error("nonexistent-output".to_string());
assert!(
matches!(input_err, DecibriError::MicrophoneNotFound(ref s) if s == "nonexistent-input"),
"Input direction must return DecibriError::MicrophoneNotFound"
);
assert!(
matches!(output_err, DecibriError::SpeakerNotFound(ref s) if s == "nonexistent-output"),
"Output direction must return DecibriError::SpeakerNotFound"
);
assert_eq!(
input_err.to_string(),
"No microphone found matching \"nonexistent-input\"",
"Input variant's Display must name the microphone"
);
assert_eq!(
output_err.to_string(),
"No speaker found matching \"nonexistent-output\"",
"Output variant's Display must name the speaker"
);
}
}