use std::str::FromStr;
use cpal::traits::{DeviceTrait, HostTrait};
use crate::Error;
const RATES: &[u32] = &[48_000, 44_100];
const FORMATS: &[cpal::SampleFormat] = &[
cpal::SampleFormat::F32,
cpal::SampleFormat::I32,
cpal::SampleFormat::I16,
cpal::SampleFormat::U16,
];
#[derive(Clone, Debug)]
pub struct Device {
pub id: String,
pub name: String,
pub default: bool,
}
pub async fn devices() -> Result<Vec<Device>, Error> {
tokio::task::spawn_blocking(list)
.await
.map_err(|err| Error::Playback(format!("audio host thread failed: {err}")))?
}
fn list() -> Result<Vec<Device>, Error> {
let mut devices = Vec::new();
for id in cpal::available_hosts() {
let Ok(host) = cpal::host_from_id(id) else { continue };
let default = host.default_output_device().and_then(|d| d.id().ok());
let Ok(outputs) = host.output_devices() else { continue };
for device in outputs {
let Ok(id) = device.id() else { continue };
devices.push(Device {
default: Some(&id) == default.as_ref(),
name: describe(&device, &id),
id: id.to_string(),
});
}
}
Ok(devices)
}
pub(super) fn open(selector: Option<&str>) -> Result<cpal::Device, Error> {
let Some(selector) = selector else {
return cpal::default_host()
.default_output_device()
.ok_or_else(|| Error::Device("no default output device".into()));
};
let id = cpal::DeviceId::from_str(selector).map_err(|err| Error::Device(format!("{selector:?}: {err}")))?;
let host = cpal::host_from_id(id.host()).map_err(|err| Error::Device(format!("{selector:?}: {err}")))?;
host.device_by_id(&id)
.ok_or_else(|| Error::Device(format!("output device {selector:?} not found")))
}
pub(super) fn negotiate(device: &cpal::Device) -> Result<cpal::SupportedStreamConfig, Error> {
let supported: Vec<_> = device
.supported_output_configs()
.map_err(|err| Error::Playback(format!("cannot enumerate output configs: {err}")))?
.filter(|config| FORMATS.contains(&config.sample_format()))
.collect();
for &rate in RATES {
for &format in FORMATS {
let config = supported
.iter()
.filter(|c| c.sample_format() == format)
.find_map(|c| (*c).try_with_sample_rate(rate));
if let Some(config) = config {
return Ok(config);
}
}
}
supported
.into_iter()
.max_by_key(|c| (c.max_sample_rate(), std::cmp::Reverse(rank(c.sample_format()))))
.map(|c| c.with_max_sample_rate())
.ok_or_else(|| Error::Unsupported("output device offers no sample format we can write".into()))
}
fn rank(format: cpal::SampleFormat) -> usize {
FORMATS.iter().position(|f| *f == format).unwrap_or(FORMATS.len())
}
fn describe(device: &cpal::Device, id: &cpal::DeviceId) -> String {
device
.description()
.map(|d| d.name().to_string())
.unwrap_or_else(|_| id.id().to_string())
}