From 0f8cb0e43aae33672a777c3b385840fa7ef33a6e Mon Sep 17 00:00:00 2001 From: Yara Date: Thu, 26 Feb 2026 23:22:52 +0100 Subject: [PATCH 1/2] limit-buffersize --- .envrc | 1 + codebook.toml | 1 + src/speakers.rs | 2 +- src/speakers/builder.rs | 34 +++-- src/speakers/builder/buffer_duration.rs | 195 ++++++++++++++++++++++++ src/speakers/config.rs | 71 ++++++--- src/stream.rs | 18 ++- 7 files changed, 287 insertions(+), 35 deletions(-) create mode 100644 .envrc create mode 100644 src/speakers/builder/buffer_duration.rs diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/codebook.toml b/codebook.toml index 5dd5806a..2c3f1bed 100644 --- a/codebook.toml +++ b/codebook.toml @@ -1,5 +1,6 @@ words = [ "cartney", + "cpal", "decorrelated", "decorrelating", "gpdf", diff --git a/src/speakers.rs b/src/speakers.rs index 9f66fcde..8ef628cc 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -109,7 +109,7 @@ mod builder; mod config; pub use builder::SpeakersBuilder; -pub use config::OutputConfig; +pub use config::{BufferSize, OutputConfig}; /// Error that can occur when we can not list the output devices #[derive(Debug, thiserror::Error, Clone)] diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 2daee923..3745552e 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -10,6 +10,10 @@ use crate::{ FixedSource, MixerDeviceSink, SampleRate, }; +use super::BufferSize; + +mod buffer_duration; + /// Error configuring or opening speakers output #[allow(missing_docs)] #[derive(Debug, thiserror::Error, Clone)] @@ -210,7 +214,7 @@ where })? .into(); - // Lets try getting f32 output from the default config, as thats + // Lets try getting f32 output from the default config, as that's // what rodio uses internally let config = if self .check_config(&default_config.with_f32_samples()) @@ -234,12 +238,12 @@ where /// /// # Example /// ```no_run - /// # use rodio::speakers::{SpeakersBuilder, OutputConfig}; + /// # use rodio::speakers::{SpeakersBuilder, OutputConfig, BufferSize}; /// # 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), + /// buffer_size: BufferSize::FrameCount(4096), /// sample_format: cpal::SampleFormat::U16, /// }; /// let builder = SpeakersBuilder::new() @@ -404,7 +408,7 @@ where /// let builder = SpeakersBuilder::new() /// .default_device()? /// .default_config()? - /// // We want mono, if thats not possible give + /// // We want mono, if that's not possible give /// // us the lowest channel count /// .prefer_channel_counts([ /// 1.try_into().expect("not zero"), @@ -424,9 +428,11 @@ where /// 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. + /// Note: You probably want to use [`SpeakersBuilder::try_buffer_duration`] + /// + /// Larger buffer sizes will increase the maximum latency. A too small + /// buffer can lead to audio artifacts if your program can not get samples + /// into the buffer at a consistent pace. /// /// Normally the default output config will have this set up correctly. /// @@ -441,10 +447,10 @@ where /// ``` pub fn try_buffer_size( &self, - buffer_size: u32, + frame_count: u32, ) -> Result, Error> { let mut new_config = self.config.expect("ConfigIsSet"); - new_config.buffer_size = cpal::BufferSize::Fixed(buffer_size); + new_config.buffer_size = BufferSize::FrameCount(frame_count); self.check_config(&new_config)?; Ok(SpeakersBuilder { @@ -459,6 +465,8 @@ where /// See the docs of [`try_buffer_size`](SpeakersBuilder::try_buffer_size) /// for more. /// + /// Note: You probably want to use [`SpeakersBuilder::prefer_buffer_durations`] + /// /// 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. @@ -491,12 +499,12 @@ where /// ``` pub fn prefer_buffer_sizes( &self, - buffer_sizes: impl IntoIterator, + frame_counts: impl IntoIterator, ) -> SpeakersBuilder { - let buffer_sizes = buffer_sizes.into_iter().take_while(|size| *size < 100_000); + let frame_counts = frame_counts.into_iter().take_while(|size| *size < 100_000); - self.set_preferred_if_supported(buffer_sizes, |config, size| { - config.buffer_size = cpal::BufferSize::Fixed(size) + self.set_preferred_if_supported(frame_counts, |config, frame_count| { + config.buffer_size = BufferSize::FrameCount(frame_count) }) } } diff --git a/src/speakers/builder/buffer_duration.rs b/src/speakers/builder/buffer_duration.rs new file mode 100644 index 00000000..79bad4ef --- /dev/null +++ b/src/speakers/builder/buffer_duration.rs @@ -0,0 +1,195 @@ +use std::marker::PhantomData; +use std::ops::{Range, RangeFrom, RangeTo}; +use std::time::Duration; + +use cpal::traits::DeviceTrait; + +use crate::speakers::builder::Error; +use crate::speakers::BufferSize; + +use super::SpeakersBuilder; +use super::{ConfigIsSet, DeviceIsSet}; + +impl SpeakersBuilder +where + E: FnMut(cpal::StreamError) + Send + Clone + 'static, +{ + /// Sets the buffer duration for the output. The buffer size is calculated + /// from this and the sample rate and channel count when we build the + /// output. Prefer this to [`SpeakersBuilder::try_buffer_size`]. + /// + /// Long buffers will cause noticeable latency. A buffer that is too short + /// however leads to audio artifacts when your machine can not generate + /// a buffer of samples on time. + /// + /// Normally the default output config will have this set up correctly. You + /// may want to tweak this to get lower latency or compensate for a + /// inconsistent audio pipeline. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// # use std::time::Duration; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// .try_buffer_duration(Duration::from_millis(20))?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_buffer_duration( + &self, + duration: Duration, + ) -> Result, Error> { + let mut new_config = self.config.expect("ConfigIsSet"); + new_config.buffer_size = BufferSize::Duration(duration); + 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_duration`](SpeakersBuilder::try_buffer_duration) + /// for more. + /// + /// Try multiple buffer durations, fall back to the default if non match. The + /// buffer durations are in order of preference. If the first can be supported + /// the second will never be tried. + /// + /// # Note + /// We will not try buffer durations longer then ten seconds to prevent this + /// from hanging too long on open ranges. + /// + /// # Example + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// # use std::time::Duration; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// .prefer_buffer_durations([ + /// Duration::from_millis(10), + /// Duration::from_millis(50), + /// ]); + /// # Ok::<(), Box>(()) + /// ``` + /// + /// Get the smallest buffer that holds more then 10 ms of audio. + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// # use std::time::Duration; + /// let builder = SpeakersBuilder::new() + /// .default_device()? + /// .default_config()? + /// .prefer_buffer_durations(Duration::from_millis(10)..); + /// # Ok::<(), Box>(()) + /// ``` + pub fn prefer_buffer_durations( + &self, + durations: impl IntoBufferSizeRange, + ) -> Result, Error> { + let mut config = self.config.expect("ConfigIsSet"); + + let (mut found_min, mut found_max) = (None, None); + let (device, supported_configs) = self.device.as_ref().expect("DeviceIsSet"); + for supported in supported_configs { + if config.channel_count.get() != supported.channels() + || config.sample_format != supported.sample_format() + || !(supported.min_sample_rate()..=supported.max_sample_rate()) + .contains(&config.sample_rate.get()) + { + continue; + } + + if let cpal::SupportedBufferSize::Range { min, max } = supported.buffer_size() { + found_min = found_min.min(Some(*min)); + found_max = found_max.max(Some(*max)); + }; + } + + // Sometimes an OS reports a crazy maximum that does not actually works + // (we've spotted u32::MAX in the wild) but it will happily try and + // break. Thus limit the buffer size to something sensible. + let (min, max) = ( + found_min.unwrap_or(1), + (found_max.unwrap_or(u32::MAX)).min(16384), + ); + let min = Duration::from_secs_f64(min as f64 / config.sample_rate.get() as f64); + let max = Duration::from_secs_f64(max as f64 / config.sample_rate.get() as f64); + let supported = min..=max; + + use BufferSizeRange as B; + let buffer_size = match &durations.into_buffer_size_range() { + B::RangeFrom(RangeFrom { start }) if supported.contains(start) => Some(start), + B::RangeFrom(RangeFrom { .. }) => Some(supported.start()), + B::RangeTo(RangeTo { end }) if supported.start() > end => None, + B::RangeTo(RangeTo { .. }) => Some(supported.start()), + B::Range(Range { start, .. }) if supported.contains(start) => Some(start), + B::Range(Range { end, .. }) if supported.contains(end) => Some(supported.start()), + B::Range(Range { .. }) => None, + B::Iter(durations) => durations.iter().find(|d| supported.contains(d)), + } + .copied() + .ok_or(Error::UnsupportedByDevice { + device_name: device + .description() + .map_or("unknown".to_string(), |d| d.name().to_string()), + })?; + + config.buffer_size = BufferSize::Duration(buffer_size); + Ok(SpeakersBuilder { + device: self.device.clone(), + config: Some(config), + error_callback: self.error_callback.clone(), + device_set: PhantomData, + config_set: PhantomData, + }) + } +} + +pub enum BufferSizeRange { + RangeFrom(RangeFrom), + RangeTo(RangeTo), + Range(Range), + Iter(Vec), +} + +pub trait IntoBufferSizeRange { + fn into_buffer_size_range(self) -> BufferSizeRange; +} + +impl IntoBufferSizeRange for RangeFrom { + fn into_buffer_size_range(self) -> BufferSizeRange { + BufferSizeRange::RangeFrom(self) + } +} +impl IntoBufferSizeRange for std::ops::Range { + fn into_buffer_size_range(self) -> BufferSizeRange { + BufferSizeRange::Range(self) + } +} +impl IntoBufferSizeRange for std::ops::RangeTo { + fn into_buffer_size_range(self) -> BufferSizeRange { + BufferSizeRange::RangeTo(self) + } +} +impl IntoBufferSizeRange for [Duration; N] { + fn into_buffer_size_range(self) -> BufferSizeRange { + BufferSizeRange::Iter(self.to_vec()) + } +} + +impl IntoBufferSizeRange for Vec { + fn into_buffer_size_range(self) -> BufferSizeRange { + BufferSizeRange::Iter(self) + } +} +impl IntoBufferSizeRange for Duration { + fn into_buffer_size_range(self) -> BufferSizeRange { + BufferSizeRange::Iter(vec![self]) + } +} diff --git a/src/speakers/config.rs b/src/speakers/config.rs index bd691643..5976038e 100644 --- a/src/speakers/config.rs +++ b/src/speakers/config.rs @@ -1,7 +1,34 @@ use std::num::NonZero; +use std::time::Duration; use crate::{math::nz, stream::DeviceSinkConfig, ChannelCount, SampleRate}; +/// The size of the buffer used by the OS +#[derive(Debug, Copy, Clone)] +pub enum BufferSize { + /// Make the the buffer size such that is holds this duration of audio + Duration(Duration), + /// Make the buffer size so that it holds this many frames + FrameCount(u32), +} + +impl Default for BufferSize { + fn default() -> Self { + Self::Duration(Duration::from_millis(50)) + } +} + +impl BufferSize { + pub(crate) fn frame_count(&self, sample_rate: SampleRate) -> u32 { + match self { + BufferSize::Duration(duration) => { + (duration.as_secs_f64() * sample_rate.get() as f64) as u32 + } + BufferSize::FrameCount(frames) => *frames, + } + } +} + /// Describes the output stream's configuration #[derive(Copy, Clone, Debug)] pub struct OutputConfig { @@ -9,26 +36,23 @@ pub struct OutputConfig { 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 buffer size, see a thorough explanation in SpeakerBuilder::with_buffer_size + pub buffer_size: 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 { + fn buffer_size_frames(&self) -> u32 { + self.buffer_size.frame_count(self.sample_rate) + } + 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) + let buffer_ok = match supported.buffer_size() { + cpal::SupportedBufferSize::Range { min, max } => { + (min..=max).contains(&&self.buffer_size_frames()) } + cpal::SupportedBufferSize::Unknown => true, }; buffer_ok @@ -48,7 +72,7 @@ impl OutputConfig { DeviceSinkConfig { channel_count: self.channel_count, sample_rate: self.sample_rate, - buffer_size: self.buffer_size, + buffer_size: cpal::BufferSize::Fixed(self.buffer_size_frames()), sample_format: self.sample_format, } } @@ -56,15 +80,24 @@ impl OutputConfig { impl From for OutputConfig { fn from(value: cpal::SupportedStreamConfig) -> Self { + use cpal::SupportedBufferSize as B; + + let sample_rate = + NonZero::new(value.sample_rate()).expect("A supported config produces samples"); + let default_frames = BufferSize::default().frame_count(sample_rate); let buffer_size = match value.buffer_size() { - cpal::SupportedBufferSize::Range { .. } => cpal::BufferSize::Default, - cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, + B::Range { min, max } if (min..=max).contains(&&default_frames) => { + BufferSize::default() + } + B::Unknown => BufferSize::default(), + B::Range { min, .. } if default_frames < *min => BufferSize::FrameCount(*min), + // default_frames > max + B::Range { max, .. } => BufferSize::FrameCount(*max), }; Self { channel_count: NonZero::new(value.channels()) .expect("A supported config never has 0 channels"), - sample_rate: NonZero::new(value.sample_rate()) - .expect("A supported config produces samples"), + sample_rate, buffer_size, sample_format: value.sample_format(), } @@ -76,7 +109,7 @@ impl Default for OutputConfig { Self { channel_count: nz!(1), sample_rate: nz!(44_100), - buffer_size: cpal::BufferSize::Default, + buffer_size: BufferSize::default(), sample_format: cpal::SampleFormat::F32, } } diff --git a/src/stream.rs b/src/stream.rs index 7b2f84b2..cd11f83b 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -187,9 +187,23 @@ impl DeviceSinkBuilder { .default_output_config() .map_err(DeviceSinkError::DefaultSinkConfigError)?; - Ok(Self::default() + let mut device = Self::default() .with_device(device) - .with_supported_config(&default_config)) + .with_supported_config(&default_config); + + // 50 ms of audio + let safe_buffer_size = + device.config.sample_rate().get() * device.config.channel_count().get() as u32 / 20; + let safe_buffer_size = safe_buffer_size.next_power_of_two(); + + // This is suboptimal, the builder might still change the sample rate or + // channel count which would throw the buffer size off. We have fixed + // that in the new speakers API, which will eventually replace this. + device.config.buffer_size = match device.config.buffer_size { + BufferSize::Default => BufferSize::Fixed(safe_buffer_size), + fixed @ BufferSize::Fixed(_) => fixed, + }; + Ok(device) } /// Sets default OS-Sink parameters for default output audio device. From 5c807d41c91b06ab04ec7f9290d32af0076ed454 Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 28 Feb 2026 17:32:05 +0100 Subject: [PATCH 2/2] lower safe buffer size min duration to 40ms --- src/stream.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/stream.rs b/src/stream.rs index cd11f83b..fbb522c2 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -191,10 +191,9 @@ impl DeviceSinkBuilder { .with_device(device) .with_supported_config(&default_config); - // 50 ms of audio - let safe_buffer_size = - device.config.sample_rate().get() * device.config.channel_count().get() as u32 / 20; - let safe_buffer_size = safe_buffer_size.next_power_of_two(); + // minimum 40ms of audio + let sample_rate = device.config.sample_rate().get(); + let safe_buffer_size = (sample_rate / (1000 / 40)).next_power_of_two(); // This is suboptimal, the builder might still change the sample rate or // channel count which would throw the buffer size off. We have fixed