use crate::{
manager::{BackendSource, Manager, Renderer},
Sound,
};
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Error as CpalError, ErrorKind, FromSample, SizedSample,
};
use std::error::Error;
pub use cpal::BufferSize as CpalBufferSize;
pub struct CpalBackend {
channel_count: u16,
sample_rate: u32,
sample_format: cpal::SampleFormat,
buffer_size: CpalBufferSize,
device: cpal::Device,
stream: Option<cpal::Stream>,
}
impl CpalBackend {
pub fn with_defaults() -> Option<CpalBackend> {
let host = cpal::default_host();
let device = host.default_output_device()?;
let default_config = device.default_output_config().ok()?;
let sample_rate = default_config.sample_rate();
let channel_count = default_config.channels();
let sample_format = default_config.sample_format();
Some(CpalBackend {
channel_count,
sample_rate,
buffer_size: CpalBufferSize::Default,
device,
stream: None,
sample_format,
})
}
pub fn with_default_host_and_device(
channel_count: u16,
sample_rate: u32,
buffer_size: CpalBufferSize,
) -> Option<CpalBackend> {
let host = cpal::default_host();
let device = host.default_output_device()?;
let sample_format = device.default_output_config().ok()?.sample_format();
Some(CpalBackend {
channel_count,
sample_rate,
buffer_size,
device,
stream: None,
sample_format,
})
}
pub fn new(
channel_count: u16,
sample_rate: u32,
buffer_size: CpalBufferSize,
device: cpal::Device,
sample_format: cpal::SampleFormat,
) -> CpalBackend {
CpalBackend {
channel_count,
sample_rate,
buffer_size,
device,
stream: None,
sample_format,
}
}
}
impl CpalBackend {
pub fn start<E>(&mut self, error_callback: E) -> Result<Manager, CpalBackendError>
where
E: FnMut(CpalError) + Send + 'static,
{
let (manager, mut renderer) = Manager::new();
renderer.set_output_channel_count_and_sample_rate(self.channel_count, self.sample_rate);
let Ok(crate::NextSample::MetadataChanged) = renderer.next_sample() else {
panic!("expected MetadataChanged event")
};
let config = cpal::StreamConfig {
channels: self.channel_count,
sample_rate: self.sample_rate,
buffer_size: self.buffer_size,
};
let timeout = None;
let stream = match self.sample_format {
cpal::SampleFormat::I16 => self
.device
.build_output_stream(
config,
make_data_callback::<i16>(renderer, self.channel_count),
error_callback,
timeout,
)
.map_err(CpalBackendError::BuildStream)?,
cpal::SampleFormat::F32 => self
.device
.build_output_stream(
config,
make_data_callback::<f32>(renderer, self.channel_count),
error_callback,
timeout,
)
.map_err(CpalBackendError::BuildStream)?,
sample_format => {
return Err(CpalBackendError::BuildStream(CpalError::with_message(
ErrorKind::UnsupportedConfig,
format!(
"unsupported output stream sample format: {:?}",
sample_format
),
)))
}
};
stream.play().map_err(CpalBackendError::PlayStream)?;
self.stream = Some(stream);
Ok(manager)
}
}
fn make_data_callback<T>(
mut renderer: Renderer,
channel_count: u16,
) -> impl FnMut(&mut [T], &cpal::OutputCallbackInfo)
where
T: SizedSample + FromSample<i16>,
{
move |buffer: &mut [T], _info: &cpal::OutputCallbackInfo| {
assert!(buffer.len().is_multiple_of(channel_count as usize));
renderer.on_start_of_batch();
buffer.fill_with(|| {
let sample = renderer
.next_sample()
.expect("renderer should never return an Error");
match sample {
crate::NextSample::Sample(s) => T::from_sample(s),
crate::NextSample::MetadataChanged => {
unreachable!("we never change metadata mid-batch")
}
crate::NextSample::Paused => T::from_sample(0), crate::NextSample::Finished => T::from_sample(0), }
});
}
}
#[derive(Debug)]
pub enum CpalBackendError {
NoDevice,
BuildStream(CpalError),
PlayStream(CpalError),
}
impl std::fmt::Display for CpalBackendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CpalBackendError::NoDevice => {
write!(f, "unable to find suitable device or config")
}
CpalBackendError::BuildStream(_) => {
write!(f, "unable to build stream")
}
CpalBackendError::PlayStream(_) => {
write!(f, "unable to play stream")
}
}
}
}
impl Error for CpalBackendError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
CpalBackendError::NoDevice => None,
CpalBackendError::BuildStream(e) => Some(e),
CpalBackendError::PlayStream(e) => Some(e),
}
}
}