use crate::common::{assert_error_traits, ChannelCount, SampleRate};
use crate::math::{nearest_multiple_of_two, nz};
use crate::mixer::{mixer, Mixer};
use crate::player::Player;
use crate::{decoder, Source};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{BufferSize, Sample, SampleFormat, StreamConfig, I24};
use std::fmt;
use std::io::{Read, Seek};
use std::marker::Sync;
use std::num::NonZero;
const HZ_44100: SampleRate = nz!(44_100);
pub struct MixerDeviceSink {
config: DeviceSinkConfig,
mixer: Mixer,
log_on_drop: bool,
_stream: cpal::Stream,
}
impl MixerDeviceSink {
pub fn mixer(&self) -> &Mixer {
&self.mixer
}
pub fn config(&self) -> &DeviceSinkConfig {
&self.config
}
pub fn log_on_drop(&mut self, enabled: bool) {
self.log_on_drop = enabled;
}
}
impl Drop for MixerDeviceSink {
fn drop(&mut self) {
if self.log_on_drop && !std::thread::panicking() {
#[cfg(feature = "tracing")]
tracing::debug!("Dropping DeviceSink, audio playing through this sink will stop");
#[cfg(not(feature = "tracing"))]
eprintln!("Dropping DeviceSink, audio playing through this sink will stop, to prevent this message from appearing use tracing or call `.log_on_drop(false)` on this DeviceSink")
}
}
}
impl fmt::Debug for MixerDeviceSink {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("MixerDeviceSink")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
#[derive(Copy, Clone, Debug)]
pub struct DeviceSinkConfig {
pub(crate) channel_count: ChannelCount,
pub(crate) sample_rate: SampleRate,
pub(crate) buffer_size: BufferSize,
pub(crate) sample_format: SampleFormat,
}
impl Default for DeviceSinkConfig {
fn default() -> Self {
Self {
channel_count: nz!(2),
sample_rate: HZ_44100,
buffer_size: BufferSize::Default,
sample_format: SampleFormat::F32,
}
}
}
impl DeviceSinkConfig {
pub fn channel_count(&self) -> ChannelCount {
self.channel_count
}
pub fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
pub fn buffer_size(&self) -> &BufferSize {
&self.buffer_size
}
pub fn sample_format(&self) -> SampleFormat {
self.sample_format
}
}
impl core::fmt::Debug for DeviceSinkBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let device = if let Some(device) = &self.device {
"Some(".to_owned()
+ &device
.description()
.ok()
.map_or("UnNamed".to_string(), |d| d.name().to_string())
+ ")"
} else {
"None".to_owned()
};
f.debug_struct("DeviceSinkBuilder")
.field("device", &device)
.field("config", &self.config)
.finish()
}
}
fn default_error_callback(err: cpal::StreamError) {
#[cfg(feature = "tracing")]
tracing::error!("audio stream error: {err}");
#[cfg(not(feature = "tracing"))]
eprintln!("audio stream error: {err}");
}
pub struct DeviceSinkBuilder<E = fn(cpal::StreamError)>
where
E: FnMut(cpal::StreamError) + Send + 'static,
{
device: Option<cpal::Device>,
config: DeviceSinkConfig,
error_callback: E,
}
impl Default for DeviceSinkBuilder {
fn default() -> Self {
Self {
device: None,
config: DeviceSinkConfig::default(),
error_callback: default_error_callback,
}
}
}
impl DeviceSinkBuilder {
pub fn from_device(device: cpal::Device) -> Result<DeviceSinkBuilder, DeviceSinkError> {
let default_config = device
.default_output_config()
.map_err(DeviceSinkError::DefaultSinkConfigError)?;
let mut device = Self::default()
.with_device(device)
.with_supported_config(&default_config);
let sample_rate = device.config.sample_rate().get();
let safe_buffer_size = nearest_multiple_of_two(sample_rate / (1000 / 50));
device.config.buffer_size = match device.config.buffer_size {
BufferSize::Default => BufferSize::Fixed(safe_buffer_size),
fixed @ BufferSize::Fixed(_) => fixed,
};
Ok(device)
}
pub fn from_default_device() -> Result<DeviceSinkBuilder, DeviceSinkError> {
let default_device = cpal::default_host()
.default_output_device()
.ok_or(DeviceSinkError::NoDevice)?;
Self::from_device(default_device)
}
pub fn open_default_sink() -> Result<MixerDeviceSink, DeviceSinkError> {
Self::from_default_device()
.and_then(|x| x.open_stream())
.or_else(|original_err| {
let devices = match cpal::default_host().output_devices() {
Ok(devices) => devices,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::error!("error getting list of output devices: {err}");
#[cfg(not(feature = "tracing"))]
eprintln!("error getting list of output devices: {err}");
return Err(original_err);
}
};
devices
.filter(|dev| {
dev.description()
.map(|desc| desc.driver().is_some_and(|driver| driver != "null"))
.unwrap_or(false)
})
.find_map(|d| {
Self::from_device(d)
.and_then(|x| x.open_sink_or_fallback())
.ok()
})
.ok_or(original_err)
})
}
}
impl<E> DeviceSinkBuilder<E>
where
E: FnMut(cpal::StreamError) + Send + 'static,
{
pub fn with_device(mut self, device: cpal::Device) -> DeviceSinkBuilder<E> {
self.device = Some(device);
self
}
pub fn with_channels(mut self, channel_count: ChannelCount) -> DeviceSinkBuilder<E> {
assert!(channel_count.get() > 0);
self.config.channel_count = channel_count;
self
}
pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> DeviceSinkBuilder<E> {
self.config.sample_rate = sample_rate;
self
}
pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> DeviceSinkBuilder<E> {
self.config.buffer_size = buffer_size;
self
}
pub fn with_sample_format(mut self, sample_format: SampleFormat) -> DeviceSinkBuilder<E> {
self.config.sample_format = sample_format;
self
}
pub fn with_supported_config(
mut self,
config: &cpal::SupportedStreamConfig,
) -> DeviceSinkBuilder<E> {
self.config = DeviceSinkConfig {
channel_count: NonZero::new(config.channels())
.expect("no valid cpal config has zero channels"),
sample_rate: NonZero::new(config.sample_rate())
.expect("no valid cpal config has zero sample rate"),
sample_format: config.sample_format(),
..Default::default()
};
self
}
pub fn with_config(mut self, config: &cpal::StreamConfig) -> DeviceSinkBuilder<E> {
self.config = DeviceSinkConfig {
channel_count: NonZero::new(config.channels)
.expect("no valid cpal config has zero channels"),
sample_rate: NonZero::new(config.sample_rate)
.expect("no valid cpal config has zero sample rate"),
buffer_size: config.buffer_size,
..self.config
};
self
}
pub fn with_error_callback<F>(self, callback: F) -> DeviceSinkBuilder<F>
where
F: FnMut(cpal::StreamError) + Send + 'static,
{
DeviceSinkBuilder {
device: self.device,
config: self.config,
error_callback: callback,
}
}
pub fn open_stream(self) -> Result<MixerDeviceSink, DeviceSinkError> {
let device = self.device.as_ref().expect("No output device specified");
MixerDeviceSink::open(device, &self.config, self.error_callback)
}
pub fn open_sink_or_fallback(&self) -> Result<MixerDeviceSink, DeviceSinkError>
where
E: Clone,
{
let device = self.device.as_ref().expect("No output device specified");
let error_callback = &self.error_callback;
MixerDeviceSink::open(device, &self.config, error_callback.clone()).or_else(|err| {
for supported_config in supported_output_configs(device)? {
if let Ok(handle) = DeviceSinkBuilder::default()
.with_device(device.clone())
.with_supported_config(&supported_config)
.with_error_callback(error_callback.clone())
.open_stream()
{
return Ok(handle);
}
}
Err(err)
})
}
}
pub fn play<R>(mixer: &Mixer, input: R) -> Result<Player, PlayError>
where
R: Read + Seek + Send + Sync + 'static,
{
let input = decoder::Decoder::new(input)?;
let player = Player::connect_new(mixer);
player.append(input);
Ok(player)
}
impl From<&DeviceSinkConfig> for StreamConfig {
fn from(config: &DeviceSinkConfig) -> Self {
cpal::StreamConfig {
channels: config.channel_count.get() as cpal::ChannelCount,
sample_rate: config.sample_rate.get(),
buffer_size: config.buffer_size,
}
}
}
#[derive(Debug, thiserror::Error, Clone)]
pub enum PlayError
where
Self: Send + Sync + 'static,
{
#[error("Failed to decode audio")]
DecoderError(
#[from]
#[source]
decoder::DecoderError,
),
#[error("No output device")]
NoDevice,
}
assert_error_traits!(PlayError);
#[derive(Debug, thiserror::Error)]
pub enum DeviceSinkError {
#[error("Could not start playing the stream")]
PlayError(#[source] cpal::PlayStreamError),
#[error("Failed to get the config for the given device")]
DefaultSinkConfigError(#[source] cpal::DefaultStreamConfigError),
#[error("Error opening the stream with the OS")]
BuildError(#[source] cpal::BuildStreamError),
#[error("Could not list supported configs for the device. Maybe its disconnected?")]
SupportedConfigsError(#[source] cpal::SupportedStreamConfigsError),
#[error("Could not find any output device")]
NoDevice,
#[error("New cpal sample format that rodio does not yet support please open an issue if you run into this.")]
UnsupportedSampleFormat,
}
impl MixerDeviceSink {
fn validate_config(config: &DeviceSinkConfig) {
if let BufferSize::Fixed(sz) = config.buffer_size {
assert!(sz > 0, "fixed buffer size must be greater than zero");
}
}
pub(crate) fn open<E>(
device: &cpal::Device,
config: &DeviceSinkConfig,
error_callback: E,
) -> Result<MixerDeviceSink, DeviceSinkError>
where
E: FnMut(cpal::StreamError) + Send + 'static,
{
Self::validate_config(config);
let (controller, source) = mixer(config.channel_count, config.sample_rate);
Self::init_stream(device, config, source, error_callback).and_then(|stream| {
stream.play().map_err(DeviceSinkError::PlayError)?;
Ok(Self {
_stream: stream,
mixer: controller,
config: *config,
log_on_drop: true,
})
})
}
fn init_stream<S, E>(
device: &cpal::Device,
config: &DeviceSinkConfig,
mut samples: S,
error_callback: E,
) -> Result<cpal::Stream, DeviceSinkError>
where
S: Source + Send + 'static,
E: FnMut(cpal::StreamError) + Send + 'static,
{
let cpal_config = config.into();
macro_rules! build_output_streams {
($($sample_format:tt, $generic:ty);+) => {
match config.sample_format {
$(
cpal::SampleFormat::$sample_format => device.build_output_stream::<$generic, _, _>(
&cpal_config,
move |data, _| {
data.iter_mut().for_each(|d| {
*d = samples
.next()
.map(Sample::from_sample)
.unwrap_or(<$generic>::EQUILIBRIUM)
})
},
error_callback,
None,
),
)+
_ => return Err(DeviceSinkError::UnsupportedSampleFormat),
}
};
}
let result = build_output_streams!(
F32, f32;
F64, f64;
I8, i8;
I16, i16;
I24, I24;
I32, i32;
I64, i64;
U8, u8;
U16, u16;
U24, cpal::U24;
U32, u32;
U64, u64
);
result.map_err(DeviceSinkError::BuildError)
}
}
pub fn supported_output_configs(
device: &cpal::Device,
) -> Result<impl Iterator<Item = cpal::SupportedStreamConfig>, DeviceSinkError> {
let mut supported: Vec<_> = device
.supported_output_configs()
.map_err(DeviceSinkError::SupportedConfigsError)?
.collect();
supported.sort_by(|a, b| b.cmp_default_heuristics(a));
Ok(supported.into_iter().flat_map(|sf| {
let max_rate = sf.max_sample_rate();
let min_rate = sf.min_sample_rate();
let mut formats = vec![sf.with_max_sample_rate()];
let preferred_rate = HZ_44100.get();
if preferred_rate < max_rate && preferred_rate > min_rate {
formats.push(sf.with_sample_rate(preferred_rate))
}
formats.push(sf.with_sample_rate(min_rate));
formats
}))
}