From ed4dc129dec632a2e091db9a9f8b457e24accc46 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 17:10:10 +0100 Subject: [PATCH 01/14] 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 1b71e929..cf6011bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,6 +179,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 cc10b383..6a1db2ca 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 e7bac2f6..eaa81176 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 { @@ -454,7 +454,7 @@ impl OutputStream { } } - fn open( + pub(crate) fn open( device: &cpal::Device, config: &OutputStreamConfig, error_callback: E, From 84e416004809042ae89a1aa582c17355f46fde8d Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 17:16:29 +0100 Subject: [PATCH 02/14] 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 3dfe4f1a7b6a5e0efeb5959e34bae9ff80987b4a Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 23:19:30 +0100 Subject: [PATCH 03/14] 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 0f177006576ed1fd8e2ee1cd470531a96bede05d Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 23:22:30 +0100 Subject: [PATCH 04/14] 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 6a1db2ca..7b23fc6e 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -523,8 +523,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 ac964d147cee5558031d27d01075ccc8e9c219c4 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 23:34:11 +0100 Subject: [PATCH 05/14] 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 cf6011bf..d7e7e190 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,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 7b23fc6e..51142f06 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -524,7 +524,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 02db9931ba086a37f4d1b09aab32eaf0d56b4405 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 16 Dec 2025 17:30:58 +0100 Subject: [PATCH 06/14] 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 3bf1695e..697a1138 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -57,14 +57,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 642df0db8fb4815d3bda741bcc170e81f7907064 Mon Sep 17 00:00:00 2001 From: Yara Date: Thu, 18 Dec 2025 01:21:16 +0100 Subject: [PATCH 07/14] 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 51142f06..ac98ab44 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -283,7 +283,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 From dcf23549e572a984f9c832e673ca0448515df599 Mon Sep 17 00:00:00 2001 From: dvdsk Date: Fri, 2 Jan 2026 23:40:42 +0100 Subject: [PATCH 08/14] apply review feedback --- src/queue.rs | 4 ++-- src/speakers.rs | 14 +++++++------- src/speakers/builder.rs | 30 +++++++++++++++--------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/queue.rs b/src/queue.rs index 697a1138..a594b1d3 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -57,7 +57,7 @@ pub struct SourcesQueueInput { impl SourcesQueueInput { /// Adds a new source to the end of the queue. #[inline] - pub fn append(&self, source: T) -> usize + pub fn append(&self, source: T) where T: Source + Send + 'static, { @@ -65,7 +65,7 @@ impl SourcesQueueInput { .lock() .unwrap(); next_sounds.push((Box::new(source) as Box<_>, None)); - next_sounds.len() + next_sounds.len(); } /// Adds a new source to the end of the queue. diff --git a/src/speakers.rs b/src/speakers.rs index 971ae676..b3c1204a 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -84,15 +84,15 @@ //! 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); +//! // List all available output devices +//! let outputs = available_outputs()?; +//! for (i, output) in outputs.iter().enumerate() { +//! println!("output {}: {}", i, output); //! } //! //! // Use a specific device (e.g., the second one) //! let mic = SpeakersBuilder::new() -//! .device(inputs[1].clone())? +//! .device(outputs[1].clone())? //! .default_config()? //! .open_stream()?; //! # Ok(()) @@ -119,11 +119,11 @@ 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")] +#[error("Could not list output devices")] pub struct ListError(#[source] cpal::DevicesError); assert_error_traits! {ListError} -/// An input device +/// An output device #[derive(Clone)] pub struct Output { inner: cpal::Device, diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 40bd6bc0..d3c55931 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -11,12 +11,12 @@ use crate::{ ChannelCount, OutputStream, SampleRate, }; -/// Error configuring or opening speakers input +/// Error configuring or opening speakers output #[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")] + #[error("There is no output device")] NoDevice, /// Failed to get the default output configuration for the device. #[error("Could not get default output configuration for output device: '{device_name}'")] @@ -54,7 +54,7 @@ pub struct ConfigNotSet; /// Some methods are only available when this types counterpart: `DeviceIsSet` is present. pub struct DeviceNotSet; -/// Builder for configuring and opening speakers input streams. +/// Builder for configuring and opening speakers output streams. #[must_use] pub struct SpeakersBuilder where @@ -122,13 +122,13 @@ impl SpeakersBuilder where E: FnMut(cpal::StreamError) + Send + Clone + 'static, { - /// Sets the input device to use. + /// Sets the output device to use. /// /// # Example /// ```no_run /// # use rodio::speakers::{SpeakersBuilder, available_outputs}; - /// let input = available_outputs()?.remove(2); - /// let builder = SpeakersBuilder::new().device(input)?; + /// let output = available_outputs()?.remove(2); + /// let builder = SpeakersBuilder::new().device(output)?; /// # Ok::<(), Box>(()) /// ``` pub fn device( @@ -152,7 +152,7 @@ where }) } - /// Uses the system's default input device. + /// Uses the system's default output device. /// /// # Example /// ```no_run @@ -187,7 +187,7 @@ impl SpeakersBuilder where E: FnMut(cpal::StreamError) + Send + Clone + 'static, { - /// Uses the device's default input configuration. + /// Uses the device's default output configuration. /// /// # Example /// ```no_run @@ -227,7 +227,7 @@ where }) } - /// Sets a custom input configuration. + /// Sets a custom output configuration. /// /// # Example /// ```no_run @@ -278,7 +278,7 @@ impl SpeakersBuilder where E: FnMut(cpal::StreamError) + Send + Clone + 'static, { - /// Sets the sample rate for input. + /// Sets the sample rate for output. /// /// # Error /// Returns an error if the requested sample rate combined with the @@ -361,7 +361,7 @@ where } } - /// Sets the number of input channels. + /// Sets the number of output channels. /// /// # Example /// ```no_run @@ -417,13 +417,13 @@ where }) } - /// Sets the buffer size for the input. + /// Sets the buffer size for the output. /// /// 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. + /// Normally the default output config will have this set up correctly. /// /// # Example /// ```no_run @@ -504,7 +504,7 @@ impl SpeakersBuilder where E: FnMut(cpal::StreamError) + Send + Clone + 'static, { - /// Returns the current input configuration. + /// Returns the current output configuration. /// /// # Example /// ```no_run @@ -526,7 +526,7 @@ impl SpeakersBuilder where E: FnMut(cpal::StreamError) + Send + Clone + 'static, { - /// Opens the speakers input stream. + /// Opens the speakers output stream. /// /// # Example /// ```no_run From 6497d0385c461f7c5c419820b014ba22229ddd11 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 00:35:13 +0100 Subject: [PATCH 09/14] make compatible with cpal 0.17 --- src/microphone.rs | 2 +- src/microphone/builder.rs | 6 +++--- src/speakers.rs | 35 +++++++++++++++++++++++------------ src/speakers/builder.rs | 33 ++++++++++++++++++++++----------- src/speakers/config.rs | 6 +++--- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/microphone.rs b/src/microphone.rs index e5641cfe..eabfa807 100644 --- a/src/microphone.rs +++ b/src/microphone.rs @@ -91,7 +91,7 @@ //! //! // Use a specific device (e.g., the second one) //! let mic = MicrophoneBuilder::new() -//! .device(inputs[1].clone().into_inner())? +//! .device(inputs[1].clone())? //! .default_config()? //! .open_stream()?; //! # Ok(()) diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index ac98ab44..e483945a 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -129,14 +129,14 @@ where /// ```no_run /// # use rodio::microphone::{MicrophoneBuilder, available_inputs}; /// let input = available_inputs()?.remove(2); - /// let builder = MicrophoneBuilder::new().device(input.into_inner())?; + /// let builder = MicrophoneBuilder::new().device(input)?; /// # Ok::<(), Box>(()) /// ``` pub fn device( &self, - device: impl Into, + device: super::Input, ) -> Result, Error> { - let device = device.into(); + let device = device.into_inner(); let supported_configs = device .supported_input_configs() .map_err(|source| Error::InputConfigs { diff --git a/src/speakers.rs b/src/speakers.rs index b3c1204a..2ef21260 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -108,8 +108,7 @@ use cpal::{ use crate::{common::assert_error_traits, StreamError}; -/// TODO -pub mod builder; +mod builder; mod config; pub use builder::SpeakersBuilder; @@ -130,39 +129,51 @@ pub struct Output { 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 } + + pub(crate) fn into_inner(self) -> cpal::Device { + self.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())) + .field( + "inner", + &self + .inner + .description() + .map(|d| d.name().to_string()) + .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())) + write!( + f, + "{}", + self.inner + .description() + .map(|d| d.name().to_string()) + .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 default = host.default_output_device().map(|d| d.id()); let devices = host.output_devices().map_err(ListError)?.map(|dev| Output { - default: Some(dev.name()) == default, + default: Some(dev.id()) == default, inner: dev, }); Ok(devices.collect()) diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index d3c55931..a52b0daa 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -76,10 +76,11 @@ where f.debug_struct("SpeakersBuilder") .field( "device", - &self - .device - .as_ref() - .map(|d| d.0.name().unwrap_or("unknown".to_string())), + &self.device.as_ref().map(|d| { + d.0.description() + .map(|d| d.name().to_string()) + .unwrap_or("unknown".to_string()) + }), ) .field("config", &self.config) .finish() @@ -133,14 +134,17 @@ where /// ``` pub fn device( &self, - device: impl Into, + device: super::Output, ) -> Result, Error> { - let device = device.into(); + let device = device.into_inner(); let supported_configs = device .supported_output_configs() .map_err(|source| Error::OutputConfigs { source, - device_name: device.name().unwrap_or_else(|_| "unknown".to_string()), + device_name: device + .description() + .map(|d| d.name().to_string()) + .unwrap_or("unknown".to_string()), })? .collect(); Ok(SpeakersBuilder { @@ -169,8 +173,9 @@ where .map_err(|source| Error::OutputConfigs { source, device_name: default_device - .name() - .unwrap_or_else(|_| "unknown".to_string()), + .description() + .map(|d| d.name().to_string()) + .unwrap_or("unknown".to_string()), })? .collect(); Ok(SpeakersBuilder { @@ -203,7 +208,10 @@ where .default_output_config() .map_err(|source| Error::DefaultOutputConfig { source, - device_name: device.name().unwrap_or_else(|_| "unknown".to_string()), + device_name: device + .description() + .map(|d| d.name().to_string()) + .unwrap_or("unknown".to_string()), })? .into(); @@ -266,7 +274,10 @@ where .any(|range| config.supported_given(range)) { Err(Error::UnsupportedByDevice { - device_name: device.name().unwrap_or_else(|_| "unknown".to_string()), + device_name: device + .description() + .map(|d| d.name().to_string()) + .unwrap_or("unknown".to_string()), }) } else { Ok(()) diff --git a/src/speakers/config.rs b/src/speakers/config.rs index 78c947f4..4debc6de 100644 --- a/src/speakers/config.rs +++ b/src/speakers/config.rs @@ -34,8 +34,8 @@ impl OutputConfig { 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 + && self.sample_rate.get() <= supported.max_sample_rate() + && self.sample_rate.get() >= supported.min_sample_rate() } pub(crate) fn with_f32_samples(&self) -> Self { @@ -63,7 +63,7 @@ impl From for OutputConfig { Self { channel_count: NonZero::new(value.channels()) .expect("A supported config never has 0 channels"), - sample_rate: NonZero::new(value.sample_rate().0) + sample_rate: NonZero::new(value.sample_rate()) .expect("A supported config produces samples"), buffer_size, sample_format: value.sample_format(), From 2da8f49a87759e27359756f5b0842065b68a42f0 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 00:47:24 +0100 Subject: [PATCH 10/14] fixes example buffersize comment --- src/microphone/builder.rs | 7 +++---- src/speakers/builder.rs | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index e483945a..551fafab 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -479,8 +479,7 @@ where /// let builder = MicrophoneBuilder::new() /// .default_device()? /// .default_config()? - /// // We want mono, if thats not possible give - /// // us the lowest channel count + /// // Multiples of two work well for us /// .prefer_buffer_sizes([ /// 2048.try_into().expect("not zero"), /// 4096.try_into().expect("not_zero"), @@ -494,8 +493,8 @@ where /// let builder = MicrophoneBuilder::new() /// .default_device()? /// .default_config()? - /// // We want mono, if thats not possible give - /// // us the lowest channel count + /// // We need a minimum buffer of 4096 + /// // or we get glitches. /// .prefer_buffer_sizes(4096..); /// # Ok::<(), Box>(()) /// ``` diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index a52b0daa..0c548549 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -479,8 +479,6 @@ where /// 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"), @@ -494,8 +492,8 @@ where /// let builder = SpeakersBuilder::new() /// .default_device()? /// .default_config()? - /// // We want mono, if thats not possible give - /// // us the lowest channel count + /// // We need a minimum buffer of 4096 + /// // or we get glitches. /// .prefer_buffer_sizes(4096..); /// # Ok::<(), Box>(()) /// ``` From 8e695feaecf79d30f85b7a271501e8a3005e8383 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 01:15:58 +0100 Subject: [PATCH 11/14] fix example --- examples/microphone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/microphone.rs b/examples/microphone.rs index 97419327..c3e55918 100644 --- a/examples/microphone.rs +++ b/examples/microphone.rs @@ -13,7 +13,7 @@ fn main() -> Result<(), Box> { .prompt()?; let input = MicrophoneBuilder::new() - .device(input.into_inner())? + .device(input)? .default_config()? .open_stream()?; From 7d7d4acfa91d5b8328da01ff310d4d7b738c0694 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 01:16:11 +0100 Subject: [PATCH 12/14] cargo fmt --- src/microphone/builder.rs | 4 ++-- src/queue.rs | 4 +--- src/speakers/builder.rs | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index 551fafab..9925d035 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -493,8 +493,8 @@ where /// let builder = MicrophoneBuilder::new() /// .default_device()? /// .default_config()? - /// // We need a minimum buffer of 4096 - /// // or we get glitches. + /// // We need a minimum buffer of 4096 + /// // or we get glitches. /// .prefer_buffer_sizes(4096..); /// # Ok::<(), Box>(()) /// ``` diff --git a/src/queue.rs b/src/queue.rs index a594b1d3..e7234b6a 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -61,9 +61,7 @@ impl SourcesQueueInput { where T: Source + Send + 'static, { - let mut next_sounds = self.next_sounds - .lock() - .unwrap(); + let mut next_sounds = self.next_sounds.lock().unwrap(); next_sounds.push((Box::new(source) as Box<_>, None)); next_sounds.len(); } diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 0c548549..fe7656af 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -492,8 +492,8 @@ where /// let builder = SpeakersBuilder::new() /// .default_device()? /// .default_config()? - /// // We need a minimum buffer of 4096 - /// // or we get glitches. + /// // We need a minimum buffer of 4096 + /// // or we get glitches. /// .prefer_buffer_sizes(4096..); /// # Ok::<(), Box>(()) /// ``` From 45d31b10498dac07793b837fe459e281ffb64ec5 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 01:45:07 +0100 Subject: [PATCH 13/14] re-enable experimental feature --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d7e7e190..cf6011bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,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; From c95f8c0c377dc3e6272beebe10ded1f1a10ffb68 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 23:00:48 +0100 Subject: [PATCH 14/14] final feedback --- src/microphone/builder.rs | 3 --- src/queue.rs | 7 ++++--- src/speakers.rs | 6 ++---- src/speakers/builder.rs | 17 +++++------------ src/speakers/config.rs | 4 ++-- 5 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index 9925d035..dd2c723a 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -479,7 +479,6 @@ where /// let builder = MicrophoneBuilder::new() /// .default_device()? /// .default_config()? - /// // Multiples of two work well for us /// .prefer_buffer_sizes([ /// 2048.try_into().expect("not zero"), /// 4096.try_into().expect("not_zero"), @@ -493,8 +492,6 @@ where /// let builder = MicrophoneBuilder::new() /// .default_device()? /// .default_config()? - /// // We need a minimum buffer of 4096 - /// // or we get glitches. /// .prefer_buffer_sizes(4096..); /// # Ok::<(), Box>(()) /// ``` diff --git a/src/queue.rs b/src/queue.rs index e7234b6a..3bf1695e 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -61,9 +61,10 @@ impl SourcesQueueInput { where T: Source + Send + 'static, { - let mut next_sounds = self.next_sounds.lock().unwrap(); - next_sounds.push((Box::new(source) as Box<_>, None)); - next_sounds.len(); + self.next_sounds + .lock() + .unwrap() + .push((Box::new(source) as Box<_>, None)); } /// Adds a new source to the end of the queue. diff --git a/src/speakers.rs b/src/speakers.rs index 2ef21260..6feb70e5 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -148,8 +148,7 @@ impl fmt::Debug for Output { &self .inner .description() - .map(|d| d.name().to_string()) - .unwrap_or("unknown".to_string()), + .map_or("unknown".to_string(), |d| d.name().to_string()), ) .finish() } @@ -162,8 +161,7 @@ impl fmt::Display for Output { "{}", self.inner .description() - .map(|d| d.name().to_string()) - .unwrap_or("unknown".to_string()), + .map_or("unknown".to_string(), |d| d.name().to_string()) ) } } diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index fe7656af..40928e48 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -78,8 +78,7 @@ where "device", &self.device.as_ref().map(|d| { d.0.description() - .map(|d| d.name().to_string()) - .unwrap_or("unknown".to_string()) + .map_or("unknown".to_string(), |d| d.name().to_string()) }), ) .field("config", &self.config) @@ -143,8 +142,7 @@ where source, device_name: device .description() - .map(|d| d.name().to_string()) - .unwrap_or("unknown".to_string()), + .map_or("unknown".to_string(), |d| d.name().to_string()), })? .collect(); Ok(SpeakersBuilder { @@ -174,8 +172,7 @@ where source, device_name: default_device .description() - .map(|d| d.name().to_string()) - .unwrap_or("unknown".to_string()), + .map_or("unknown".to_string(), |d| d.name().to_string()), })? .collect(); Ok(SpeakersBuilder { @@ -210,8 +207,7 @@ where source, device_name: device .description() - .map(|d| d.name().to_string()) - .unwrap_or("unknown".to_string()), + .map_or("unknown".to_string(), |d| d.name().to_string()), })? .into(); @@ -276,8 +272,7 @@ where Err(Error::UnsupportedByDevice { device_name: device .description() - .map(|d| d.name().to_string()) - .unwrap_or("unknown".to_string()), + .map_or("unknown".to_string(), |d| d.name().to_string()), }) } else { Ok(()) @@ -492,8 +487,6 @@ where /// let builder = SpeakersBuilder::new() /// .default_device()? /// .default_config()? - /// // We need a minimum buffer of 4096 - /// // or we get glitches. /// .prefer_buffer_sizes(4096..); /// # Ok::<(), Box>(()) /// ``` diff --git a/src/speakers/config.rs b/src/speakers/config.rs index 4debc6de..0b96476b 100644 --- a/src/speakers/config.rs +++ b/src/speakers/config.rs @@ -2,7 +2,7 @@ use std::num::NonZero; use crate::{math::nz, stream::OutputStreamConfig, ChannelCount, SampleRate}; -/// Describes the input stream's configuration +/// Describes the output stream's configuration #[derive(Copy, Clone, Debug)] pub struct OutputConfig { /// The number of channels @@ -27,7 +27,7 @@ impl OutputConfig { }, ) => { let n_samples = n_frames * self.channel_count.get() as u32; - (*min_samples..*max_samples).contains(&n_samples) + (*min_samples..=*max_samples).contains(&n_samples) } };