use std::str::FromStr;
use cpal::traits::{DeviceTrait, HostTrait};
use crate::Error;
const RATES: &[u32] = &[48_000, 44_100];
const CHANNELS: &[u16] = &[2, 1];
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 host: String,
}
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 preferred = cpal::default_host().id();
let mut devices = Vec::new();
let mut seen = std::collections::HashSet::new();
for id in cpal::available_hosts() {
let host = match cpal::host_from_id(id) {
Ok(host) => host,
Err(err) => {
tracing::debug!(host = id.name(), error = %err, "skipping an audio host that would not open");
continue;
}
};
let default = host.default_output_device().and_then(|d| d.id().ok());
let outputs = match host.output_devices() {
Ok(outputs) => outputs,
Err(err) => {
tracing::debug!(host = id.name(), error = %err, "skipping a host that would not list its outputs");
continue;
}
};
for device in outputs {
let Ok(device_id) = device.id() else {
tracing::debug!(host = id.name(), "skipping an output device with no id");
continue;
};
if !seen.insert(device_id.to_string()) {
continue;
}
devices.push(Device {
default: id == preferred && Some(&device_id) == default.as_ref(),
name: describe(&device, &device_id),
host: id.name().to_string(),
id: device_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 = device
.supported_output_configs()
.map_err(|err| Error::Playback(format!("cannot enumerate output configs: {err}")))?;
choose(supported).ok_or_else(|| Error::Unsupported("output device offers no sample format we can write".into()))
}
fn choose(supported: impl Iterator<Item = cpal::SupportedStreamConfigRange>) -> Option<cpal::SupportedStreamConfig> {
supported
.filter(|config| FORMATS.contains(&config.sample_format()))
.min_by_key(preference)
.map(|config| match preferred_rate(&config) {
Some(rate) => config.try_with_sample_rate(rate).expect("a rate the range covers"),
None => config.with_max_sample_rate(),
})
}
fn preference(config: &cpal::SupportedStreamConfigRange) -> (usize, usize, std::cmp::Reverse<u32>, usize) {
let channels = CHANNELS
.iter()
.position(|count| *count == config.channels())
.unwrap_or(CHANNELS.len());
let rate = match preferred_rate(config) {
Some(rate) => (RATES.iter().position(|r| *r == rate).expect("from RATES"), rate),
None => (RATES.len(), config.max_sample_rate()),
};
(
channels,
rate.0,
std::cmp::Reverse(rate.1),
rank(config.sample_format()),
)
}
fn preferred_rate(config: &cpal::SupportedStreamConfigRange) -> Option<u32> {
RATES
.iter()
.copied()
.find(|rate| config.min_sample_rate() <= *rate && *rate <= config.max_sample_rate())
}
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())
}
#[cfg(test)]
mod tests {
use cpal::{SampleFormat, SupportedBufferSize, SupportedStreamConfigRange};
use super::*;
fn range(channels: u16, rate: u32, format: SampleFormat) -> SupportedStreamConfigRange {
SupportedStreamConfigRange::new(channels, rate, rate, SupportedBufferSize::Unknown, format)
}
#[test]
fn stereo_wins_even_when_the_device_lists_mono_first() {
let chosen = choose([range(1, 48_000, SampleFormat::F32), range(2, 48_000, SampleFormat::F32)].into_iter())
.expect("a config");
assert_eq!(chosen.channels(), 2);
}
#[test]
fn mono_is_taken_when_that_is_all_there_is() {
let chosen = choose([range(1, 48_000, SampleFormat::F32)].into_iter()).expect("a config");
assert_eq!(chosen.channels(), 1);
}
#[test]
fn a_count_we_do_not_prefer_still_opens() {
let chosen = choose([range(6, 48_000, SampleFormat::F32)].into_iter()).expect("a config");
assert_eq!(chosen.channels(), 6);
}
#[test]
fn channels_outrank_the_sample_rate() {
let chosen = choose([range(1, 48_000, SampleFormat::F32), range(2, 44_100, SampleFormat::F32)].into_iter())
.expect("a config");
assert_eq!((chosen.channels(), chosen.sample_rate()), (2, 44_100));
}
#[test]
fn f32_is_preferred_over_a_format_we_convert_to() {
let chosen = choose([range(2, 48_000, SampleFormat::I16), range(2, 48_000, SampleFormat::F32)].into_iter())
.expect("a config");
assert_eq!(chosen.sample_format(), SampleFormat::F32);
}
#[test]
fn a_device_we_cannot_write_to_is_rejected() {
assert!(choose([range(2, 48_000, SampleFormat::I8)].into_iter()).is_none());
assert!(choose(std::iter::empty()).is_none());
}
#[test]
fn stereo_at_an_awkward_rate_beats_mono_at_a_preferred_one() {
let chosen = choose([range(1, 48_000, SampleFormat::F32), range(2, 96_000, SampleFormat::F32)].into_iter())
.expect("a config");
assert_eq!((chosen.channels(), chosen.sample_rate()), (2, 96_000));
}
#[test]
fn stereo_beats_mono_when_neither_is_on_a_preferred_rate() {
let chosen = choose(
[
range(1, 192_000, SampleFormat::F32),
range(2, 96_000, SampleFormat::F32),
]
.into_iter(),
)
.expect("a config");
assert_eq!((chosen.channels(), chosen.sample_rate()), (2, 96_000));
}
#[test]
fn the_last_resort_takes_the_highest_rate() {
let chosen = choose(
[
range(2, 96_000, SampleFormat::I16),
range(2, 96_000, SampleFormat::F32),
range(2, 32_000, SampleFormat::F32),
]
.into_iter(),
)
.expect("a config");
assert_eq!(
(chosen.sample_rate(), chosen.sample_format()),
(96_000, SampleFormat::F32)
);
}
}