From ed4dc129dec632a2e091db9a9f8b457e24accc46 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 14 Dec 2025 17:10:10 +0100 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 2f3014ae892d83f19c9afca217c42a8b8eabae19 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 16:09:17 +0100 Subject: [PATCH 14/16] rename OutputStream -> OS-Sink, Sink -> Player --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- UPGRADE.md | 4 +- examples/automatic_gain_control.rs | 8 +- examples/basic.rs | 14 +- examples/callback_on_end.rs | 10 +- examples/custom_config.rs | 6 +- examples/distortion.rs | 2 +- examples/distortion_mp3.rs | 8 +- examples/distortion_wav.rs | 8 +- examples/distortion_wav_alternate.rs | 8 +- examples/error_callback.rs | 4 +- examples/limit_wav.rs | 8 +- examples/low_pass.rs | 8 +- examples/microphone.rs | 2 +- examples/mix_multiple_sources.rs | 10 +- examples/music_flac.rs | 8 +- examples/music_m4a.rs | 8 +- examples/music_mp3.rs | 8 +- examples/music_ogg.rs | 8 +- examples/music_wav.rs | 8 +- examples/noise_generator.rs | 6 +- examples/reverb.rs | 8 +- examples/seek_mp3.rs | 14 +- examples/signal_generator.rs | 2 +- examples/spatial.rs | 14 +- examples/stereo.rs | 8 +- src/common.rs | 2 +- src/lib.rs | 80 +++++---- src/mixer.rs | 6 +- src/{sink.rs => player.rs} | 72 ++++---- src/source/dither.rs | 2 +- src/source/mod.rs | 8 +- src/source/skippable.rs | 4 +- src/source/spatial.rs | 2 +- src/source/speed.rs | 24 +-- src/{spatial_sink.rs => spatial_player.rs} | 56 +++--- src/speakers.rs | 18 +- src/speakers/builder.rs | 85 ++++++++- src/speakers/config.rs | 6 +- src/stream.rs | 198 ++++++++++----------- src/wav_output.rs | 4 +- 42 files changed, 419 insertions(+), 344 deletions(-) rename src/{sink.rs => player.rs} (87%) rename src/{spatial_sink.rs => spatial_player.rs} (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb03c86..381a89ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -165,7 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 implementation. ### Fixed -- `Sink.try_seek` now updates `controls.position` before returning. Calls to `Sink.get_pos` +- `player.try_seek` now updates `controls.position` before returning. Calls to `player.get_pos` done immediately after a seek will now return the correct value. ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fb03fd0..34f2062b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ src/: - Follow [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) - Use `rustfmt` for formatting - Implement `Source` trait for new audio sources -- Use `Sink` for playback management +- Use `Player` for playback management ## Common Tasks diff --git a/UPGRADE.md b/UPGRADE.md index 884d2f9b..e7c91532 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -56,12 +56,12 @@ No changes are required. The following Rodio *0.20* code: ```rust let (_stream, handle) = rodio::OutputStream::try_default()?; -let sink = rodio::Sink::try_new(&handle)?; +let player = rodio::Player::try_new(&handle)?; ``` Should be written like this in Rodio *0.21*: ```rust let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; -let sink = rodio::Sink::connect_new(stream_handle.mixer()); +let player = rodio::Player::connect_new(stream_handle.mixer()); ``` The `SpatialSink` changes mirror those in `Sink` described above. diff --git a/examples/automatic_gain_control.rs b/examples/automatic_gain_control.rs index 625f982c..07f01c62 100644 --- a/examples/automatic_gain_control.rs +++ b/examples/automatic_gain_control.rs @@ -8,8 +8,8 @@ use std::thread; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); // Decode the sound file into a source let file = File::open("assets/music.flac")?; @@ -29,7 +29,7 @@ fn main() -> Result<(), Box> { // Add the source now equipped with automatic gain control and controlled via // periodic_access to the sink for the playback. - sink.append(controlled); + player.append(controlled); // After 5 seconds of playback disable automatic gain control using the // shared AtomicBool `agc_enabled`. You could do this from another part @@ -42,6 +42,6 @@ fn main() -> Result<(), Box> { agc_enabled.store(false, Ordering::Relaxed); // Keep the program running until the playback is complete. - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/basic.rs b/examples/basic.rs index 5628bd3b..4c45190e 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -6,15 +6,15 @@ use std::thread; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; let mixer = stream_handle.mixer(); let beep1 = { // Play a WAV file. let file = std::fs::File::open("assets/beep.wav")?; - let sink = rodio::play(mixer, BufReader::new(file))?; - sink.set_volume(0.2); - sink + let player = rodio::play(mixer, BufReader::new(file))?; + player.set_volume(0.2); + player }; println!("Started beep1"); thread::sleep(Duration::from_millis(1500)); @@ -32,9 +32,9 @@ fn main() -> Result<(), Box> { let beep3 = { // Play an OGG file. let file = std::fs::File::open("assets/beep3.ogg")?; - let sink = rodio::play(mixer, BufReader::new(file))?; - sink.set_volume(0.2); - sink + let player = rodio::play(mixer, BufReader::new(file))?; + player.set_volume(0.2); + player }; println!("Started beep3"); thread::sleep(Duration::from_millis(1500)); diff --git a/examples/callback_on_end.rs b/examples/callback_on_end.rs index f367f7f4..4df0dc81 100644 --- a/examples/callback_on_end.rs +++ b/examples/callback_on_end.rs @@ -3,11 +3,11 @@ use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); // lets increment a number after `music.wav` has played. We are going to use atomics // however you could also use a `Mutex` or send a message through a `std::sync::mpsc`. @@ -17,7 +17,7 @@ fn main() -> Result<(), Box> { // playlist_pos into the closure. That way we can still access playlist_pos // after appending the EmptyCallback. let playlist_pos_clone = playlist_pos.clone(); - sink.append(rodio::source::EmptyCallback::new(Box::new(move || { + player.append(rodio::source::EmptyCallback::new(Box::new(move || { println!("empty callback is now running"); playlist_pos_clone.fetch_add(1, Ordering::Relaxed); }))); @@ -27,7 +27,7 @@ fn main() -> Result<(), Box> { "playlist position is: {}", playlist_pos.load(Ordering::Relaxed) ); - sink.sleep_until_end(); + player.sleep_until_end(); assert_eq!(playlist_pos.load(Ordering::Relaxed), 1); println!( "playlist position is: {}", diff --git a/examples/custom_config.rs b/examples/custom_config.rs index aa83a7ee..0afd87ee 100644 --- a/examples/custom_config.rs +++ b/examples/custom_config.rs @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { let default_device = cpal::default_host() .default_output_device() .ok_or("No default audio output device is found.")?; - let stream_handle = rodio::OutputStreamBuilder::from_device(default_device)? + let stream_handle = rodio::OsSinkBuilder::from_device(default_device)? // No need to set all parameters explicitly here, // the defaults were set from the device's description. .with_buffer_size(BufferSize::Fixed(256)) @@ -20,8 +20,8 @@ fn main() -> Result<(), Box> { .with_sample_format(SampleFormat::F32) // Note that the function below still tries alternative configs if the specified one fails. // If you need to only use the exact specified configuration, - // then use OutputStreamBuilder::open_stream() instead. - .open_stream_or_fallback()?; + // then use OsSinkBuilder::open_sink() instead. + .open_sink_or_fallback()?; let mixer = stream_handle.mixer(); let wave = SineWave::new(740.0) diff --git a/examples/distortion.rs b/examples/distortion.rs index eae17bf7..16962397 100644 --- a/examples/distortion.rs +++ b/examples/distortion.rs @@ -5,7 +5,7 @@ use std::time::Duration; fn main() -> Result<(), Box> { // Open the default output stream and get the mixer - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; let mixer = stream_handle.mixer(); // Create a sine wave source and apply distortion diff --git a/examples/distortion_mp3.rs b/examples/distortion_mp3.rs index 895c7647..52f7f056 100644 --- a/examples/distortion_mp3.rs +++ b/examples/distortion_mp3.rs @@ -3,15 +3,15 @@ use std::error::Error; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; // Apply distortion effect before appending to the sink let source = rodio::Decoder::try_from(file)?.distortion(4.0, 0.3); - sink.append(source); + player.append(source); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/distortion_wav.rs b/examples/distortion_wav.rs index 0955fd51..825433cb 100644 --- a/examples/distortion_wav.rs +++ b/examples/distortion_wav.rs @@ -3,15 +3,15 @@ use std::error::Error; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; // Apply distortion effect before appending to the sink let source = rodio::Decoder::try_from(file)?.distortion(4.0, 0.3); - sink.append(source); + player.append(source); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/distortion_wav_alternate.rs b/examples/distortion_wav_alternate.rs index 8d3f36b4..cead8298 100644 --- a/examples/distortion_wav_alternate.rs +++ b/examples/distortion_wav_alternate.rs @@ -9,8 +9,8 @@ use std::time::Duration; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; let source = rodio::Decoder::try_from(file)?; @@ -31,7 +31,7 @@ fn main() -> Result<(), Box> { src.set_threshold(if enable { 0.3 } else { 1.0 }); }); - sink.append(distorted); + player.append(distorted); println!("Playing music.wav with alternating distortion effect..."); // Alternate the distortion effect every second for 10 seconds @@ -43,7 +43,7 @@ fn main() -> Result<(), Box> { } // Wait for playback to finish - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/error_callback.rs b/examples/error_callback.rs index ab335583..096348a6 100644 --- a/examples/error_callback.rs +++ b/examples/error_callback.rs @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { let (tx, rx) = std::sync::mpsc::channel(); - let stream_handle = rodio::OutputStreamBuilder::from_device(default_device)? + let stream_handle = rodio::OsSinkBuilder::from_device(default_device)? .with_error_callback(move |err| { // Filter for where err is an actionable error. if matches!( @@ -24,7 +24,7 @@ fn main() -> Result<(), Box> { } } }) - .open_stream_or_fallback()?; + .open_sink_or_fallback()?; let mixer = stream_handle.mixer(); diff --git a/examples/limit_wav.rs b/examples/limit_wav.rs index b104d3ff..ddb9e8c0 100644 --- a/examples/limit_wav.rs +++ b/examples/limit_wav.rs @@ -2,18 +2,18 @@ use rodio::{source::LimitSettings, Source}; use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; let source = rodio::Decoder::try_from(file)? .amplify(3.0) .limit(LimitSettings::default()); - sink.append(source); + player.append(source); println!("Playing music.wav with limiting until finished..."); - sink.sleep_until_end(); + player.sleep_until_end(); println!("Done."); Ok(()) diff --git a/examples/low_pass.rs b/examples/low_pass.rs index ec343888..ab8e575d 100644 --- a/examples/low_pass.rs +++ b/examples/low_pass.rs @@ -4,15 +4,15 @@ use std::io::BufReader; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; let decoder = rodio::Decoder::new(BufReader::new(file))?; let source = decoder.low_pass(200); - sink.append(source); + player.append(source); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/microphone.rs b/examples/microphone.rs index c3e55918..107e9f8d 100644 --- a/examples/microphone.rs +++ b/examples/microphone.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Box> { let recording = input.take_duration(Duration::from_secs(5)).record(); println!("Playing the recording"); - let mut output = rodio::OutputStreamBuilder::open_default_stream()?; + let mut output = rodio::OsSinkBuilder::open_default_sink()?; output.mixer().add(recording); thread::sleep(Duration::from_secs(5)); diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index 4d4360d6..7cb8df39 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -9,10 +9,10 @@ const NOTE_DURATION: Duration = Duration::from_secs(1); const NOTE_AMPLITUDE: Float = 0.20; fn main() -> Result<(), Box> { - // Construct a dynamic controller and mixer, stream_handle, and sink. + // Construct a dynamic controller and mixer, stream_handle, and player. let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), NonZero::new(44_100).unwrap()); - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); // Create four unique sources. The frequencies used here correspond // notes in the key of C and in octave 4: C4, or middle C on a piano, @@ -37,10 +37,10 @@ fn main() -> Result<(), Box> { controller.add(source_a); // Append the dynamic mixer to the sink to play a C major 6th chord. - sink.append(mixer); + player.append(mixer); // Sleep the thread until sink is empty. - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_flac.rs b/examples/music_flac.rs index f93028be..76c688d4 100644 --- a/examples/music_flac.rs +++ b/examples/music_flac.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.flac")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_m4a.rs b/examples/music_m4a.rs index b8a20921..1f1d7af1 100644 --- a/examples/music_m4a.rs +++ b/examples/music_m4a.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.m4a")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_mp3.rs b/examples/music_mp3.rs index bacc7309..1d2851ae 100644 --- a/examples/music_mp3.rs +++ b/examples/music_mp3.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_ogg.rs b/examples/music_ogg.rs index e9ed41d2..6ea3aafc 100644 --- a/examples/music_ogg.rs +++ b/examples/music_ogg.rs @@ -1,14 +1,14 @@ use std::{error::Error, io::Cursor}; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = include_bytes!("../assets/music.ogg"); let cursor = Cursor::new(file); - sink.append(rodio::Decoder::try_from(cursor)?); + player.append(rodio::Decoder::try_from(cursor)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_wav.rs b/examples/music_wav.rs index ea50de38..d48c75fb 100644 --- a/examples/music_wav.rs +++ b/examples/music_wav.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs index a3c3d444..64a6b3ca 100644 --- a/examples/noise_generator.rs +++ b/examples/noise_generator.rs @@ -7,11 +7,11 @@ use rodio::{ source::noise::{ Blue, Brownian, Pink, Velvet, Violet, WhiteGaussian, WhiteTriangular, WhiteUniform, }, - Sample, Source, + MixerOsSink, Sample, Source, }; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; let sample_rate = stream_handle.config().sample_rate(); play_noise( @@ -74,7 +74,7 @@ fn main() -> Result<(), Box> { } /// Helper function to play a noise type with description -fn play_noise(stream_handle: &rodio::OutputStream, source: S, name: &str, description: &str) +fn play_noise(stream_handle: &MixerOsSink, source: S, name: &str, description: &str) where S: Source + Send + 'static, { diff --git a/examples/reverb.rs b/examples/reverb.rs index b8c04b52..5a4e636c 100644 --- a/examples/reverb.rs +++ b/examples/reverb.rs @@ -3,15 +3,15 @@ use std::error::Error; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.ogg")?; let source = rodio::Decoder::try_from(file)?; let with_reverb = source.buffered().reverb(Duration::from_millis(40), 0.7); - sink.append(with_reverb); + player.append(with_reverb); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/seek_mp3.rs b/examples/seek_mp3.rs index c27f5346..0788ea89 100644 --- a/examples/seek_mp3.rs +++ b/examples/seek_mp3.rs @@ -2,22 +2,22 @@ use std::error::Error; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); std::thread::sleep(std::time::Duration::from_secs(2)); - sink.try_seek(Duration::from_secs(0))?; + player.try_seek(Duration::from_secs(0))?; std::thread::sleep(std::time::Duration::from_secs(2)); - sink.try_seek(Duration::from_secs(4))?; + player.try_seek(Duration::from_secs(4))?; - sink.sleep_until_end(); + player.sleep_until_end(); // This doesn't do anything since the sound has ended already. - sink.try_seek(Duration::from_secs(5))?; + player.try_seek(Duration::from_secs(5))?; println!("seek example ended"); Ok(()) diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index 2257f30d..8949d171 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -8,7 +8,7 @@ fn main() -> Result<(), Box> { use std::thread; use std::time::Duration; - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; let test_signal_duration = Duration::from_millis(1000); let interval_duration = Duration::from_millis(1500); diff --git a/examples/spatial.rs b/examples/spatial.rs index f1b97e44..0013d379 100644 --- a/examples/spatial.rs +++ b/examples/spatial.rs @@ -18,10 +18,10 @@ fn main() -> Result<(), Box> { let total_duration = iter_duration * 2 * repeats; - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; let mut positions = ([0., 0., 0.], [-1., 0., 0.], [1., 0., 0.]); - let sink = rodio::SpatialSink::connect_new( + let player = rodio::SpatialPlayer::connect_new( stream_handle.mixer(), positions.0, positions.1, @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { let source = rodio::Decoder::try_from(file)? .repeat_infinite() .take_duration(total_duration); - sink.append(source); + player.append(source); // A sound emitter playing the music starting at the centre gradually moves to the right // until it stops and begins traveling to the left, it will eventually pass through the // listener again and go to the far left. @@ -41,20 +41,20 @@ fn main() -> Result<(), Box> { for _ in 0..num_steps { thread::sleep(refresh_duration); positions.0[0] += step_distance; - sink.set_emitter_position(positions.0); + player.set_emitter_position(positions.0); } for _ in 0..(num_steps * 2) { thread::sleep(refresh_duration); positions.0[0] -= step_distance; - sink.set_emitter_position(positions.0); + player.set_emitter_position(positions.0); } for _ in 0..num_steps { thread::sleep(refresh_duration); positions.0[0] += step_distance; - sink.set_emitter_position(positions.0); + player.set_emitter_position(positions.0); } } - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/stereo.rs b/examples/stereo.rs index c86eef73..872fa436 100644 --- a/examples/stereo.rs +++ b/examples/stereo.rs @@ -4,13 +4,13 @@ use rodio::Source; use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::OsSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/RL.ogg")?; - sink.append(rodio::Decoder::try_from(file)?.amplify(0.2)); + player.append(rodio::Decoder::try_from(file)?.amplify(0.2)); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/src/common.rs b/src/common.rs index f6da20eb..ca2359e2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,7 +1,7 @@ use std::fmt::{Debug, Display}; use std::num::NonZero; -/// Stream sample rate (a frame rate or samples per second per channel). +/// Sample rate (a frame rate or samples per second per channel). pub type SampleRate = NonZero; /// Number of channels in a stream. Can never be Zero diff --git a/src/lib.rs b/src/lib.rs index cf6011bf..f37dc163 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,31 +3,31 @@ //! The main concept of this library is the [`Source`] trait, which //! represents a sound (streaming or not). In order to play a sound, there are three steps: //! -//! - Get an output stream handle to a physical device. For example, get a stream to the system's -//! default sound device with [`OutputStreamBuilder::open_default_stream()`]. +//! - Get an OS-Sink handle to a physical device. For example, get a sink to the system's +//! default sound device with [`OsSinkBuilder::open_default_stream()`]. //! - Create an object that represents the streaming sound. It can be a sine wave, a buffer, a //! [`decoder`], etc. or even your own type that implements the [`Source`] trait. -//! - Add the source to the output stream using [`OutputStream::mixer()`](OutputStream::mixer) -//! on the output stream handle. +//! - Add the source to the OS-Sink using [`OsSink::mixer()`](OutputStream::mixer) +//! on the OS-Sink handle. //! //! Here is a complete example of how you would play an audio file: //! #![cfg_attr(not(feature = "playback"), doc = "```ignore")] #![cfg_attr(feature = "playback", doc = "```no_run")] //! use std::fs::File; -//! use rodio::{Decoder, OutputStream, source::Source}; +//! use rodio::{Decoder, MixerOsSink, source::Source}; //! -//! // Get an output stream handle to the default physical sound device. -//! // Note that the playback stops when the stream_handle is dropped.//! -//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! // Get an OS-Sink handle to the default physical sound device. +//! // Note that the playback stops when the handle is dropped.//! +//! let handle = rodio::OsSinkBuilder::open_default_sink() //! .expect("open default audio stream"); -//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); +//! let player = rodio::Player::connect_new(&handle.mixer()); //! // Load a sound from a file, using a path relative to Cargo.toml //! let file = File::open("examples/music.ogg").unwrap(); //! // Decode that sound file into a source //! let source = Decoder::try_from(file).unwrap(); //! // Play the sound directly on the device -//! stream_handle.mixer().add(source); +//! handle.mixer().add(source); //! //! // The sound plays in a separate audio thread, //! // so we need to keep the main thread alive while it's playing. @@ -39,17 +39,17 @@ #![cfg_attr(feature = "playback", doc = "```no_run")] //! use std::fs::File; //! use std::io::BufReader; -//! use rodio::{Decoder, OutputStream, source::Source}; +//! use rodio::{Decoder, MixerOsSink, source::Source}; //! -//! // Get an output stream handle to the default physical sound device. -//! // Note that the playback stops when the stream_handle is dropped. -//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! // Get an OS-Sink handle to the default physical sound device. +//! // Note that the playback stops when the sink_handle is dropped. +//! let sink_handle = rodio::OsSinkBuilder::open_default_sink() //! .expect("open default audio stream"); //! //! // Load a sound from a file, using a path relative to Cargo.toml //! let file = BufReader::new(File::open("examples/music.ogg").unwrap()); -//! // Note that the playback stops when the sink is dropped -//! let sink = rodio::play(&stream_handle.mixer(), file).unwrap(); +//! // Note that the playback stops when the player is dropped +//! let player = rodio::play(&sink_handle.mixer(), file).unwrap(); //! //! // The sound plays in a separate audio thread, //! // so we need to keep the main thread alive while it's playing. @@ -57,43 +57,43 @@ //! ``` //! //! -//! ## Sink +//! ## Player //! //! In order to make it easier to control the playback, the rodio library also provides a type -//! named [`Sink`] which represents an audio track. [`Sink`] plays its input sources sequentially, +//! named [`Player`] which represents an audio track. [`Player`] plays its input sources sequentially, //! one after another. To play sounds in simultaneously in parallel, use [`mixer::Mixer`] instead. //! -//! To play a song Create a [`Sink`] connect it to the output stream, -//! and [`.append()`](Sink::append) your sound to it. +//! To play a song Create a [`Player`], connect it to the OS-Sink, +//! and [`.append()`](Player::append) your sound to it. //! #![cfg_attr(not(feature = "playback"), doc = "```ignore")] #![cfg_attr(feature = "playback", doc = "```no_run")] //! use std::time::Duration; -//! use rodio::{OutputStream, Sink}; +//! use rodio::{MixerOsSink, Player}; //! use rodio::source::{SineWave, Source}; //! //! // _stream must live as long as the sink -//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! let handle = rodio::OsSinkBuilder::open_default_sink() //! .expect("open default audio stream"); -//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); +//! let player = rodio::Player::connect_new(&handle.mixer()); //! //! // Add a dummy source of the sake of the example. //! let source = SineWave::new(440.0).take_duration(Duration::from_secs_f32(0.25)).amplify(0.20); -//! sink.append(source); +//! player.append(source); //! -//! // The sound plays in a separate thread. This call will block the current thread until the sink -//! // has finished playing all its queued sounds. -//! sink.sleep_until_end(); +//! // The sound plays in a separate thread. This call will block the current thread until the +//! // player has finished playing all its queued sounds. +//! player.sleep_until_end(); //! ``` //! -//! The [`append`](Sink::append) method will add the sound at the end of the -//! sink. It will be played when all the previous sounds have been played. If you want multiple -//! sounds to play simultaneously, you should create multiple [`Sink`]s. +//! The [`append`](Player::append) method will add the sound at the end of the +//! player. It will be played when all the previous sounds have been played. If you want multiple +//! sounds to play simultaneously consider building your own [`Player`] from rodio parts. //! -//! The [`Sink`] type also provides utilities such as playing/pausing or controlling the volume. +//! The [`Player`] type also provides utilities such as playing/pausing or controlling the volume. //! -//!
Note that playback through Sink will end if the associated -//! OutputStream is dropped.
+//!
Note that playback through Player will end if the associated +//! OsSink is dropped.
//! //! ## Filters //! @@ -177,9 +177,9 @@ pub use cpal::{ }; mod common; -mod sink; -mod spatial_sink; -#[cfg(all(feature = "playback", feature = "experimental"))] +mod player; +mod spatial_player; +// #[cfg(all(feature = "playback", feature = "experimental"))] pub mod speakers; #[cfg(feature = "playback")] pub mod stream; @@ -196,16 +196,18 @@ pub mod math; pub mod microphone; pub mod mixer; pub mod queue; +// #[cfg(feature = "experimental")] +pub mod fixed_source; pub mod source; pub mod static_buffer; pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate}; pub use crate::decoder::Decoder; -pub use crate::sink::Sink; +pub use crate::player::Player as Player; pub use crate::source::Source; -pub use crate::spatial_sink::SpatialSink; +pub use crate::spatial_player::SpatialPlayer; #[cfg(feature = "playback")] -pub use crate::stream::{play, OutputStream, OutputStreamBuilder, PlayError, StreamError}; +pub use crate::stream::{play, MixerOsSink, OsSinkBuilder, PlayError, OsSinkError}; #[cfg(feature = "wav_output")] #[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] pub use crate::wav_output::wav_to_file; diff --git a/src/mixer.rs b/src/mixer.rs index 4f3241e8..896deaa5 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -15,9 +15,9 @@ use std::time::Duration; /// After creating a mixer, you can add new sounds with the controller. /// /// Note that mixer without any input source behaves like an `Empty` (not: `Zero`) source, -/// and thus, just after appending to a sink, the mixer is removed from the sink. -/// As a result, input sources added to the mixer later might not be forwarded to the sink. -/// Add `Zero` source to prevent detaching the mixer from sink. +/// and thus, just after appending to a player, the mixer is removed from the player. +/// As a result, input sources added to the mixer later might not be forwarded to the player. +/// Add `Zero` source to prevent detaching the mixer from player. pub fn mixer(channels: ChannelCount, sample_rate: SampleRate) -> (Mixer, MixerSource) { let input = Mixer(Arc::new(Inner { has_pending: AtomicBool::new(false), diff --git a/src/sink.rs b/src/player.rs similarity index 87% rename from src/sink.rs rename to src/player.rs index 8c1f61d2..9be3bfdc 100644 --- a/src/sink.rs +++ b/src/player.rs @@ -15,9 +15,9 @@ use crate::{queue, source::Done, Source}; /// Handle to a device that outputs sounds. /// -/// Dropping the `Sink` stops all its sounds. You can use `detach` if you want the sounds to continue +/// Dropping the `Player` stops all its sounds. You can use `detach` if you want the sounds to continue /// playing. -pub struct Sink { +pub struct Player { queue_tx: Arc, sleep_until_end: Mutex>>, @@ -67,21 +67,21 @@ struct Controls { position: Mutex, } -impl Sink { - /// Builds a new `Sink`, beginning playback on a stream. +impl Player { + /// Builds a new `Player`, beginning playback on a stream. #[inline] - pub fn connect_new(mixer: &Mixer) -> Sink { - let (sink, source) = Sink::new(); + pub fn connect_new(mixer: &Mixer) -> Player { + let (sink, source) = Player::new(); mixer.add(source); sink } - /// Builds a new `Sink`. + /// Builds a new `Player`. #[inline] - pub fn new() -> (Sink, queue::SourcesQueueOutput) { + pub fn new() -> (Player, queue::SourcesQueueOutput) { let (queue_tx, queue_rx) = queue::queue(true); - let sink = Sink { + let sink = Player { queue_tx, sleep_until_end: Mutex::new(None), controls: Arc::new(Controls { @@ -181,7 +181,7 @@ impl Sink { /// Gets the speed of the sound. /// - /// See [`Sink::set_speed`] for details on what *speed* means. + /// See [`Player::set_speed`] for details on what *speed* means. #[inline] pub fn speed(&self) -> f32 { *self.controls.speed.lock().unwrap() @@ -205,7 +205,7 @@ impl Sink { *self.controls.speed.lock().unwrap() = value; } - /// Resumes playback of a paused sink. + /// Resumes playback of a paused player. /// /// No effect if not paused. #[inline] @@ -256,7 +256,7 @@ impl Sink { } } - /// Pauses playback of this sink. + /// Pauses playback of this player. /// /// No effect if already paused. /// @@ -267,15 +267,15 @@ impl Sink { /// Gets if a sink is paused /// - /// Sinks can be paused and resumed using `pause()` and `play()`. This returns `true` if the + /// Players can be paused and resumed using `pause()` and `play()`. This returns `true` if the /// sink is paused. pub fn is_paused(&self) -> bool { self.controls.pause.load(Ordering::SeqCst) } - /// Removes all currently loaded `Source`s from the `Sink`, and pauses it. + /// Removes all currently loaded `Source`s from the `Player`, and pauses it. /// - /// See `pause()` for information about pausing a `Sink`. + /// See `pause()` for information about pausing a `Player`. pub fn clear(&self) { let len = self.sound_count.load(Ordering::SeqCst) as u32; *self.controls.to_clear.lock().unwrap() = len; @@ -283,10 +283,10 @@ impl Sink { self.pause(); } - /// Skips to the next `Source` in the `Sink` + /// Skips to the next `Source` in the `Player` /// - /// If there are more `Source`s appended to the `Sink` at the time, - /// it will play the next one. Otherwise, the `Sink` will finish as if + /// If there are more `Source`s appended to the `Player` at the time, + /// it will play the next one. Otherwise, the `Player` will finish as if /// it had finished playing a `Source` all the way through. pub fn skip_one(&self) { let len = self.sound_count.load(Ordering::SeqCst) as u32; @@ -334,7 +334,7 @@ impl Sink { /// This takes into account any speedup or delay applied. /// /// Example: if you apply a speedup of *2* to an mp3 decoder source and - /// [`get_pos()`](Sink::get_pos) returns *5s* then the position in the mp3 + /// [`get_pos()`](Player::get_pos) returns *5s* then the position in the mp3 /// recording is *10s* from its start. #[inline] pub fn get_pos(&self) -> Duration { @@ -342,7 +342,7 @@ impl Sink { } } -impl Drop for Sink { +impl Drop for Player { #[inline] fn drop(&mut self) { self.queue_tx.set_keep_alive_if_empty(false); @@ -359,11 +359,11 @@ mod tests { use crate::buffer::SamplesBuffer; use crate::math::nz; - use crate::{Sink, Source}; + use crate::{Player, Source}; #[test] fn test_pause_and_stop() { - let (sink, mut source) = Sink::new(); + let (player, mut source) = Player::new(); assert_eq!(source.next(), Some(0.0)); // TODO (review) How did this test passed before? I might have broken something but @@ -375,49 +375,49 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // Low rate to ensure immediate control. - sink.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); + player.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); let mut reference_src = SamplesBuffer::new(nz!(1), nz!(1), v); assert_eq!(source.next(), reference_src.next()); assert_eq!(source.next(), reference_src.next()); - sink.pause(); + player.pause(); assert_eq!(source.next(), Some(0.0)); - sink.play(); + player.play(); assert_eq!(source.next(), reference_src.next()); assert_eq!(source.next(), reference_src.next()); - sink.stop(); + player.stop(); assert_eq!(source.next(), Some(0.0)); - assert!(sink.empty()); + assert!(player.empty()); } #[test] fn test_stop_and_start() { - let (sink, mut queue_rx) = Sink::new(); + let (player, mut queue_rx) = Player::new(); let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; - sink.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); + player.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); let mut src = SamplesBuffer::new(nz!(1), nz!(1), v.clone()); assert_eq!(queue_rx.next(), src.next()); assert_eq!(queue_rx.next(), src.next()); - sink.stop(); + player.stop(); - assert!(sink.controls.stopped.load(Ordering::SeqCst)); + assert!(player.controls.stopped.load(Ordering::SeqCst)); assert_eq!(queue_rx.next(), Some(0.0)); src = SamplesBuffer::new(nz!(1), nz!(1), v.clone()); - sink.append(SamplesBuffer::new(nz!(1), nz!(1), v)); + player.append(SamplesBuffer::new(nz!(1), nz!(1), v)); - assert!(!sink.controls.stopped.load(Ordering::SeqCst)); + assert!(!player.controls.stopped.load(Ordering::SeqCst)); // Flush silence let mut queue_rx = queue_rx.skip_while(|v| *v == 0.0); @@ -427,16 +427,16 @@ mod tests { #[test] fn test_volume() { - let (sink, mut queue_rx) = Sink::new(); + let (player, mut queue_rx) = Player::new(); let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // High rate to avoid immediate control. - sink.append(SamplesBuffer::new(nz!(2), nz!(44100), v.clone())); + player.append(SamplesBuffer::new(nz!(2), nz!(44100), v.clone())); let src = SamplesBuffer::new(nz!(2), nz!(44100), v.clone()); let mut src = src.amplify(0.5); - sink.set_volume(0.5); + player.set_volume(0.5); for _ in 0..v.len() { assert_eq!(queue_rx.next(), src.next()); diff --git a/src/source/dither.rs b/src/source/dither.rs index f87e472c..adc8b436 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -22,7 +22,7 @@ //! - **Choose TPDF** for most professional audio applications (it's the default) //! - **Use target output bit depth** - Not the source bit depth! //! -//! When you later change volume (e.g., with `Sink::set_volume()`), both the signal +//! When you later change volume (e.g., with `Player::set_volume()`), both the signal //! and dither noise scale together, maintaining proper dithering behavior. use rand::{rngs::SmallRng, Rng}; diff --git a/src/source/mod.rs b/src/source/mod.rs index 5cc890b1..b458d13a 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -397,15 +397,15 @@ pub trait Source: Iterator { /// ```rust /// // Apply Automatic Gain Control to the source (AGC is on by default) /// use rodio::source::{Source, SineWave, AutomaticGainControlSettings}; - /// use rodio::Sink; + /// use rodio::Player; /// use std::time::Duration; /// let source = SineWave::new(444.0); // An example. - /// let (sink, output) = Sink::new(); // An example. + /// let (player, output) = Player::new(); // An example. /// /// let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default()); /// /// // Add the AGC-controlled source to the sink - /// sink.append(agc_source); + /// player.append(agc_source); /// /// ``` #[inline] @@ -642,7 +642,7 @@ pub trait Source: Iterator { } /// Adds a method [`Skippable::skip`] for skipping this source. Skipping - /// makes Source::next() return None. Which in turn makes the Sink skip to + /// makes Source::next() return None. Which in turn makes the Player skip to /// the next source. fn skippable(self) -> Skippable where diff --git a/src/source/skippable.rs b/src/source/skippable.rs index ba4d43d9..0fc46560 100644 --- a/src/source/skippable.rs +++ b/src/source/skippable.rs @@ -5,7 +5,7 @@ use std::time::Duration; use super::SeekError; /// Wrap the source in a skippable. It allows ending the current source early by -/// calling [`Skippable::skip`]. If this source is in a queue such as the Sink +/// calling [`Skippable::skip`]. If this source is in a queue such as the Player /// ending the source early is equal to skipping the source. pub fn skippable(source: I) -> Skippable { Skippable { @@ -15,7 +15,7 @@ pub fn skippable(source: I) -> Skippable { } /// Wrap the source in a skippable. It allows ending the current source early by -/// calling [`Skippable::skip`]. If this source is in a queue such as the Sink +/// calling [`Skippable::skip`]. If this source is in a queue such as the Player /// ending the source early is equal to skipping the source. #[derive(Clone, Debug)] pub struct Skippable { diff --git a/src/source/spatial.rs b/src/source/spatial.rs index 8aad0b7e..136dad9d 100644 --- a/src/source/spatial.rs +++ b/src/source/spatial.rs @@ -27,7 +27,7 @@ impl Spatial where I: Source, { - /// Builds a new `SpatialSink`, beginning playback on a stream. + /// Builds a new `SpatialPlayer`, beginning playback on a stream. pub fn new( input: I, emitter_position: [f32; 3], diff --git a/src/source/speed.rs b/src/source/speed.rs index e38c2b99..b9ea51da 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -1,7 +1,7 @@ //! Playback Speed control Module. //! //! The main concept of this module is the [`Speed`] struct, which -//! encapsulates playback speed controls of the current sink. +//! encapsulates playback speed controls of the current player. //! //! In order to speed up a sink, the speed struct: //! - Increases the current sample rate by the given factor. @@ -14,18 +14,18 @@ #![cfg_attr(not(feature = "playback"), doc = "```ignore")] #![cfg_attr(feature = "playback", doc = "```no_run")] //!# use std::fs::File; -//!# use rodio::{Decoder, Sink, OutputStream, source::{Source, SineWave}}; +//!# use rodio::{Decoder, Player, source::{Source, SineWave}}; //! -//! // Get an output stream handle to the default physical sound device. -//! // Note that no sound will be played if the _stream is dropped. -//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() -//! .expect("open default audio stream"); +//! // Get an OS-Sink handle to the default physical sound device. +//! // Note that no sound will be played if the _handle_ is dropped. +//! let handle = rodio::OsSinkBuilder::open_default_sink() +//! .expect("open default audio sink"); //! // Load a sound from a file, using a path relative to `Cargo.toml` //! let file = File::open("examples/music.ogg").unwrap(); //! // Decode that sound file into a source //! let source = Decoder::try_from(file).unwrap(); //! // Play the sound directly on the device 2x faster -//! stream_handle.mixer().add(source.speed(2.0)); +//! handle.mixer().add(source.speed(2.0)); //! std::thread::sleep(std::time::Duration::from_secs(5)); //! ``` //! Here is how you would do it using the sink: @@ -35,11 +35,11 @@ //! let source = SineWave::new(440.0) //! .take_duration(std::time::Duration::from_secs_f32(20.25)) //! .amplify(0.20); -//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() -//! .expect("open default audio stream"); -//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); -//! sink.set_speed(2.0); -//! sink.append(source); +//! let handle = rodio::OsSinkBuilder::open_default_sink() +//! .expect("open default audio sink"); +//! let player = rodio::Player::connect_new(&handle.mixer()); +//! player.set_speed(2.0); +//! player.append(source); //! std::thread::sleep(std::time::Duration::from_secs(5)); //! ``` //! Notice the increase in pitch as the factor increases diff --git a/src/spatial_sink.rs b/src/spatial_player.rs similarity index 86% rename from src/spatial_sink.rs rename to src/spatial_player.rs index 8a0719c9..d1fe3cb2 100644 --- a/src/spatial_sink.rs +++ b/src/spatial_player.rs @@ -6,13 +6,13 @@ use dasp_sample::FromSample; use crate::mixer::Mixer; use crate::source::{SeekError, Spatial}; -use crate::{Float, Sink, Source}; +use crate::{Float, Player, Source}; /// A sink that allows changing the position of the source and the listeners /// ears while playing. The sources played are then transformed to give a simple /// spatial effect. See [`Spatial`] for details. -pub struct SpatialSink { - sink: Sink, +pub struct SpatialPlayer { + player: Player, positions: Arc>, } @@ -22,16 +22,16 @@ struct SoundPositions { right_ear: [f32; 3], } -impl SpatialSink { - /// Builds a new `SpatialSink`. +impl SpatialPlayer { + /// Builds a new `SpatialPlayer`. pub fn connect_new( mixer: &Mixer, emitter_position: [f32; 3], left_ear: [f32; 3], right_ear: [f32; 3], - ) -> SpatialSink { - SpatialSink { - sink: Sink::connect_new(mixer), + ) -> SpatialPlayer { + SpatialPlayer { + player: Player::connect_new(mixer), positions: Arc::new(Mutex::new(SoundPositions { emitter_position, left_ear, @@ -74,7 +74,7 @@ impl SpatialSink { let pos = positions.lock().unwrap(); i.set_positions(pos.emitter_position, pos.left_ear, pos.right_ear); }); - self.sink.append(source); + self.player.append(source); } // Gets the volume of the sound. @@ -83,7 +83,7 @@ impl SpatialSink { /// multiply each sample by this value. #[inline] pub fn volume(&self) -> Float { - self.sink.volume() + self.player.volume() } /// Changes the volume of the sound. @@ -92,7 +92,7 @@ impl SpatialSink { /// multiply each sample by this value. #[inline] pub fn set_volume(&self, value: Float) { - self.sink.set_volume(value); + self.player.set_volume(value); } /// Changes the play speed of the sound. Does not adjust the samples, only the playback speed. @@ -111,7 +111,7 @@ impl SpatialSink { /// See [`Speed`](crate::source::Speed) for details #[inline] pub fn speed(&self) -> f32 { - self.sink.speed() + self.player.speed() } /// Changes the speed of the sound. @@ -120,7 +120,7 @@ impl SpatialSink { /// change the play speed of the sound. #[inline] pub fn set_speed(&self, value: f32) { - self.sink.set_speed(value) + self.player.set_speed(value) } /// Resumes playback of a paused sound. @@ -128,62 +128,62 @@ impl SpatialSink { /// No effect if not paused. #[inline] pub fn play(&self) { - self.sink.play(); + self.player.play(); } - /// Pauses playback of this sink. + /// Pauses playback of this player. /// /// No effect if already paused. /// /// A paused sound can be resumed with `play()`. pub fn pause(&self) { - self.sink.pause(); + self.player.pause(); } /// Gets if a sound is paused /// /// Sounds can be paused and resumed using pause() and play(). This gets if a sound is paused. pub fn is_paused(&self) -> bool { - self.sink.is_paused() + self.player.is_paused() } - /// Removes all currently loaded `Source`s from the `SpatialSink` and pauses it. + /// Removes all currently loaded `Source`s from the `SpatialPlayer` and pauses it. /// - /// See `pause()` for information about pausing a `Sink`. + /// See `pause()` for information about pausing a `Player`. #[inline] pub fn clear(&self) { - self.sink.clear(); + self.player.clear(); } /// Stops the sink by emptying the queue. #[inline] pub fn stop(&self) { - self.sink.stop() + self.player.stop() } /// Destroys the sink without stopping the sounds that are still playing. #[inline] pub fn detach(self) { - self.sink.detach(); + self.player.detach(); } /// Sleeps the current thread until the sound ends. #[inline] pub fn sleep_until_end(&self) { - self.sink.sleep_until_end(); + self.player.sleep_until_end(); } /// Returns true if this sink has no more sounds to play. #[inline] pub fn empty(&self) -> bool { - self.sink.empty() + self.player.empty() } /// Returns the number of sounds currently in the queue. #[allow(clippy::len_without_is_empty)] #[inline] pub fn len(&self) -> usize { - self.sink.len() + self.player.len() } /// Attempts to seek to a given position in the current source. @@ -205,7 +205,7 @@ impl SpatialSink { /// When seeking beyond the end of a source this /// function might return an error if the duration of the source is not known. pub fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { - self.sink.try_seek(pos) + self.player.try_seek(pos) } /// Returns the position of the sound that's being played. @@ -213,10 +213,10 @@ impl SpatialSink { /// This takes into account any speedup or delay applied. /// /// Example: if you apply a speedup of *2* to an mp3 decoder source and - /// [`get_pos()`](Sink::get_pos) returns *5s* then the position in the mp3 + /// [`get_pos()`](Player::get_pos) returns *5s* then the position in the mp3 /// recording is *10s* from its start. #[inline] pub fn get_pos(&self) -> Duration { - self.sink.get_pos() + self.player.get_pos() } } diff --git a/src/speakers.rs b/src/speakers.rs index 2ef21260..c74c7e10 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -1,6 +1,6 @@ //! A speakers sink //! -//! An audio *stream* originates at a [Source] and flows to a Sink. This is a +//! An audio *stream* originates at a [Source] and flows to a player. This is a //! Sink that plays audio over the systems speakers or headphones through an //! audio output device; //! @@ -13,7 +13,7 @@ //! let speakers = SpeakersBuilder::new() //! .default_device()? //! .default_config()? -//! .open_stream()?; +//! .open_mixer_sink()?; //! let mixer = speakers.mixer(); //! //! // Play a beep for 4 seconds @@ -46,7 +46,7 @@ //! ]) //! .prefer_buffer_sizes(512..); //! -//! let mic = builder.open_stream()?; +//! let mixer = builder.open_mixer_sink()?; //! # Ok(()) //! # } //! ``` @@ -73,7 +73,7 @@ //! // builder remains unchanged with default configuration //! } //! -//! let mic = builder.open_stream()?; +//! let speakers = builder.open_mixer_sink()?; //! # Ok(()) //! # } //! ``` @@ -91,10 +91,10 @@ //! } //! //! // Use a specific device (e.g., the second one) -//! let mic = SpeakersBuilder::new() +//! let speakers = SpeakersBuilder::new() //! .device(outputs[1].clone())? //! .default_config()? -//! .open_stream()?; +//! .open_mixer_sink()?; //! # Ok(()) //! # } //! ``` @@ -106,7 +106,7 @@ use cpal::{ Device, }; -use crate::{common::assert_error_traits, StreamError}; +use crate::{common::assert_error_traits, OsSinkError}; mod builder; mod config; @@ -184,7 +184,7 @@ impl Speakers { device: Device, config: OutputConfig, error_callback: impl FnMut(cpal::StreamError) + Send + 'static, - ) -> Result { - crate::stream::OutputStream::open(&device, &config.into_cpal_config(), error_callback) + ) -> Result { + crate::stream::MixerOsSink::open(&device, &config.into_cpal_config(), error_callback) } } diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index fe7656af..f744eb30 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -1,14 +1,15 @@ use std::{fmt::Debug, marker::PhantomData}; use cpal::{ - traits::{DeviceTrait, HostTrait}, + traits::{DeviceTrait, HostTrait, StreamTrait}, SupportedStreamConfigRange, }; use crate::{ common::assert_error_traits, + fixed_source::FixedSource, speakers::{self, config::OutputConfig}, - ChannelCount, OutputStream, SampleRate, + ChannelCount, MixerOsSink, SampleRate, OsSinkError, }; /// Error configuring or opening speakers output @@ -54,7 +55,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 output streams. +/// Builder for configuring and opening an OS-Sink, usually a speaker or headphone. #[must_use] pub struct SpeakersBuilder where @@ -535,7 +536,7 @@ impl SpeakersBuilder where E: FnMut(cpal::StreamError) + Send + Clone + 'static, { - /// Opens the speakers output stream. + /// Opens the OS-Sink and provide a mixer for playing sources on it. /// /// # Example /// ```no_run @@ -545,18 +546,90 @@ where /// let speakers = SpeakersBuilder::new() /// .default_device()? /// .default_config()? - /// .open_stream()?; + /// .open_mixer_sink()?; /// 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 { + pub fn open_mixer_sink(&self) -> Result { speakers::Speakers::open( self.device.as_ref().expect("DeviceIsSet").0.clone(), *self.config.as_ref().expect("ConfigIsSet"), self.error_callback.clone(), ) } + + /// TODO + pub fn open_queue_sink(&self) -> Result { + todo!() + } + + /// TODO + pub fn play( + self, + mut source: impl FixedSource + Send + 'static, + ) -> Result { + use cpal::Sample as _; + + let config = self.config.expect("ConfigIsSet"); + let device = self.device.expect("DeviceIsSet").0; + let cpal_config1 = config.into_cpal_config(); + let cpal_config2 = (&cpal_config1).into(); + + macro_rules! build_output_streams { + ($($sample_format:tt, $generic:ty);+) => { + match config.sample_format { + $( + cpal::SampleFormat::$sample_format => device.build_output_stream::<$generic, _, _>( + &cpal_config2, + move |data, _| { + data.iter_mut().for_each(|d| { + *d = source + .next() + .map(cpal::Sample::from_sample) + .unwrap_or(<$generic>::EQUILIBRIUM) + }) + }, + self.error_callback, + None, + ), + )+ + _ => return Err(OsSinkError::UnsupportedSampleFormat), + } + }; + } + + let result = build_output_streams!( + F32, f32; + F64, f64; + I8, i8; + I16, i16; + I24, cpal::I24; + I32, i32; + I64, i64; + U8, u8; + U16, u16; + U24, cpal::U24; + U32, u32; + U64, u64 + ); + + result + .map_err(OsSinkError::BuildError) + .map(|stream| { + stream + .play() + .map_err(OsSinkError::PlayError) + .map(|()| stream) + })? + .map(SinkHandle) + } } + +// TODO +pub struct QueueSink; + +// TODO +pub struct SinkHandle(cpal::Stream); diff --git a/src/speakers/config.rs b/src/speakers/config.rs index 4debc6de..947532bc 100644 --- a/src/speakers/config.rs +++ b/src/speakers/config.rs @@ -1,6 +1,6 @@ use std::num::NonZero; -use crate::{math::nz, stream::OutputStreamConfig, ChannelCount, SampleRate}; +use crate::{math::nz, stream::OsSinkConfig, ChannelCount, SampleRate}; /// Describes the input stream's configuration #[derive(Copy, Clone, Debug)] @@ -44,8 +44,8 @@ impl OutputConfig { this } - pub(crate) fn into_cpal_config(self) -> crate::stream::OutputStreamConfig { - OutputStreamConfig { + pub(crate) fn into_cpal_config(self) -> crate::stream::OsSinkConfig { + OsSinkConfig { channel_count: self.channel_count, sample_rate: self.sample_rate, buffer_size: self.buffer_size, diff --git a/src/stream.rs b/src/stream.rs index eaa81176..eab18e7d 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,14 +1,14 @@ //! Output audio via the OS via mixers or play directly //! //! This module provides a builder that's used to configure and open audio output. Once -//! opened sources can be mixed into the output via `OutputStream::mixer`. +//! opened sources can be mixed into the output via `OsSink::mixer`. //! //! There is also a convenience function `play` for using that output mixer to //! play a single sound. use crate::common::{assert_error_traits, ChannelCount, SampleRate}; use crate::math::nz; use crate::mixer::{mixer, Mixer}; -use crate::sink::Sink; +use crate::player::Player; use crate::{decoder, Source}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::{BufferSize, Sample, SampleFormat, StreamConfig, I24}; @@ -22,80 +22,80 @@ const HZ_44100: SampleRate = nz!(44_100); /// `cpal::Stream` container. Use `mixer()` method to control output. /// ///
When dropped playback will end, and the associated -/// output stream will be disposed
+/// OS-Sink will be disposed /// /// # Note /// On drop this will print a message to stderr or emit a log msg when tracing is /// enabled. Though we recommend you do not you can disable that print/log with: -/// [`OutputStream::log_on_drop(false)`](OutputStream::log_on_drop). -/// If the `OutputStream` is dropped because the program is panicking we do not print +/// [`OsSink::log_on_drop(false)`](OsSink::log_on_drop). +/// If the `OsSink` is dropped because the program is panicking we do not print /// or log anything. /// /// # Example /// ```no_run -/// # use rodio::OutputStreamBuilder; +/// # use rodio::OsSinkBuilder; /// # fn main() -> Result<(), Box> { -/// let mut stream_handle = OutputStreamBuilder::open_default_stream()?; -/// stream_handle.log_on_drop(false); // Not recommended during development -/// println!("Output config: {:?}", stream_handle.config()); -/// let mixer = stream_handle.mixer(); +/// let mut handle = OsSinkBuilder::open_default_sink()?; +/// handle.log_on_drop(false); // Not recommended during development +/// println!("Output config: {:?}", handle.config()); +/// let mixer = handle.mixer(); /// # Ok(()) /// # } /// ``` -pub struct OutputStream { - config: OutputStreamConfig, +pub struct MixerOsSink { + config: OsSinkConfig, mixer: Mixer, log_on_drop: bool, _stream: cpal::Stream, } -impl OutputStream { - /// Access the output stream's mixer. +impl MixerOsSink { + /// Access the sink's mixer. pub fn mixer(&self) -> &Mixer { &self.mixer } - /// Access the output stream's config. - pub fn config(&self) -> &OutputStreamConfig { + /// Access the sink's config. + pub fn config(&self) -> &OsSinkConfig { &self.config } - /// When [`OutputStream`] is dropped a message is logged to stderr or + /// When [`OS-Sink`] is dropped a message is logged to stderr or /// emitted through tracing if the tracing feature is enabled. pub fn log_on_drop(&mut self, enabled: bool) { self.log_on_drop = enabled; } } -impl Drop for OutputStream { +impl Drop for MixerOsSink { fn drop(&mut self) { if self.log_on_drop && !std::thread::panicking() { #[cfg(feature = "tracing")] - tracing::debug!("Dropping OutputStream, audio playing through this stream will stop"); + tracing::debug!("Dropping OsSink, audio playing through this sink will stop"); #[cfg(not(feature = "tracing"))] - eprintln!("Dropping OutputStream, audio playing through this stream will stop, to prevent this message from appearing use tracing or call `.log_on_drop(false)` on this OutputStream") + eprintln!("Dropping OsSink, audio playing through this sink will stop, to prevent this message from appearing use tracing or call `.log_on_drop(false)` on this OsSink") } } } -impl fmt::Debug for OutputStream { +impl fmt::Debug for MixerOsSink { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("OutputStream") + f.debug_struct("MixerOsSink") .field("config", &self.config) .finish_non_exhaustive() } } -/// Describes the output stream's configuration +/// Describes the OS-Sink's configuration #[derive(Copy, Clone, Debug)] -pub struct OutputStreamConfig { +pub struct OsSinkConfig { pub(crate) channel_count: ChannelCount, pub(crate) sample_rate: SampleRate, pub(crate) buffer_size: BufferSize, pub(crate) sample_format: SampleFormat, } -impl Default for OutputStreamConfig { +impl Default for OsSinkConfig { fn default() -> Self { Self { channel_count: nz!(2), @@ -106,29 +106,29 @@ impl Default for OutputStreamConfig { } } -impl OutputStreamConfig { - /// Access the output stream config's channel count. +impl OsSinkConfig { + /// Access the OS-Sink config's channel count. pub fn channel_count(&self) -> ChannelCount { self.channel_count } - /// Access the output stream config's sample rate. + /// Access the OS-Sink config's sample rate. pub fn sample_rate(&self) -> SampleRate { self.sample_rate } - /// Access the output stream config's buffer size. + /// Access the OS-Sink config's buffer size. pub fn buffer_size(&self) -> &BufferSize { &self.buffer_size } - /// Access the output stream config's sample format. + /// Access the OS-Sink config's sample format. pub fn sample_format(&self) -> SampleFormat { self.sample_format } } -impl core::fmt::Debug for OutputStreamBuilder { +impl core::fmt::Debug for OsSinkBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let device = if let Some(device) = &self.device { "Some(".to_owned() @@ -141,7 +141,7 @@ impl core::fmt::Debug for OutputStreamBuilder { "None".to_owned() }; - f.debug_struct("OutputStreamBuilder") + f.debug_struct("OsSinkBuilder") .field("device", &device) .field("config", &self.config) .finish() @@ -155,56 +155,56 @@ fn default_error_callback(err: cpal::StreamError) { eprintln!("audio stream error: {err}"); } -/// Convenience builder for audio output stream. +/// Convenience builder for audio OS-player. /// It provides methods to configure several parameters of the audio output and opening default /// device. See examples for use-cases. /// -///
When the OutputStream is dropped playback will end, and the associated -/// output stream will be disposed
-pub struct OutputStreamBuilder +///
When the OsSink is dropped playback will end, and the associated +/// OS-Sink will be disposed
+pub struct OsSinkBuilder where E: FnMut(cpal::StreamError) + Send + 'static, { device: Option, - config: OutputStreamConfig, + config: OsSinkConfig, error_callback: E, } -impl Default for OutputStreamBuilder { +impl Default for OsSinkBuilder { fn default() -> Self { Self { device: None, - config: OutputStreamConfig::default(), + config: OsSinkConfig::default(), error_callback: default_error_callback, } } } -impl OutputStreamBuilder { +impl OsSinkBuilder { /// Sets output device and its default parameters. - pub fn from_device(device: cpal::Device) -> Result { + pub fn from_device(device: cpal::Device) -> Result { let default_config = device .default_output_config() - .map_err(StreamError::DefaultStreamConfigError)?; + .map_err(OsSinkError::DefaultSinkConfigError)?; Ok(Self::default() .with_device(device) .with_supported_config(&default_config)) } - /// Sets default output stream parameters for default output audio device. - pub fn from_default_device() -> Result { + /// Sets default OS-Sink parameters for default output audio device. + pub fn from_default_device() -> Result { let default_device = cpal::default_host() .default_output_device() - .ok_or(StreamError::NoDevice)?; + .ok_or(OsSinkError::NoDevice)?; Self::from_device(default_device) } - /// Try to open a new output stream for the default output device with its default configuration. - /// Failing that attempt to open output stream with alternative configuration and/or non default + /// Try to open a new OS-Sink for the default output device with its default configuration. + /// Failing that attempt to open OS-Sink with alternative configuration and/or non default /// output devices. Returns stream for first of the tried configurations that succeeds. /// If all attempts fail return the initial error. - pub fn open_default_stream() -> Result { + pub fn open_default_sink() -> Result { Self::from_default_device() .and_then(|x| x.open_stream()) .or_else(|original_err| { @@ -221,7 +221,7 @@ impl OutputStreamBuilder { devices .find_map(|d| { Self::from_device(d) - .and_then(|x| x.open_stream_or_fallback()) + .and_then(|x| x.open_sink_or_fallback()) .ok() }) .ok_or(original_err) @@ -229,27 +229,27 @@ impl OutputStreamBuilder { } } -impl OutputStreamBuilder +impl OsSinkBuilder where E: FnMut(cpal::StreamError) + Send + 'static, { /// Sets output audio device keeping all existing stream parameters intact. /// This method is useful if you want to set other parameters yourself. /// To also set parameters that are appropriate for the device use [Self::from_device()] instead. - pub fn with_device(mut self, device: cpal::Device) -> OutputStreamBuilder { + pub fn with_device(mut self, device: cpal::Device) -> OsSinkBuilder { self.device = Some(device); self } - /// Sets number of output stream's channels. - pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { + /// Sets number of OS-Sink's channels. + pub fn with_channels(mut self, channel_count: ChannelCount) -> OsSinkBuilder { assert!(channel_count.get() > 0); self.config.channel_count = channel_count; self } - /// Sets output stream's sample rate. - pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OutputStreamBuilder { + /// Sets OS-Sink's sample rate. + pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OsSinkBuilder { self.config.sample_rate = sample_rate; self } @@ -292,13 +292,13 @@ where /// - Low-latency (audio production, live monitoring): 512-1024 /// - General use (games, media playback): 1024-2048 /// - Stability-focused (background music, non-interactive): 2048-4096 - pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { + pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OsSinkBuilder { self.config.buffer_size = buffer_size; self } /// Select scalar type that will carry a sample. - pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OutputStreamBuilder { + pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OsSinkBuilder { self.config.sample_format = sample_format; self } @@ -308,8 +308,8 @@ where pub fn with_supported_config( mut self, config: &cpal::SupportedStreamConfig, - ) -> OutputStreamBuilder { - self.config = OutputStreamConfig { + ) -> OsSinkBuilder { + self.config = OsSinkConfig { channel_count: NonZero::new(config.channels()) .expect("no valid cpal config has zero channels"), sample_rate: NonZero::new(config.sample_rate()) @@ -320,9 +320,9 @@ where self } - /// Set all output stream parameters at once from CPAL stream config. - pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { - self.config = OutputStreamConfig { + /// Set all OS-Sink parameters at once from CPAL stream config. + pub fn with_config(mut self, config: &cpal::StreamConfig) -> OsSinkBuilder { + self.config = OsSinkConfig { channel_count: NonZero::new(config.channels) .expect("no valid cpal config has zero channels"), sample_rate: NonZero::new(config.sample_rate) @@ -334,38 +334,38 @@ where } /// Set a callback that will be called when an error occurs with the stream - pub fn with_error_callback(self, callback: F) -> OutputStreamBuilder + pub fn with_error_callback(self, callback: F) -> OsSinkBuilder where F: FnMut(cpal::StreamError) + Send + 'static, { - OutputStreamBuilder { + OsSinkBuilder { device: self.device, config: self.config, error_callback: callback, } } - /// Open output stream using parameters configured so far. - pub fn open_stream(self) -> Result { + /// Open OS-Sink using parameters configured so far. + pub fn open_stream(self) -> Result { let device = self.device.as_ref().expect("No output device specified"); - OutputStream::open(device, &self.config, self.error_callback) + MixerOsSink::open(device, &self.config, self.error_callback) } - /// Try opening a new output stream with the builder's current stream configuration. + /// Try opening a new OS-Sink with the builder's current stream configuration. /// Failing that attempt to open stream with other available configurations /// supported by the device. /// If all attempts fail returns initial error. - pub fn open_stream_or_fallback(&self) -> Result + pub fn open_sink_or_fallback(&self) -> Result where E: Clone, { let device = self.device.as_ref().expect("No output device specified"); let error_callback = &self.error_callback; - OutputStream::open(device, &self.config, error_callback.clone()).or_else(|err| { + MixerOsSink::open(device, &self.config, error_callback.clone()).or_else(|err| { for supported_config in supported_output_configs(device)? { - if let Ok(handle) = OutputStreamBuilder::default() + if let Ok(handle) = OsSinkBuilder::default() .with_device(device.clone()) .with_supported_config(&supported_config) .with_error_callback(error_callback.clone()) @@ -380,19 +380,19 @@ where } /// A convenience function. Plays a sound once. -/// Returns a `Sink` that can be used to control the sound. -pub fn play(mixer: &Mixer, input: R) -> Result +/// Returns a `Player` that can be used to control the sound. +pub fn play(mixer: &Mixer, input: R) -> Result where R: Read + Seek + Send + Sync + 'static, { let input = decoder::Decoder::new(input)?; - let sink = Sink::connect_new(mixer); - sink.append(input); - Ok(sink) + let player = Player::connect_new(mixer); + player.append(input); + Ok(player) } -impl From<&OutputStreamConfig> for StreamConfig { - fn from(config: &OutputStreamConfig) -> Self { +impl From<&OsSinkConfig> for StreamConfig { + fn from(config: &OsSinkConfig) -> Self { cpal::StreamConfig { channels: config.channel_count.get() as cpal::ChannelCount, sample_rate: config.sample_rate.get(), @@ -422,22 +422,22 @@ assert_error_traits!(PlayError); /// Errors that might occur when interfacing with audio output. #[derive(Debug, thiserror::Error)] -pub enum StreamError { - /// Could not start playing the stream, see [cpal::PlayStreamError] for +pub enum OsSinkError { + /// Could not start playing the sink, see [cpal::PlayStreamError] for /// details. #[error("Could not start playing the stream")] - PlayStreamError(#[source] cpal::PlayStreamError), + PlayError(#[source] cpal::PlayStreamError), /// Failed to get the stream config for the given device. See /// [cpal::DefaultStreamConfigError] for details. - #[error("Failed to get the stream config for the given device")] - DefaultStreamConfigError(#[source] cpal::DefaultStreamConfigError), - /// Error opening stream with OS. See [cpal::BuildStreamError] for details. + #[error("Failed to get the config for the given device")] + DefaultSinkConfigError(#[source] cpal::DefaultStreamConfigError), + /// Error opening sink with OS. See [cpal::BuildStreamError] for details. #[error("Error opening the stream with the OS")] - BuildStreamError(#[source] cpal::BuildStreamError), - /// Could not list supported stream configs for the device. Maybe it + BuildError(#[source] cpal::BuildStreamError), + /// Could not list supported configs for the device. Maybe it /// disconnected. For details see: [cpal::SupportedStreamConfigsError]. - #[error("Could not list supported stream configs for the device. Maybe its disconnected?")] - SupportedStreamConfigsError(#[source] cpal::SupportedStreamConfigsError), + #[error("Could not list supported configs for the device. Maybe its disconnected?")] + SupportedConfigsError(#[source] cpal::SupportedStreamConfigsError), /// Could not find any output device #[error("Could not find any output device")] NoDevice, @@ -447,8 +447,8 @@ pub enum StreamError { UnsupportedSampleFormat, } -impl OutputStream { - fn validate_config(config: &OutputStreamConfig) { +impl MixerOsSink { + fn validate_config(config: &OsSinkConfig) { if let BufferSize::Fixed(sz) = config.buffer_size { assert!(sz > 0, "fixed buffer size must be greater than zero"); } @@ -456,16 +456,16 @@ impl OutputStream { pub(crate) fn open( device: &cpal::Device, - config: &OutputStreamConfig, + config: &OsSinkConfig, error_callback: E, - ) -> Result + ) -> Result where E: FnMut(cpal::StreamError) + Send + 'static, { Self::validate_config(config); let (controller, source) = mixer(config.channel_count, config.sample_rate); Self::init_stream(device, config, source, error_callback).and_then(|stream| { - stream.play().map_err(StreamError::PlayStreamError)?; + stream.play().map_err(OsSinkError::PlayError)?; Ok(Self { _stream: stream, mixer: controller, @@ -477,10 +477,10 @@ impl OutputStream { fn init_stream( device: &cpal::Device, - config: &OutputStreamConfig, + config: &OsSinkConfig, mut samples: S, error_callback: E, - ) -> Result + ) -> Result where S: Source + Send + 'static, E: FnMut(cpal::StreamError) + Send + 'static, @@ -505,7 +505,7 @@ impl OutputStream { None, ), )+ - _ => return Err(StreamError::UnsupportedSampleFormat), + _ => return Err(OsSinkError::UnsupportedSampleFormat), } }; } @@ -525,17 +525,17 @@ impl OutputStream { U64, u64 ); - result.map_err(StreamError::BuildStreamError) + result.map_err(OsSinkError::BuildError) } } /// Return all formats supported by the device. pub fn supported_output_configs( device: &cpal::Device, -) -> Result, StreamError> { +) -> Result, OsSinkError> { let mut supported: Vec<_> = device .supported_output_configs() - .map_err(StreamError::SupportedStreamConfigsError)? + .map_err(OsSinkError::SupportedConfigsError)? .collect(); supported.sort_by(|a, b| b.cmp_default_heuristics(a)); diff --git a/src/wav_output.rs b/src/wav_output.rs index 981be50d..f9234169 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -24,7 +24,7 @@ assert_error_traits!(ToWavError); /// Saves Source's output into a wav file. The output samples format is 32-bit /// float. This function is intended primarily for testing and diagnostics. It can be used to see -/// the output without opening output stream to a real audio device. +/// the output without opening OS-Sink to a real audio device. /// /// If the file already exists it will be overwritten. /// @@ -42,7 +42,7 @@ pub fn wav_to_file( /// Saves Source's output into a writer. The output samples format is 32-bit float. This function /// is intended primarily for testing and diagnostics. It can be used to see the output without -/// opening output stream to a real audio device. +/// opening an OS-Sink to a real audio device. /// /// # Example /// ```rust From a0e3633b4d039b7be96f09356b35f2159ed8652a Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 17:13:40 +0100 Subject: [PATCH 15/16] remove fixed_source --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f37dc163..4bb7db59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,8 +196,6 @@ pub mod math; pub mod microphone; pub mod mixer; pub mod queue; -// #[cfg(feature = "experimental")] -pub mod fixed_source; pub mod source; pub mod static_buffer; From 2f3a2646499ae5dedc9b9632d992979a279251ff Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 3 Jan 2026 17:14:04 +0100 Subject: [PATCH 16/16] re-enable features --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4bb7db59..a499f7e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,7 @@ pub use cpal::{ mod common; mod player; mod spatial_player; -// #[cfg(all(feature = "playback", feature = "experimental"))] +#[cfg(all(feature = "playback", feature = "experimental"))] pub mod speakers; #[cfg(feature = "playback")] pub mod stream;