diff --git a/src/lib.rs b/src/lib.rs index b4fcfaa0..66ebfd3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,6 +171,8 @@ pub use cpal::{ mod common; mod sink; mod spatial_sink; +// #[cfg(all(feature = "playback", feature = "experimental"))] +pub mod speakers; #[cfg(feature = "playback")] pub mod stream; #[cfg(feature = "wav_output")] diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index deac72da..bbbdfaa7 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -38,11 +38,11 @@ pub enum Error { } assert_error_traits! {Error} -/// Generic on the `MicrophoneBuilder` which is only present when a config has been set. +/// Generic on the `MicrophoneBuilder` which is only present when a device has been set. /// Methods needing a config are only available on MicrophoneBuilder with this /// Generic set. pub struct DeviceIsSet; -/// Generic on the `MicrophoneBuilder` which is only present when a device has been set. +/// Generic on the `MicrophoneBuilder` which is only present when a config has been set. /// Methods needing a device set are only available on MicrophoneBuilder with this /// Generic set. pub struct ConfigIsSet; @@ -272,7 +272,12 @@ where Ok(()) } } +} +impl MicrophoneBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ /// Sets the sample rate for input. /// /// # Error @@ -512,8 +517,8 @@ where /// println!("Channel count: {}", config.channel_count.get()); /// # Ok::<(), Box>(()) /// ``` - pub fn get_config(&self) -> &InputConfig { - self.config.as_ref().expect("ConfigIsSet") + pub fn get_config(&self) -> InputConfig { + self.config.expect("ConfigIsSet") } } diff --git a/src/queue.rs b/src/queue.rs index 495606b4..c70adc8b 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -56,14 +56,15 @@ pub struct SourcesQueueInput { impl SourcesQueueInput { /// Adds a new source to the end of the queue. #[inline] - pub fn append(&self, source: T) + pub fn append(&self, source: T) -> usize where T: Source + Send + 'static, { - self.next_sounds + let mut next_sounds = self.next_sounds .lock() - .unwrap() - .push((Box::new(source) as Box<_>, None)); + .unwrap(); + next_sounds.push((Box::new(source) as Box<_>, None)); + next_sounds.len() } /// Adds a new source to the end of the queue. diff --git a/src/speakers.rs b/src/speakers.rs new file mode 100644 index 00000000..971ae676 --- /dev/null +++ b/src/speakers.rs @@ -0,0 +1,179 @@ +//! A speakers sink +//! +//! An audio *stream* originates at a [Source] and flows to a Sink. This is a +//! Sink that plays audio over the systems speakers or headphones through an +//! audio output device; +//! +//! # Basic Usage +//! +//! ```no_run +//! # use rodio::speakers::SpeakersBuilder; +//! # use rodio::{Source, source::SineWave}; +//! # use std::time::Duration; +//! let speakers = SpeakersBuilder::new() +//! .default_device()? +//! .default_config()? +//! .open_stream()?; +//! let mixer = speakers.mixer(); +//! +//! // Play a beep for 4 seconds +//! mixer.add(SineWave::new(440.).take_duration(Duration::from_secs(4))); +//! std::thread::sleep(Duration::from_secs(4)); +//! +//! # Ok::<(), Box>(()) +//! ``` +//! +//! # Use preferred parameters if supported +//! Attempt to set a specific channel count, sample rate and buffer size but +//! fall back to the default if the device does not support these +//! +//! ```no_run +//! use rodio::speakers::SpeakersBuilder; +//! use rodio::Source; +//! use std::time::Duration; +//! +//! # fn main() -> Result<(), Box> { +//! let mut builder = SpeakersBuilder::new() +//! .default_device()? +//! .default_config()? +//! .prefer_channel_counts([ +//! 1.try_into().expect("not zero"), +//! 2.try_into().expect("not zero"), +//! ]) +//! .prefer_sample_rates([ +//! 16_000.try_into().expect("not zero"), +//! 32_000.try_into().expect("not zero"), +//! ]) +//! .prefer_buffer_sizes(512..); +//! +//! let mic = builder.open_stream()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Configuration with Error Handling +//! Attempt to set a specific channel count but fall back to the default if +//! the device doesn't support it: +//! +//! ```no_run +//! use rodio::speakers::SpeakersBuilder; +//! use rodio::Source; +//! use std::time::Duration; +//! +//! # fn main() -> Result<(), Box> { +//! let mut builder = SpeakersBuilder::new() +//! .default_device()? +//! .default_config()?; +//! +//! // Try to set stereo recording (2 channels), but continue with default if unsupported +//! if let Ok(configured_builder) = builder.try_channels(2.try_into()?) { +//! builder = configured_builder; +//! } else { +//! println!("Stereo recording not supported, using default channel configuration"); +//! // builder remains unchanged with default configuration +//! } +//! +//! let mic = builder.open_stream()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Device Selection +//! +//! ```no_run +//! use rodio::speakers::{SpeakersBuilder, available_outputs}; +//! +//! # fn main() -> Result<(), Box> { +//! // List all available input devices +//! let inputs = available_outputs()?; +//! for (i, input) in inputs.iter().enumerate() { +//! println!("Input {}: {}", i, input); +//! } +//! +//! // Use a specific device (e.g., the second one) +//! let mic = SpeakersBuilder::new() +//! .device(inputs[1].clone())? +//! .default_config()? +//! .open_stream()?; +//! # Ok(()) +//! # } +//! ``` + +use core::fmt; + +use cpal::{ + traits::{DeviceTrait, HostTrait}, + Device, +}; + +use crate::{common::assert_error_traits, StreamError}; + +/// TODO +pub mod builder; +mod config; + +pub use builder::SpeakersBuilder; +pub use config::OutputConfig; + +struct Speakers; + +/// Error that can occur when we can not list the output devices +#[derive(Debug, thiserror::Error, Clone)] +#[error("Could not list input devices")] +pub struct ListError(#[source] cpal::DevicesError); +assert_error_traits! {ListError} + +/// An input device +#[derive(Clone)] +pub struct Output { + inner: cpal::Device, + default: bool, +} + +impl From for cpal::Device { + fn from(val: Output) -> Self { + val.inner + } +} + +impl Output { + /// TODO doc comment also mirror to microphone api + pub fn is_default(&self) -> bool { + self.default + } +} + +impl fmt::Debug for Output { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Device") + .field("inner", &self.inner.name().unwrap_or("unknown".to_string())) + .finish() + } +} + +impl fmt::Display for Output { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner.name().unwrap_or("unknown".to_string())) + } +} + +/// Returns a list of available output devices on the system. +pub fn available_outputs() -> Result, ListError> { + let host = cpal::default_host(); + let default = host.default_output_device().map(|d| d.name()); + let devices = host.output_devices().map_err(ListError)?.map(|dev| Output { + default: Some(dev.name()) == default, + inner: dev, + }); + Ok(devices.collect()) +} + +impl Speakers { + fn open( + device: Device, + config: OutputConfig, + error_callback: impl FnMut(cpal::StreamError) + Send + 'static, + ) -> Result { + crate::stream::OutputStream::open(&device, &config.into_cpal_config(), error_callback) + } +} diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs new file mode 100644 index 00000000..40bd6bc0 --- /dev/null +++ b/src/speakers/builder.rs @@ -0,0 +1,553 @@ +use std::{fmt::Debug, marker::PhantomData}; + +use cpal::{ + traits::{DeviceTrait, HostTrait}, + SupportedStreamConfigRange, +}; + +use crate::{ + common::assert_error_traits, + speakers::{self, config::OutputConfig}, + ChannelCount, OutputStream, SampleRate, +}; + +/// Error configuring or opening speakers input +#[allow(missing_docs)] +#[derive(Debug, thiserror::Error, Clone)] +pub enum Error { + /// No output device is available on the system. + #[error("There is no input device")] + NoDevice, + /// Failed to get the default output configuration for the device. + #[error("Could not get default output configuration for output device: '{device_name}'")] + DefaultOutputConfig { + #[source] + source: cpal::DefaultStreamConfigError, + device_name: String, + }, + /// Failed to get the supported output configurations for the device. + #[error("Could not get supported output configurations for output device: '{device_name}'")] + OutputConfigs { + #[source] + source: cpal::SupportedStreamConfigsError, + device_name: String, + }, + /// The requested output configuration is not supported by the device. + #[error("The output configuration is not supported by output device: '{device_name}'")] + UnsupportedByDevice { device_name: String }, +} +assert_error_traits! {Error} + +/// Generic on the `SpeakersBuilder` which is only present when a config has been set. +/// Methods needing a config are only available on SpeakersBuilder with this +/// Generic set. +pub struct DeviceIsSet; +/// Generic on the `SpeakersBuilder` which is only present when a device has been set. +/// Methods needing a device set are only available on SpeakersBuilder with this +/// Generic set. +pub struct ConfigIsSet; + +/// Generic on the `SpeakersBuilder` which indicates no config has been set. +/// Some methods are only available when this types counterpart: `ConfigIsSet` is present. +pub struct ConfigNotSet; +/// Generic on the `SpeakersBuilder` which indicates no device has been set. +/// Some methods are only available when this types counterpart: `DeviceIsSet` is present. +pub struct DeviceNotSet; + +/// Builder for configuring and opening speakers input streams. +#[must_use] +pub struct SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + device: Option<(cpal::Device, Vec)>, + config: Option, + error_callback: E, + + device_set: PhantomData, + config_set: PhantomData, +} + +impl Debug for SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SpeakersBuilder") + .field( + "device", + &self + .device + .as_ref() + .map(|d| d.0.name().unwrap_or("unknown".to_string())), + ) + .field("config", &self.config) + .finish() + } +} + +impl Default for SpeakersBuilder { + fn default() -> Self { + Self { + device: None, + config: None, + error_callback: default_error_callback, + + device_set: PhantomData, + config_set: PhantomData, + } + } +} + +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}"); +} + +impl SpeakersBuilder { + /// Creates a new speakers builder. + /// + /// # Example + /// ```no_run + /// let builder = rodio::speakers::SpeakersBuilder::new(); + /// ``` + pub fn new() -> SpeakersBuilder { + Self::default() + } +} + +impl SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + /// Sets the input device to use. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::{SpeakersBuilder, available_outputs}; + /// let input = available_outputs()?.remove(2); + /// let builder = SpeakersBuilder::new().device(input)?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn device( + &self, + device: impl Into, + ) -> Result, Error> { + let device = device.into(); + let supported_configs = device + .supported_output_configs() + .map_err(|source| Error::OutputConfigs { + source, + device_name: device.name().unwrap_or_else(|_| "unknown".to_string()), + })? + .collect(); + Ok(SpeakersBuilder { + device: Some((device, supported_configs)), + config: self.config, + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } + + /// Uses the system's default input device. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new().default_device()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn default_device(&self) -> Result, Error> { + let default_device = cpal::default_host() + .default_output_device() + .ok_or(Error::NoDevice)?; + let supported_configs = default_device + .supported_output_configs() + .map_err(|source| Error::OutputConfigs { + source, + device_name: default_device + .name() + .unwrap_or_else(|_| "unknown".to_string()), + })? + .collect(); + Ok(SpeakersBuilder { + device: Some((default_device, supported_configs)), + config: self.config, + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } +} + +impl SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + /// Uses the device's default input configuration. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn default_config(&self) -> Result, Error> { + let device = &self.device.as_ref().expect("DeviceIsSet").0; + let default_config: OutputConfig = device + .default_output_config() + .map_err(|source| Error::DefaultOutputConfig { + source, + device_name: device.name().unwrap_or_else(|_| "unknown".to_string()), + })? + .into(); + + // Lets try getting f32 output from the default config, as thats + // what rodio uses internally + let config = if self + .check_config(&default_config.with_f32_samples()) + .is_ok() + { + default_config.with_f32_samples() + } else { + default_config + }; + + Ok(SpeakersBuilder { + device: self.device.clone(), + config: Some(config), + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } + + /// Sets a custom input configuration. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::{SpeakersBuilder, OutputConfig}; + /// # use std::num::NonZero; + /// let config = OutputConfig { + /// sample_rate: NonZero::new(44_100).expect("44100 is not zero"), + /// channel_count: NonZero::new(2).expect("2 is not zero"), + /// buffer_size: cpal::BufferSize::Fixed(42_000), + /// sample_format: cpal::SampleFormat::U16, + /// }; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .config(config)?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn config( + &self, + config: OutputConfig, + ) -> Result, Error> { + self.check_config(&config)?; + + Ok(SpeakersBuilder { + device: self.device.clone(), + config: Some(config), + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } + + fn check_config(&self, config: &OutputConfig) -> Result<(), Error> { + let (device, supported_configs) = self.device.as_ref().expect("DeviceIsSet"); + if !supported_configs + .iter() + .any(|range| config.supported_given(range)) + { + Err(Error::UnsupportedByDevice { + device_name: device.name().unwrap_or_else(|_| "unknown".to_string()), + }) + } else { + Ok(()) + } + } +} + +impl SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + /// Sets the sample rate for input. + /// + /// # Error + /// Returns an error if the requested sample rate combined with the + /// other parameters can not be supported. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// .try_sample_rate(44_100.try_into()?)?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_sample_rate( + &self, + sample_rate: SampleRate, + ) -> Result, Error> { + let mut new_config = self.config.expect("ConfigIsSet"); + new_config.sample_rate = sample_rate; + self.check_config(&new_config)?; + + Ok(SpeakersBuilder { + device: self.device.clone(), + config: Some(new_config), + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } + + /// Try multiple sample rates, fall back to the default it non match. The + /// sample rates are in order of preference. If the first can be supported + /// the second will never be tried. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// // 16k or its double with can trivially be resampled to 16k + /// .prefer_sample_rates([ + /// 16_000.try_into().expect("not zero"), + /// 32_000.try_into().expect("not_zero"), + /// ]); + /// # Ok::<(), Box>(()) + /// ``` + pub fn prefer_sample_rates( + &self, + sample_rates: impl IntoIterator, + ) -> SpeakersBuilder { + self.set_preferred_if_supported(sample_rates, |config, sample_rate| { + config.sample_rate = sample_rate + }) + } + + fn set_preferred_if_supported( + &self, + options: impl IntoIterator, + setter: impl Fn(&mut OutputConfig, T), + ) -> SpeakersBuilder { + let mut config = self.config.expect("ConfigIsSet"); + let mut final_config = config; + + for option in options.into_iter() { + setter(&mut config, option); + if self.check_config(&config).is_ok() { + final_config = config; + break; + } + } + + SpeakersBuilder { + device: self.device.clone(), + config: Some(final_config), + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + } + } + + /// Sets the number of input channels. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// .try_channels(2.try_into()?)?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_channels( + &self, + channel_count: ChannelCount, + ) -> Result, Error> { + let mut new_config = self.config.expect("ConfigIsSet"); + new_config.channel_count = channel_count; + self.check_config(&new_config)?; + + Ok(SpeakersBuilder { + device: self.device.clone(), + config: Some(new_config), + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } + + /// Try multiple channel counts, fall back to the default it non match. The + /// channel counts are in order of preference. If the first can be supported + /// the second will never be tried. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// // We want mono, if thats not possible give + /// // us the lowest channel count + /// .prefer_channel_counts([ + /// 1.try_into().expect("not zero"), + /// 2.try_into().expect("not_zero"), + /// 3.try_into().expect("not_zero"), + /// ]); + /// # Ok::<(), Box>(()) + /// ``` + pub fn prefer_channel_counts( + &self, + channel_counts: impl IntoIterator, + ) -> SpeakersBuilder { + self.set_preferred_if_supported(channel_counts, |config, count| { + config.channel_count = count + }) + } + + /// Sets the buffer size for the input. + /// + /// This has no impact on latency, though a too small buffer can lead to audio + /// artifacts if your program can not get samples out of the buffer before they + /// get overridden again. + /// + /// Normally the default input config will have this set up correctly. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// .try_buffer_size(4096)?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_buffer_size( + &self, + buffer_size: u32, + ) -> Result, Error> { + let mut new_config = self.config.expect("ConfigIsSet"); + new_config.buffer_size = cpal::BufferSize::Fixed(buffer_size); + self.check_config(&new_config)?; + + Ok(SpeakersBuilder { + device: self.device.clone(), + config: Some(new_config), + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } + + /// See the docs of [`try_buffer_size`](SpeakersBuilder::try_buffer_size) + /// for more. + /// + /// Try multiple buffer sizes, fall back to the default it non match. The + /// buffer sizes are in order of preference. If the first can be supported + /// the second will never be tried. + /// + /// # Note + /// We will not try buffer sizes larger then 100_000 to prevent this + /// from hanging too long on open ranges. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// // We want mono, if thats not possible give + /// // us the lowest channel count + /// .prefer_buffer_sizes([ + /// 2048.try_into().expect("not zero"), + /// 4096.try_into().expect("not_zero"), + /// ]); + /// # Ok::<(), Box>(()) + /// ``` + /// + /// Get the smallest buffer size larger then 512. + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// // We want mono, if thats not possible give + /// // us the lowest channel count + /// .prefer_buffer_sizes(4096..); + /// # Ok::<(), Box>(()) + /// ``` + pub fn prefer_buffer_sizes( + &self, + buffer_sizes: impl IntoIterator, + ) -> SpeakersBuilder { + let buffer_sizes = buffer_sizes.into_iter().take_while(|size| *size < 100_000); + + self.set_preferred_if_supported(buffer_sizes, |config, size| { + config.buffer_size = cpal::BufferSize::Fixed(size) + }) + } +} + +impl SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + /// Returns the current input configuration. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()?; + /// let config = builder.get_config(); + /// println!("Sample rate: {}", config.sample_rate.get()); + /// println!("Channel count: {}", config.channel_count.get()); + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_config(&self) -> OutputConfig { + self.config.expect("ConfigIsSet") + } +} + +impl SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + /// Opens the speakers input stream. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// # use rodio::{Source, source::SineWave}; + /// # use std::time::Duration; + /// let speakers = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// .open_stream()?; + /// let mixer = speakers.mixer(); + /// mixer.add(SineWave::new(440.).take_duration(Duration::from_secs(4))); + /// std::thread::sleep(Duration::from_secs(4)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn open_stream(&self) -> Result { + speakers::Speakers::open( + self.device.as_ref().expect("DeviceIsSet").0.clone(), + *self.config.as_ref().expect("ConfigIsSet"), + self.error_callback.clone(), + ) + } +} diff --git a/src/speakers/config.rs b/src/speakers/config.rs new file mode 100644 index 00000000..78c947f4 --- /dev/null +++ b/src/speakers/config.rs @@ -0,0 +1,83 @@ +use std::num::NonZero; + +use crate::{math::nz, stream::OutputStreamConfig, ChannelCount, SampleRate}; + +/// Describes the input stream's configuration +#[derive(Copy, Clone, Debug)] +pub struct OutputConfig { + /// The number of channels + pub channel_count: ChannelCount, + /// The sample rate the audio card will be playing back at + pub sample_rate: SampleRate, + /// The buffersize, see a thorough explanation in SpeakerBuilder::with_buffer_size + pub buffer_size: cpal::BufferSize, + /// The sample format used by the audio card. + /// Note we will always convert to this from f32 + pub sample_format: cpal::SampleFormat, +} +impl OutputConfig { + pub(crate) fn supported_given(&self, supported: &cpal::SupportedStreamConfigRange) -> bool { + let buffer_ok = match (self.buffer_size, supported.buffer_size()) { + (cpal::BufferSize::Default, _) | (_, cpal::SupportedBufferSize::Unknown) => true, + ( + cpal::BufferSize::Fixed(n_frames), + cpal::SupportedBufferSize::Range { + min: min_samples, + max: max_samples, + }, + ) => { + let n_samples = n_frames * self.channel_count.get() as u32; + (*min_samples..*max_samples).contains(&n_samples) + } + }; + + buffer_ok + && self.channel_count.get() == supported.channels() + && self.sample_format == supported.sample_format() + && self.sample_rate.get() <= supported.max_sample_rate().0 + && self.sample_rate.get() >= supported.min_sample_rate().0 + } + + pub(crate) fn with_f32_samples(&self) -> Self { + let mut this = *self; + this.sample_format = cpal::SampleFormat::F32; + this + } + + pub(crate) fn into_cpal_config(self) -> crate::stream::OutputStreamConfig { + OutputStreamConfig { + channel_count: self.channel_count, + sample_rate: self.sample_rate, + buffer_size: self.buffer_size, + sample_format: self.sample_format, + } + } +} + +impl From for OutputConfig { + fn from(value: cpal::SupportedStreamConfig) -> Self { + let buffer_size = match value.buffer_size() { + cpal::SupportedBufferSize::Range { .. } => cpal::BufferSize::Default, + cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, + }; + Self { + channel_count: NonZero::new(value.channels()) + .expect("A supported config never has 0 channels"), + sample_rate: NonZero::new(value.sample_rate().0) + .expect("A supported config produces samples"), + buffer_size, + sample_format: value.sample_format(), + } + } +} + +impl Default for OutputConfig { + fn default() -> Self { + Self { + channel_count: nz!(1), + sample_rate: nz!(44_100), + buffer_size: cpal::BufferSize::Default, + sample_format: cpal::SampleFormat::F32, + } + } +} diff --git a/src/stream.rs b/src/stream.rs index 446c69a7..559be6ef 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -89,10 +89,10 @@ impl fmt::Debug for OutputStream { /// Describes the output stream's configuration #[derive(Copy, Clone, Debug)] pub struct OutputStreamConfig { - channel_count: ChannelCount, - sample_rate: SampleRate, - buffer_size: BufferSize, - sample_format: SampleFormat, + pub(crate) channel_count: ChannelCount, + pub(crate) sample_rate: SampleRate, + pub(crate) buffer_size: BufferSize, + pub(crate) sample_format: SampleFormat, } impl Default for OutputStreamConfig { @@ -449,7 +449,7 @@ impl OutputStream { } } - fn open( + pub(crate) fn open( device: &cpal::Device, config: &OutputStreamConfig, error_callback: E,