From 1e4dbbe6ebe78296ee84c6c4ddcf7506966b1e02 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 17:10:10 +0100 Subject: [PATCH 1/7] Adds a builder for configuring outputstreams Its basically the same as the microphone builder. It will replace the OutputStream. In the future we'll add more abstractions on top. Until its done it lives under an experimental flag. Names are subject to change too, Speakers is probably not ideal but it conveys the meaning better then OutputStream. I'm thinking of having a Source -> Stream -> Sink terminolgy where a Sink could be the audio card, the network or a file (the wavwriter). --- src/lib.rs | 2 + src/microphone/builder.rs | 4 +- src/speakers.rs | 169 ++++++++++++ src/speakers/builder.rs | 548 ++++++++++++++++++++++++++++++++++++++ src/speakers/config.rs | 83 ++++++ src/stream.rs | 10 +- 6 files changed, 809 insertions(+), 7 deletions(-) create mode 100644 src/speakers.rs create mode 100644 src/speakers/builder.rs create mode 100644 src/speakers/config.rs diff --git a/src/lib.rs b/src/lib.rs index b4fcfaa0..b0720831 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..d3d6cd4c 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; diff --git a/src/speakers.rs b/src/speakers.rs new file mode 100644 index 00000000..81a5a68a --- /dev/null +++ b/src/speakers.rs @@ -0,0 +1,169 @@ +//! 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}; + +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, +} + +impl From for cpal::Device { + fn from(val: Output) -> Self { + val.inner + } +} + +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 devices = host + .output_devices() + .map_err(ListError)? + .map(|dev| Output { 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..d8b57f3d --- /dev/null +++ b/src/speakers/builder.rs @@ -0,0 +1,548 @@ +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(()) + } + } + + /// 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.as_ref().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..af813587 --- /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, From 409dd4adea1189bdd8b16d479e892875e69f6d69 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 17:16:29 +0100 Subject: [PATCH 2/7] clippy --- src/speakers/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/speakers/config.rs b/src/speakers/config.rs index af813587..78c947f4 100644 --- a/src/speakers/config.rs +++ b/src/speakers/config.rs @@ -44,7 +44,7 @@ impl OutputConfig { this } - pub(crate) fn into_cpal_config(&self) -> crate::stream::OutputStreamConfig { + pub(crate) fn into_cpal_config(self) -> crate::stream::OutputStreamConfig { OutputStreamConfig { channel_count: self.channel_count, sample_rate: self.sample_rate, From 290ce7d9f85e8f52f37d90d4f9a218bf6e0d2283 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 23:19:30 +0100 Subject: [PATCH 3/7] make speakers::builder accesible --- src/speakers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/speakers.rs b/src/speakers.rs index 81a5a68a..5f1d8c20 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -108,7 +108,7 @@ use cpal::{ use crate::{common::assert_error_traits, StreamError}; -mod builder; +pub mod builder; mod config; pub use builder::SpeakersBuilder; From 31c30e524803034a45d7dd00a66f894ae6f3da02 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 23:22:30 +0100 Subject: [PATCH 4/7] return owned config --- src/microphone/builder.rs | 4 ++-- src/speakers/builder.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index d3d6cd4c..46482b9b 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -512,8 +512,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.copied().expect("ConfigIsSet") } } diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index d8b57f3d..88483ee3 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -512,8 +512,8 @@ where /// println!("Channel count: {}", config.channel_count.get()); /// # Ok::<(), Box>(()) /// ``` - pub fn get_config(&self) -> &OutputConfig { - self.config.as_ref().expect("ConfigIsSet") + pub fn get_config(&self) -> OutputConfig { + self.config.copied().expect("ConfigIsSet") } } From 739977bda83b608ed6a78a7ef4fc351dcaada03e Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 23:34:11 +0100 Subject: [PATCH 5/7] add default to Output --- src/lib.rs | 2 +- src/microphone/builder.rs | 2 +- src/speakers.rs | 18 ++++++++++++++---- src/speakers/builder.rs | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b0720831..66ebfd3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,7 +171,7 @@ pub use cpal::{ mod common; mod sink; mod spatial_sink; -#[cfg(all(feature = "playback", feature = "experimental"))] +// #[cfg(all(feature = "playback", feature = "experimental"))] pub mod speakers; #[cfg(feature = "playback")] pub mod stream; diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index 46482b9b..c91644df 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -513,7 +513,7 @@ where /// # Ok::<(), Box>(()) /// ``` pub fn get_config(&self) -> InputConfig { - self.config.copied().expect("ConfigIsSet") + self.config.expect("ConfigIsSet") } } diff --git a/src/speakers.rs b/src/speakers.rs index 5f1d8c20..af489753 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -108,6 +108,7 @@ use cpal::{ use crate::{common::assert_error_traits, StreamError}; +/// TODO pub mod builder; mod config; @@ -126,6 +127,7 @@ assert_error_traits! {ListError} #[derive(Clone)] pub struct Output { inner: cpal::Device, + default: bool, } impl From for cpal::Device { @@ -134,6 +136,13 @@ impl From for cpal::Device { } } +impl Output { + /// TODO + 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") @@ -151,10 +160,11 @@ impl fmt::Display for Output { /// Returns a list of available output devices on the system. pub fn available_outputs() -> Result, ListError> { let host = cpal::default_host(); - let devices = host - .output_devices() - .map_err(ListError)? - .map(|dev| Output { inner: dev }); + 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()) } diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 88483ee3..71c73575 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -513,7 +513,7 @@ where /// # Ok::<(), Box>(()) /// ``` pub fn get_config(&self) -> OutputConfig { - self.config.copied().expect("ConfigIsSet") + self.config.expect("ConfigIsSet") } } From 3e1063015f0c5331e2b4ce94c6fea946435ad413 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 16 Dec 2025 17:30:58 +0100 Subject: [PATCH 6/7] return len when appending to queue --- src/queue.rs | 9 +++++---- src/speakers.rs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) 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 index af489753..971ae676 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -137,7 +137,7 @@ impl From for cpal::Device { } impl Output { - /// TODO + /// TODO doc comment also mirror to microphone api pub fn is_default(&self) -> bool { self.default } From f81266bda2e3270e1fe0c7b344acf4017951c566 Mon Sep 17 00:00:00 2001 From: Yara Date: Thu, 18 Dec 2025 01:21:16 +0100 Subject: [PATCH 7/7] fix typed builder not being constraint enough --- src/microphone/builder.rs | 5 +++++ src/speakers/builder.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index c91644df..bbbdfaa7 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -272,7 +272,12 @@ where Ok(()) } } +} +impl MicrophoneBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ /// Sets the sample rate for input. /// /// # Error diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 71c73575..40bd6bc0 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -272,7 +272,12 @@ where Ok(()) } } +} +impl SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ /// Sets the sample rate for input. /// /// # Error