From c6af26eff490e22e7268fea2dbc152a0f1e4f678 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:09:28 +0900 Subject: [PATCH 1/9] Implement buffer_size_range for pipewire, alsa, and wasapi & update example --- examples/set_buffer_size.rs | 106 +++++++++++++++++++++----------- src/backends/alsa/device.rs | 11 +++- src/backends/pipewire/device.rs | 22 ++++++- src/backends/pipewire/mod.rs | 2 + src/backends/pipewire/stream.rs | 1 - src/backends/wasapi/device.rs | 64 ++++++++++++++++--- src/backends/wasapi/stream.rs | 2 +- src/lib.rs | 2 - 8 files changed, 159 insertions(+), 51 deletions(-) diff --git a/examples/set_buffer_size.rs b/examples/set_buffer_size.rs index 7e86ecd..69cd27a 100644 --- a/examples/set_buffer_size.rs +++ b/examples/set_buffer_size.rs @@ -1,47 +1,86 @@ -//! This example demonstrates how to request a specific buffer size from the CoreAudio backend. -//! Probably only works on macOS. +//! This example demonstrates how to request a specific buffer size from the audio backends. + +use interflow::channel_map::{ChannelMap32, CreateBitset}; +use interflow::prelude::*; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use util::sine::SineWave; mod util; -#[cfg(os_coreaudio)] fn main() -> anyhow::Result<()> { - use interflow::backends::coreaudio::CoreAudioDriver; - use interflow::channel_map::{ChannelMap32, CreateBitset}; - use interflow::prelude::*; - use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }; - use util::sine::SineWave; - - struct MyCallback { - first_callback: Arc, - sine_wave: SineWave, + #[cfg(target_os = "macos")] + { + use interflow::backends::coreaudio::CoreAudioDriver; + run(CoreAudioDriver) + } + #[cfg(target_os = "windows")] + { + use interflow::backends::wasapi::WasapiDriver; + run(WasapiDriver::default()) } + #[cfg(all(target_os = "linux", not(feature = "pipewire")))] + { + use interflow::backends::alsa::AlsaDriver; + run(AlsaDriver::default()) + } + #[cfg(all(target_os = "linux", feature = "pipewire"))] + { + use interflow::backends::pipewire::PipewireDriver; + run(PipewireDriver::new()?) + } + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + println!("This example is not available on this platform."); + Ok(()) + } +} - impl AudioOutputCallback for MyCallback { - fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { - if self.first_callback.swap(false, Ordering::SeqCst) { - println!( - "Actual buffer size granted by OS: {}", - output.buffer.num_samples() - ); - } +struct MyCallback { + first_callback: Arc, + sine_wave: SineWave, +} + +impl AudioOutputCallback for MyCallback { + fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { + if self.first_callback.swap(false, Ordering::SeqCst) { + println!( + "Actual buffer size granted by OS: {}", + output.buffer.num_samples() + ); + } - for mut frame in output.buffer.as_interleaved_mut().rows_mut() { - let sample = self - .sine_wave - .next_sample(context.stream_config.samplerate as f32); - for channel_sample in &mut frame { - *channel_sample = sample; - } + for mut frame in output.buffer.as_interleaved_mut().rows_mut() { + let sample = self + .sine_wave + .next_sample(context.stream_config.samplerate as f32); + for channel_sample in &mut frame { + *channel_sample = sample; } } } +} +fn run(driver: D) -> anyhow::Result<()> +where + D: AudioDriver, + D::Device: AudioOutputDevice, + ::Error: std::error::Error + Send + Sync + 'static, + ::StreamHandle: AudioStreamHandle, + <::StreamHandle as AudioStreamHandle>::Error: + std::error::Error + Send + Sync + 'static, +{ env_logger::init(); - let driver = CoreAudioDriver; + let full_backend_name = std::any::type_name::(); + let backend_name = full_backend_name + .split("::") + .last() + .unwrap_or(full_backend_name); + println!("Using backend: {}", backend_name); + let device = driver .default_device(DeviceType::OUTPUT) .expect("Failed to query for default output device") @@ -80,8 +119,3 @@ fn main() -> anyhow::Result<()> { stream.eject()?; Ok(()) } - -#[cfg(not(os_coreaudio))] -fn main() { - println!("This example is only available on platforms that support CoreAudio (e.g. macOS)."); -} diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index 509b3b8..0e06468 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -57,6 +57,15 @@ impl AudioDevice for AlsaDevice { log::info!("TODO: enumerate configurations"); None::<[StreamConfig; 0]> } + + fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { + // Query ALSA hardware parameters for the buffer size range. + let hwp = pcm::HwParams::any(&self.pcm)?; + Ok(( + Some(hwp.get_buffer_size_min()? as usize), + Some(hwp.get_buffer_size_max()? as usize), + )) + } } impl AudioInputDevice for AlsaDevice { @@ -153,7 +162,7 @@ impl AlsaDevice { Ok(StreamConfig { samplerate: samplerate as _, channels, - buffer_size_range: (None, None), + buffer_size_range: self.buffer_size_range()?, exclusive: false, }) } diff --git a/src/backends/pipewire/device.rs b/src/backends/pipewire/device.rs index 18c9bef..76779c8 100644 --- a/src/backends/pipewire/device.rs +++ b/src/backends/pipewire/device.rs @@ -67,6 +67,24 @@ impl AudioDevice for PipewireDevice { fn enumerate_configurations(&self) -> Option> { Some([]) } + + fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { + // The buffer size range is stored in a string property from the ALSA compatibility API. + Ok(self + .properties()? + .map(|props| { + props + .get("api.alsa.period-size-range") + .map(|range_str| { + let mut parts = range_str.split_whitespace(); + let min = parts.next().and_then(|s| s.parse().ok()); + let max = parts.next().and_then(|s| s.parse().ok()); + (min, max) + }) + .unwrap_or((None, None)) + }) + .unwrap_or((None, None))) + } } impl AudioInputDevice for PipewireDevice { @@ -77,7 +95,7 @@ impl AudioInputDevice for PipewireDevice { samplerate: 48000.0, channels: 0b11, exclusive: false, - buffer_size_range: (None, None), + buffer_size_range: self.buffer_size_range()?, }) } @@ -104,7 +122,7 @@ impl AudioOutputDevice for PipewireDevice { samplerate: 48000.0, channels: 0b11, exclusive: false, - buffer_size_range: (None, None), + buffer_size_range: self.buffer_size_range()?, }) } diff --git a/src/backends/pipewire/mod.rs b/src/backends/pipewire/mod.rs index f571420..c871392 100644 --- a/src/backends/pipewire/mod.rs +++ b/src/backends/pipewire/mod.rs @@ -3,3 +3,5 @@ pub mod driver; pub mod error; pub mod stream; mod utils; + +pub use driver::PipewireDriver; diff --git a/src/backends/pipewire/stream.rs b/src/backends/pipewire/stream.rs index d24cd41..b74dfe7 100644 --- a/src/backends/pipewire/stream.rs +++ b/src/backends/pipewire/stream.rs @@ -9,7 +9,6 @@ use crate::{ use libspa::buffer::Data; use libspa::param::audio::{AudioFormat, AudioInfoRaw}; use libspa::pod::Pod; -use libspa::utils::Direction; use libspa_sys::{SPA_PARAM_EnumFormat, SPA_TYPE_OBJECT_Format}; use pipewire::context::Context; use pipewire::keys; diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index ebc8d68..82f59f6 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -7,6 +7,7 @@ use crate::{ Channel, DeviceType, StreamConfig, }; use std::borrow::Cow; +use windows::core::Interface; use windows::Win32::Media::Audio; /// Type of devices available from the WASAPI driver. @@ -54,6 +55,59 @@ impl AudioDevice for WasapiDevice { fn enumerate_configurations(&self) -> Option> { None::<[StreamConfig; 0]> } + + fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { + let audio_client = self.device.activate::()?; + let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; + let samplerate = format.nSamplesPerSec; + + // Attempt IAudioClient3/IAudioClient2 to get the buffer size range. + if let Ok(client) = audio_client.cast::() { + let mut min_buffer_duration = 0; + let mut max_buffer_duration = 0; + // Based on the stream implementation, we assume event driven mode. + let event_driven = true; + unsafe { + client.GetBufferSizeLimits( + &format, + event_driven.into(), + &mut min_buffer_duration, + &mut max_buffer_duration, + )?; + } + // Convert from 100-nanosecond units to frames. + let to_frames = |period| (period as u64 * samplerate as u64 / 10_000_000) as usize; + return Ok(( + Some(to_frames(min_buffer_duration)), + Some(to_frames(max_buffer_duration)), + )); + } + if let Ok(client) = audio_client.cast::() { + let mut min_buffer_duration = 0; + let mut max_buffer_duration = 0; + let event_driven = true; + unsafe { + client.GetBufferSizeLimits( + &format, + event_driven.into(), + &mut min_buffer_duration, + &mut max_buffer_duration, + )?; + } + let to_frames = |period| (period as u64 * samplerate as u64 / 10_000_000) as usize; + return Ok(( + Some(to_frames(min_buffer_duration)), + Some(to_frames(max_buffer_duration)), + )); + } + + // Fallback to GetBufferSize for older WASAPI versions. + let frame_size = unsafe { audio_client.GetBufferSize() }.ok(); + Ok(( + frame_size.map(|v| v as usize), + frame_size.map(|v| v as usize), + )) + } } impl AudioInputDevice for WasapiDevice { @@ -62,14 +116,11 @@ impl AudioInputDevice for WasapiDevice { fn default_input_config(&self) -> Result { let audio_client = self.device.activate::()?; let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; - let frame_size = unsafe { audio_client.GetBufferSize() } - .map(|i| i as usize) - .ok(); Ok(StreamConfig { channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, samplerate: format.nSamplesPerSec as _, - buffer_size_range: (frame_size, frame_size), + buffer_size_range: self.buffer_size_range()?, }) } @@ -92,14 +143,11 @@ impl AudioOutputDevice for WasapiDevice { fn default_output_config(&self) -> Result { let audio_client = self.device.activate::()?; let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; - let frame_size = unsafe { audio_client.GetBufferSize() } - .map(|i| i as usize) - .ok(); Ok(StreamConfig { channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, samplerate: format.nSamplesPerSec as _, - buffer_size_range: (frame_size, frame_size), + buffer_size_range: self.buffer_size_range()?, }) } diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs index 815851e..cde9c2e 100644 --- a/src/backends/wasapi/stream.rs +++ b/src/backends/wasapi/stream.rs @@ -483,7 +483,7 @@ pub(crate) fn is_output_config_supported( device: WasapiMMDevice, stream_config: &StreamConfig, ) -> bool { - let mut try_ = || unsafe { + let try_ = || unsafe { let audio_client: Audio::IAudioClient = device.activate()?; let sharemode = if stream_config.exclusive { Audio::AUDCLNT_SHAREMODE_EXCLUSIVE diff --git a/src/lib.rs b/src/lib.rs index d76ad36..ff15c34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,6 @@ use bitflags::bitflags; use std::borrow::Cow; -use std::fmt; -use std::fmt::Formatter; use crate::audio_buffer::{AudioMut, AudioRef}; use crate::channel_map::ChannelMap32; From aa4befcb9745a3f1caf33c8dadb5ab92d6f4f53a Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:20:02 +0900 Subject: [PATCH 2/9] Check the format & samplerate can be used --- src/backends/wasapi/device.rs | 46 ++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index 82f59f6..7a0cecf 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -7,8 +7,10 @@ use crate::{ Channel, DeviceType, StreamConfig, }; use std::borrow::Cow; +use std::ptr; use windows::core::Interface; use windows::Win32::Media::Audio; +use windows::Win32::System::Com::CoTaskMemFree; /// Type of devices available from the WASAPI driver. #[derive(Debug, Clone)] @@ -58,8 +60,44 @@ impl AudioDevice for WasapiDevice { fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { let audio_client = self.device.activate::()?; - let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; - let samplerate = format.nSamplesPerSec; + + // A local RAII wrapper to ensure CoTaskMemFree is always called. + struct ComWaveFormat(*mut Audio::WAVEFORMATEX); + impl Drop for ComWaveFormat { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { CoTaskMemFree(Some(self.0 as *const _)) }; + } + } + } + + let format_to_use = (|| unsafe { + // Get the mix format, now managed by our RAII wrapper. + let format_ptr = ComWaveFormat(audio_client.GetMixFormat()?); + + let mut closest_match_ptr: *mut Audio::WAVEFORMATEX = ptr::null_mut(); + let res = audio_client.IsFormatSupported( + Audio::AUDCLNT_SHAREMODE_SHARED, + &*format_ptr.0, + Some(&mut closest_match_ptr), + ); + + if res.is_ok() { + // The original format is supported. + return Ok(format_ptr.0.read_unaligned()); + } + + // Wrap the returned suggestion in our RAII struct as well. + let closest_match = ComWaveFormat(closest_match_ptr); + if !closest_match.0.is_null() { + return Ok(closest_match.0.read_unaligned()); + } + + res?; + unreachable!(); + })()?; + + let samplerate = format_to_use.nSamplesPerSec; // Attempt IAudioClient3/IAudioClient2 to get the buffer size range. if let Ok(client) = audio_client.cast::() { @@ -69,7 +107,7 @@ impl AudioDevice for WasapiDevice { let event_driven = true; unsafe { client.GetBufferSizeLimits( - &format, + &format_to_use, event_driven.into(), &mut min_buffer_duration, &mut max_buffer_duration, @@ -88,7 +126,7 @@ impl AudioDevice for WasapiDevice { let event_driven = true; unsafe { client.GetBufferSizeLimits( - &format, + &format_to_use, event_driven.into(), &mut min_buffer_duration, &mut max_buffer_duration, From 8e5586c0ec97c5c3f0cf0ae622b036f1fcb240d7 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:25:30 +0900 Subject: [PATCH 3/9] Small fix --- src/backends/wasapi/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index 7a0cecf..4d31eda 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -93,7 +93,7 @@ impl AudioDevice for WasapiDevice { return Ok(closest_match.0.read_unaligned()); } - res?; + res.ok()?; unreachable!(); })()?; From 6ad410200c39d6c9d8441c68aa6dcc74bede999c Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:39:38 +0900 Subject: [PATCH 4/9] Return vals for the format --- src/backends/wasapi/device.rs | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index 4d31eda..35d6e7a 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -71,30 +71,32 @@ impl AudioDevice for WasapiDevice { } } - let format_to_use = (|| unsafe { - // Get the mix format, now managed by our RAII wrapper. - let format_ptr = ComWaveFormat(audio_client.GetMixFormat()?); - - let mut closest_match_ptr: *mut Audio::WAVEFORMATEX = ptr::null_mut(); - let res = audio_client.IsFormatSupported( - Audio::AUDCLNT_SHAREMODE_SHARED, - &*format_ptr.0, - Some(&mut closest_match_ptr), - ); - - if res.is_ok() { - // The original format is supported. - return Ok(format_ptr.0.read_unaligned()); - } + let format_to_use = (|| -> Result { + unsafe { + // Get the mix format, now managed by our RAII wrapper. + let format_ptr = ComWaveFormat(audio_client.GetMixFormat()?); + + let mut closest_match_ptr: *mut Audio::WAVEFORMATEX = ptr::null_mut(); + let res = audio_client.IsFormatSupported( + Audio::AUDCLNT_SHAREMODE_SHARED, + &*format_ptr.0, + Some(&mut closest_match_ptr), + ); + + if res.is_ok() { + // The original format is supported. + return Ok(format_ptr.0.read_unaligned()); + } - // Wrap the returned suggestion in our RAII struct as well. - let closest_match = ComWaveFormat(closest_match_ptr); - if !closest_match.0.is_null() { - return Ok(closest_match.0.read_unaligned()); - } + // Wrap the returned suggestion in our RAII struct as well. + let closest_match = ComWaveFormat(closest_match_ptr); + if !closest_match.0.is_null() { + return Ok(closest_match.0.read_unaligned()); + } - res.ok()?; - unreachable!(); + res.ok()?; + unreachable!(); + } })()?; let samplerate = format_to_use.nSamplesPerSec; From 0c5585f61f39fb09408e11128dce428b495d5848 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:25:25 +0900 Subject: [PATCH 5/9] Quantum --- src/backends/pipewire/device.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/backends/pipewire/device.rs b/src/backends/pipewire/device.rs index 76779c8..955951d 100644 --- a/src/backends/pipewire/device.rs +++ b/src/backends/pipewire/device.rs @@ -73,15 +73,13 @@ impl AudioDevice for PipewireDevice { Ok(self .properties()? .map(|props| { - props - .get("api.alsa.period-size-range") - .map(|range_str| { - let mut parts = range_str.split_whitespace(); - let min = parts.next().and_then(|s| s.parse().ok()); - let max = parts.next().and_then(|s| s.parse().ok()); - (min, max) - }) - .unwrap_or((None, None)) + let min = props + .get("default.clock.min-quantum") + .and_then(|s| s.parse().ok()); + let max = props + .get("default.clock.max-quantum") + .and_then(|s| s.parse().ok()); + (min, max) }) .unwrap_or((None, None))) } From 3b282d8a9066cd1ae75f80161f7f2a3fe6c45ce1 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:51:10 +0900 Subject: [PATCH 6/9] Alsa tweak --- src/backends/alsa/device.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index 0e06468..1c935cf 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -125,12 +125,12 @@ impl AlsaDevice { let hwp = pcm::HwParams::any(&self.pcm)?; hwp.set_channels(config.channels as _)?; hwp.set_rate(config.samplerate as _, alsa::ValueOr::Nearest)?; - if let Some(min) = config.buffer_size_range.0 { - hwp.set_buffer_size_min(min as _)?; - } - if let Some(max) = config.buffer_size_range.1 { - hwp.set_buffer_size_max(max as _)?; + + // Prefer the min buffer size, otherwise fall back to the max. + if let Some(buffer_size) = config.buffer_size_range.0.or(config.buffer_size_range.1) { + hwp.set_buffer_size_near(buffer_size as _)?; } + hwp.set_format(pcm::Format::float())?; hwp.set_access(pcm::Access::RWInterleaved)?; Ok(hwp) From 614fcbb877583f8f3dd6d240c16963819891aa96 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:52:56 +0900 Subject: [PATCH 7/9] And set period size? --- src/backends/alsa/device.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index 1c935cf..a020ed9 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -128,7 +128,8 @@ impl AlsaDevice { // Prefer the min buffer size, otherwise fall back to the max. if let Some(buffer_size) = config.buffer_size_range.0.or(config.buffer_size_range.1) { - hwp.set_buffer_size_near(buffer_size as _)?; + let buffer_size = hwp.set_buffer_size_near(buffer_size as _)?; + hwp.set_period_size_near(buffer_size / 2, alsa::ValueOr::Nearest)?; } hwp.set_format(pcm::Format::float())?; From 8b2105bc5e8c9ce4aaba13d4f15db8f45cbbb3c6 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:56:05 +0900 Subject: [PATCH 8/9] Set buffer *after* period --- src/backends/alsa/device.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index a020ed9..9db4223 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -128,8 +128,10 @@ impl AlsaDevice { // Prefer the min buffer size, otherwise fall back to the max. if let Some(buffer_size) = config.buffer_size_range.0.or(config.buffer_size_range.1) { - let buffer_size = hwp.set_buffer_size_near(buffer_size as _)?; - hwp.set_period_size_near(buffer_size / 2, alsa::ValueOr::Nearest)?; + // Set the period size first, to half the desired buffer size. + hwp.set_period_size_near((buffer_size / 2) as i64, alsa::ValueOr::Nearest)?; + // Then, set the buffer size. + hwp.set_buffer_size_near(buffer_size as i64)?; } hwp.set_format(pcm::Format::float())?; From bf298d9052a227e64f5c678a41ac68eed5f67505 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Wed, 13 Aug 2025 19:15:07 +0900 Subject: [PATCH 9/9] Revert WASAPI --- examples/set_buffer_size.rs | 23 +++++-- src/backends/alsa/device.rs | 11 ++-- src/backends/alsa/stream.rs | 6 +- src/backends/coreaudio.rs | 2 +- src/backends/pipewire/device.rs | 2 +- src/backends/wasapi/device.rs | 104 +++----------------------------- src/backends/wasapi/stream.rs | 2 +- 7 files changed, 38 insertions(+), 112 deletions(-) diff --git a/examples/set_buffer_size.rs b/examples/set_buffer_size.rs index 69cd27a..7932dbb 100644 --- a/examples/set_buffer_size.rs +++ b/examples/set_buffer_size.rs @@ -46,8 +46,11 @@ struct MyCallback { impl AudioOutputCallback for MyCallback { fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { if self.first_callback.swap(false, Ordering::SeqCst) { + let (min_buf, max_buf) = context.stream_config.buffer_size_range; + let buffer_size = min_buf.unwrap_or(0); + println!("Actual buffer size granted by OS: {}", buffer_size); println!( - "Actual buffer size granted by OS: {}", + "Period size (frames per callback): {}", output.buffer.num_samples() ); } @@ -81,10 +84,20 @@ where .unwrap_or(full_backend_name); println!("Using backend: {}", backend_name); - let device = driver + let mut device = driver .default_device(DeviceType::OUTPUT) - .expect("Failed to query for default output device") - .expect("No default output device found on this system"); + .expect("Failed to query for default output device"); + + if device.is_none() { + println!("No default output device found, falling back to first available device."); + device = driver + .list_devices() + .expect("Failed to query for devices") + .into_iter() + .find(|d| d.device_type().contains(DeviceType::OUTPUT)); + } + + let device = device.expect("No output devices found on this system"); println!("Using device: {}", device.name()); @@ -103,7 +116,7 @@ where samplerate: 48000.0, channels: ChannelMap32::from_indices([0, 1]), buffer_size_range: (Some(requested_buffer_size), Some(requested_buffer_size)), - exclusive: false, + exclusive: true, }; let callback = MyCallback { diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index 9db4223..a15a8e8 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -126,12 +126,9 @@ impl AlsaDevice { hwp.set_channels(config.channels as _)?; hwp.set_rate(config.samplerate as _, alsa::ValueOr::Nearest)?; - // Prefer the min buffer size, otherwise fall back to the max. if let Some(buffer_size) = config.buffer_size_range.0.or(config.buffer_size_range.1) { - // Set the period size first, to half the desired buffer size. - hwp.set_period_size_near((buffer_size / 2) as i64, alsa::ValueOr::Nearest)?; - // Then, set the buffer size. - hwp.set_buffer_size_near(buffer_size as i64)?; + let buffer_size = hwp.set_buffer_size_near(buffer_size as i64)?; + hwp.set_period_size_near(buffer_size / 2, alsa::ValueOr::Nearest)?; } hwp.set_format(pcm::Format::float())?; @@ -151,7 +148,9 @@ impl AlsaDevice { log::debug!("Apply config: hwp {hwp:#?}"); - swp.set_start_threshold(hwp.get_buffer_size()?)?; + let (_, period_size) = self.pcm.get_params()?; + swp.set_avail_min(period_size as i64)?; + swp.set_start_threshold(period_size as i64)?; self.pcm.sw_params(&swp)?; log::debug!("Apply config: swp {swp:#?}"); diff --git a/src/backends/alsa/stream.rs b/src/backends/alsa/stream.rs index ac649c1..11054f7 100644 --- a/src/backends/alsa/stream.rs +++ b/src/backends/alsa/stream.rs @@ -60,8 +60,10 @@ impl AlsaStream { buf }; let (hwp, _, io) = device.apply_config(&stream_config)?; - let (_, period_size) = device.pcm.get_params()?; + let (buffer_size, period_size) = device.pcm.get_params()?; + let buffer_size = buffer_size as usize; let period_size = period_size as usize; + log::info!("Buffer size : {buffer_size}"); log::info!("Period size : {period_size}"); let num_channels = hwp.get_channels()? as usize; log::info!("Num channels: {num_channels}"); @@ -71,7 +73,7 @@ impl AlsaStream { samplerate, channels: ChannelMap32::default() .with_indices(std::iter::repeat(1).take(num_channels)), - buffer_size_range: (Some(period_size), Some(period_size)), + buffer_size_range: (Some(buffer_size), Some(buffer_size)), exclusive: false, }; let mut timestamp = Timestamp::new(samplerate); diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs index 93dd7a6..67747cf 100644 --- a/src/backends/coreaudio.rs +++ b/src/backends/coreaudio.rs @@ -278,7 +278,7 @@ impl AudioInputDevice for CoreAudioDevice { Ok(StreamConfig { channels: 0b11, samplerate, - buffer_size_range: self.buffer_size_range()?, + buffer_size_range: (None, None), exclusive: false, }) } diff --git a/src/backends/pipewire/device.rs b/src/backends/pipewire/device.rs index 955951d..178b421 100644 --- a/src/backends/pipewire/device.rs +++ b/src/backends/pipewire/device.rs @@ -93,7 +93,7 @@ impl AudioInputDevice for PipewireDevice { samplerate: 48000.0, channels: 0b11, exclusive: false, - buffer_size_range: self.buffer_size_range()?, + buffer_size_range: (None, None), }) } diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index 35d6e7a..ebc8d68 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -7,10 +7,7 @@ use crate::{ Channel, DeviceType, StreamConfig, }; use std::borrow::Cow; -use std::ptr; -use windows::core::Interface; use windows::Win32::Media::Audio; -use windows::Win32::System::Com::CoTaskMemFree; /// Type of devices available from the WASAPI driver. #[derive(Debug, Clone)] @@ -57,97 +54,6 @@ impl AudioDevice for WasapiDevice { fn enumerate_configurations(&self) -> Option> { None::<[StreamConfig; 0]> } - - fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { - let audio_client = self.device.activate::()?; - - // A local RAII wrapper to ensure CoTaskMemFree is always called. - struct ComWaveFormat(*mut Audio::WAVEFORMATEX); - impl Drop for ComWaveFormat { - fn drop(&mut self) { - if !self.0.is_null() { - unsafe { CoTaskMemFree(Some(self.0 as *const _)) }; - } - } - } - - let format_to_use = (|| -> Result { - unsafe { - // Get the mix format, now managed by our RAII wrapper. - let format_ptr = ComWaveFormat(audio_client.GetMixFormat()?); - - let mut closest_match_ptr: *mut Audio::WAVEFORMATEX = ptr::null_mut(); - let res = audio_client.IsFormatSupported( - Audio::AUDCLNT_SHAREMODE_SHARED, - &*format_ptr.0, - Some(&mut closest_match_ptr), - ); - - if res.is_ok() { - // The original format is supported. - return Ok(format_ptr.0.read_unaligned()); - } - - // Wrap the returned suggestion in our RAII struct as well. - let closest_match = ComWaveFormat(closest_match_ptr); - if !closest_match.0.is_null() { - return Ok(closest_match.0.read_unaligned()); - } - - res.ok()?; - unreachable!(); - } - })()?; - - let samplerate = format_to_use.nSamplesPerSec; - - // Attempt IAudioClient3/IAudioClient2 to get the buffer size range. - if let Ok(client) = audio_client.cast::() { - let mut min_buffer_duration = 0; - let mut max_buffer_duration = 0; - // Based on the stream implementation, we assume event driven mode. - let event_driven = true; - unsafe { - client.GetBufferSizeLimits( - &format_to_use, - event_driven.into(), - &mut min_buffer_duration, - &mut max_buffer_duration, - )?; - } - // Convert from 100-nanosecond units to frames. - let to_frames = |period| (period as u64 * samplerate as u64 / 10_000_000) as usize; - return Ok(( - Some(to_frames(min_buffer_duration)), - Some(to_frames(max_buffer_duration)), - )); - } - if let Ok(client) = audio_client.cast::() { - let mut min_buffer_duration = 0; - let mut max_buffer_duration = 0; - let event_driven = true; - unsafe { - client.GetBufferSizeLimits( - &format_to_use, - event_driven.into(), - &mut min_buffer_duration, - &mut max_buffer_duration, - )?; - } - let to_frames = |period| (period as u64 * samplerate as u64 / 10_000_000) as usize; - return Ok(( - Some(to_frames(min_buffer_duration)), - Some(to_frames(max_buffer_duration)), - )); - } - - // Fallback to GetBufferSize for older WASAPI versions. - let frame_size = unsafe { audio_client.GetBufferSize() }.ok(); - Ok(( - frame_size.map(|v| v as usize), - frame_size.map(|v| v as usize), - )) - } } impl AudioInputDevice for WasapiDevice { @@ -156,11 +62,14 @@ impl AudioInputDevice for WasapiDevice { fn default_input_config(&self) -> Result { let audio_client = self.device.activate::()?; let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; + let frame_size = unsafe { audio_client.GetBufferSize() } + .map(|i| i as usize) + .ok(); Ok(StreamConfig { channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, samplerate: format.nSamplesPerSec as _, - buffer_size_range: self.buffer_size_range()?, + buffer_size_range: (frame_size, frame_size), }) } @@ -183,11 +92,14 @@ impl AudioOutputDevice for WasapiDevice { fn default_output_config(&self) -> Result { let audio_client = self.device.activate::()?; let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; + let frame_size = unsafe { audio_client.GetBufferSize() } + .map(|i| i as usize) + .ok(); Ok(StreamConfig { channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, samplerate: format.nSamplesPerSec as _, - buffer_size_range: self.buffer_size_range()?, + buffer_size_range: (frame_size, frame_size), }) } diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs index cde9c2e..815851e 100644 --- a/src/backends/wasapi/stream.rs +++ b/src/backends/wasapi/stream.rs @@ -483,7 +483,7 @@ pub(crate) fn is_output_config_supported( device: WasapiMMDevice, stream_config: &StreamConfig, ) -> bool { - let try_ = || unsafe { + let mut try_ = || unsafe { let audio_client: Audio::IAudioClient = device.activate()?; let sharemode = if stream_config.exclusive { Audio::AUDCLNT_SHAREMODE_EXCLUSIVE