From 8cc6f25ff7f584786b19ba62968e8e6d25ccd0c4 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Sat, 13 Sep 2025 23:10:46 -0700 Subject: [PATCH 01/30] feat: add device_id support for macOS to DeviceTrait --- src/error.rs | 25 +++++++++++++++++++++++++ src/host/aaudio/mod.rs | 4 ++++ src/host/alsa/mod.rs | 9 +++++++++ src/host/asio/device.rs | 4 ++++ src/host/asio/mod.rs | 4 ++++ src/host/coreaudio/ios/mod.rs | 11 ++++++++++- src/host/coreaudio/macos/mod.rs | 14 +++++++++----- src/host/emscripten/mod.rs | 9 +++++++++ src/host/jack/device.rs | 4 ++++ src/host/null/mod.rs | 10 ++++++---- src/host/wasapi/device.rs | 8 ++++++++ src/host/webaudio/mod.rs | 10 ++++++++++ src/platform/mod.rs | 9 +++++++++ src/traits.rs | 5 ++++- 14 files changed, 115 insertions(+), 11 deletions(-) diff --git a/src/error.rs b/src/error.rs index 2fed3622b..1a5886793 100644 --- a/src/error.rs +++ b/src/error.rs @@ -65,6 +65,31 @@ impl From for DevicesError { } } +/// An error that may occur while attempting to retrieve a device id. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DeviceIdError { + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, + UnsupportedOS, +} + +impl Display for DeviceIdError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::UnsupportedOS => {f.write_str("Device ids are unsupported for this OS")} + } + } +} + +impl Error for DeviceIdError {} + +impl From for DeviceIdError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + /// An error that may occur while attempting to retrieve a device name. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DeviceNameError { diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index f5f024461..418cad070 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -327,6 +327,10 @@ impl DeviceTrait for Device { } } + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + fn supported_input_configs( &self, ) -> Result { diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index b1a29f348..38b063d3b 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -68,6 +68,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn device_id(&self) -> Result { + Device::device_id(self) + } + fn supported_input_configs( &self, ) -> Result { @@ -307,6 +311,11 @@ impl Device { Ok(self.to_string()) } + #[inline] + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + fn supported_configs( &self, stream_t: alsa::Direction, diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index dd854136e..ae4db9543 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -54,6 +54,10 @@ impl Device { Ok(self.driver.name().to_string()) } + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + /// Gets the supported input configs. /// TODO currently only supports the default. /// Need to find all possible configs. diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 73d936d99..3af353848 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -62,6 +62,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn device_id(&self) -> Result { + Device::device_id(self) + } + fn supported_input_configs( &self, ) -> Result { diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 753bbd099..65b3885f3 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -18,7 +18,7 @@ use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -74,6 +74,10 @@ impl Device { Ok("Default Device".to_owned()) } + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + #[inline] fn supported_input_configs( &self, @@ -140,6 +144,11 @@ impl DeviceTrait for Device { Device::name(self) } + #[inline] + fn device_id(&self) -> Result { + Device::device_id(self) + } + #[inline] fn supported_input_configs( &self, diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index c0809fa2f..314047ea3 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -4,11 +4,7 @@ use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_ use super::OSStatus; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError }; use coreaudio::audio_unit::render_callback::{self, data}; use coreaudio::audio_unit::{AudioUnit, Element, Scope}; @@ -91,6 +87,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn device_id(&self) -> Result{ + Device::id(self) + } + fn supported_input_configs( &self, ) -> Result { @@ -185,6 +185,10 @@ impl Device { }) } + fn id(&self) -> Result { + Ok(self.audio_device_id) + } + // Logic re-used between `supported_input_configs` and `supported_output_configs`. #[allow(clippy::cast_ptr_alignment)] fn supported_configs( diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 4d6b3daf8..66f083f1b 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -69,6 +69,11 @@ impl Device { Ok("Default Device".to_owned()) } + #[inline] + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + #[inline] fn supported_input_configs( &self, @@ -144,6 +149,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn device_id(&self) -> Result { + Device::device_id(self) + } + fn supported_input_configs( &self, ) -> Result { diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 7a911dce4..3ae08c64f 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -146,6 +146,10 @@ impl DeviceTrait for Device { Ok(self.name.clone()) } + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + fn supported_input_configs( &self, ) -> Result { diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 0a9752b70..19a464ebd 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -2,10 +2,7 @@ use std::time::Duration; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, - InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, - StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError }; #[derive(Default)] @@ -47,6 +44,11 @@ impl DeviceTrait for Device { Ok("null".to_owned()) } + #[inline] + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + #[inline] fn supported_input_configs( &self, diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 4e4cf14b8..232c0fa89 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -58,6 +58,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn device_id(&self) -> Result { + Device::device_id(self) + } + fn supports_input(&self) -> bool { self.data_flow() == Audio::eCapture } @@ -321,6 +325,10 @@ impl Device { } } + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + #[inline] fn from_immdevice(device: Audio::IMMDevice) -> Self { Device { diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index fb835b735..2bda974d4 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -84,6 +84,11 @@ impl Device { Ok("Default Device".to_owned()) } + #[inline] + fn device_id(&self) -> Result { + Err(DeviceIdError::UnsupportedOS) + } + #[inline] fn supported_input_configs( &self, @@ -142,6 +147,11 @@ impl DeviceTrait for Device { Device::name(self) } + #[inline] + fn device_id(&self) -> Result { + Device::device_id(self) + } + #[inline] fn supported_input_configs( &self, diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fd12eaaac..783ba4fe6 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -314,6 +314,15 @@ macro_rules! impl_platform_host { } } + fn device_id(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.device_id(), + )* + } + } + fn supports_input(&self) -> bool { match self.0 { $( diff --git a/src/traits.rs b/src/traits.rs index 2f1bd3469..c824e3ae2 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -3,7 +3,7 @@ use std::time::Duration; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -89,6 +89,9 @@ pub trait DeviceTrait { /// The human-readable name of the device. fn name(&self) -> Result; + /// The device-id of the device. (For supported OS's only) + fn device_id(&self) -> Result; + /// True if the device supports audio input, otherwise false fn supports_input(&self) -> bool { self.supported_input_configs() From 9161bbb533009e29e9f323987dcb29fa4f7d63d3 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+xephyris@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:51:02 -0700 Subject: [PATCH 02/30] fix: fix import statements for other APIs --- src/host/aaudio/mod.rs | 2 +- src/host/alsa/mod.rs | 2 +- src/host/asio/device.rs | 1 + src/host/emscripten/mod.rs | 2 +- src/host/jack/device.rs | 2 +- src/host/wasapi/device.rs | 2 +- src/host/webaudio/mod.rs | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 418cad070..366a03522 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -12,7 +12,7 @@ use java_interface::{AudioDeviceDirection, AudioDeviceInfo}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceNameError, DevicesError, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, + DeviceNameError, DeviceIdError, DevicesError, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, OutputStreamTimestamp, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, SizedSample, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 38b063d3b..f64c88be7 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -16,7 +16,7 @@ pub use self::enumerate::{default_input_device, default_output_device, Devices}; use crate::{ traits::{DeviceTrait, HostTrait, StreamTrait}, BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, + DefaultStreamConfigError, DeviceNameError, DeviceIdError, DevicesError, FrameCount, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index ae4db9543..b558628ce 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -5,6 +5,7 @@ use super::sys; use crate::BackendSpecificError; use crate::DefaultStreamConfigError; use crate::DeviceNameError; +use crate::DeviceIdError; use crate::DevicesError; use crate::SampleFormat; use crate::SampleRate; diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 66f083f1b..e6bfd633c 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -7,7 +7,7 @@ use web_sys::AudioContext; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceIdError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 3ae08c64f..a870d8a42 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -1,6 +1,6 @@ use crate::traits::DeviceTrait; use crate::{ - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceIdError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 232c0fa89..7db74cdb7 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -1,6 +1,6 @@ use crate::FrameCount; use crate::{ - BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceNameError, + BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceNameError, DeviceIdError, DevicesError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, COMMON_SAMPLE_RATES, diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 2bda974d4..0802ed344 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -8,7 +8,7 @@ use self::web_sys::{AudioContext, AudioContextOptions}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + DeviceNameError, DeviceIdError,DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; From 845baac311789b74715ac6f259c1ae77d6608311 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Mon, 15 Sep 2025 23:16:08 -0700 Subject: [PATCH 03/30] wip: use DeviceId enum instead to work around conflicting device_id implementations --- src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index efea3a379..c3c299490 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,6 +219,11 @@ where /// The desired number of frames for the hardware buffer. pub type FrameCount = u32; +pub enum DeviceId { + MacOS(u32), + Windows(String), +} + /// The buffer size used by the device. /// /// [`Default`] is used when no specific buffer size is set and uses the default From bd9b0710dbeb3b37a2e68f8f93ed3bd53d17939f Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Tue, 16 Sep 2025 21:41:47 -0700 Subject: [PATCH 04/30] feat: transition to using DeviceId for enum for better cross compatibility --- src/host/aaudio/mod.rs | 6 +++--- src/host/alsa/mod.rs | 8 ++++---- src/host/asio/device.rs | 3 ++- src/host/asio/mod.rs | 6 +++--- src/host/coreaudio/ios/mod.rs | 8 ++++---- src/host/coreaudio/macos/mod.rs | 8 ++++---- src/host/emscripten/mod.rs | 8 ++++---- src/host/jack/device.rs | 4 ++-- src/host/null/mod.rs | 8 ++++++-- src/host/wasapi/device.rs | 8 ++++---- src/host/webaudio/mod.rs | 8 ++++---- src/lib.rs | 1 + src/platform/mod.rs | 4 ++-- src/traits.rs | 7 ++----- 14 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 366a03522..9faf604a4 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -12,7 +12,7 @@ use java_interface::{AudioDeviceDirection, AudioDeviceInfo}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceNameError, DeviceIdError, DevicesError, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, + DeviceNameError, DeviceId, DeviceIdError, DevicesError, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, OutputStreamTimestamp, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, SizedSample, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -210,7 +210,7 @@ fn configure_for_device( config: &StreamConfig, ) -> ndk::audio::AudioStreamBuilder { let mut builder = if let Some(info) = &device.0 { - builder.device_id(info.id) + builder.id(info.id) } else { builder }; @@ -327,7 +327,7 @@ impl DeviceTrait for Device { } } - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index f64c88be7..389408f8c 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -16,7 +16,7 @@ pub use self::enumerate::{default_input_device, default_output_device, Devices}; use crate::{ traits::{DeviceTrait, HostTrait, StreamTrait}, BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceNameError, DeviceIdError, DevicesError, FrameCount, InputCallbackInfo, + DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, DevicesError, FrameCount, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -68,8 +68,8 @@ impl DeviceTrait for Device { Device::name(self) } - fn device_id(&self) -> Result { - Device::device_id(self) + fn id(&self) -> Result { + Device::id(self) } fn supported_input_configs( @@ -312,7 +312,7 @@ impl Device { } #[inline] - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index b558628ce..95a87755a 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -5,6 +5,7 @@ use super::sys; use crate::BackendSpecificError; use crate::DefaultStreamConfigError; use crate::DeviceNameError; +use crate::DeviceId; use crate::DeviceIdError; use crate::DevicesError; use crate::SampleFormat; @@ -55,7 +56,7 @@ impl Device { Ok(self.driver.name().to_string()) } - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 3af353848..34d6fe553 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -2,7 +2,7 @@ extern crate asio_sys as sys; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigsError, }; @@ -62,8 +62,8 @@ impl DeviceTrait for Device { Device::name(self) } - fn device_id(&self) -> Result { - Device::device_id(self) + fn id(&self) -> Result { + Device::id(self) } fn supported_input_configs( diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 65b3885f3..7f587adde 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -18,7 +18,7 @@ use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceIdError, + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -74,7 +74,7 @@ impl Device { Ok("Default Device".to_owned()) } - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } @@ -145,8 +145,8 @@ impl DeviceTrait for Device { } #[inline] - fn device_id(&self) -> Result { - Device::device_id(self) + fn id(&self) -> Result { + Device::id(self) } #[inline] diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index 314047ea3..aa650643e 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -4,7 +4,7 @@ use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_ use super::OSStatus; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError }; use coreaudio::audio_unit::render_callback::{self, data}; use coreaudio::audio_unit::{AudioUnit, Element, Scope}; @@ -87,7 +87,7 @@ impl DeviceTrait for Device { Device::name(self) } - fn device_id(&self) -> Result{ + fn id(&self) -> Result{ Device::id(self) } @@ -185,8 +185,8 @@ impl Device { }) } - fn id(&self) -> Result { - Ok(self.audio_device_id) + fn id(&self) -> Result { + Ok(DeviceId::MacOS(self.audio_device_id)) } // Logic re-used between `supported_input_configs` and `supported_output_configs`. diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index e6bfd633c..2bec598ba 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -7,7 +7,7 @@ use web_sys::AudioContext; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceIdError, DevicesError, + BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -70,7 +70,7 @@ impl Device { } #[inline] - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } @@ -149,8 +149,8 @@ impl DeviceTrait for Device { Device::name(self) } - fn device_id(&self) -> Result { - Device::device_id(self) + fn id(&self) -> Result { + Device::id(self) } fn supported_input_configs( diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index a870d8a42..31fbd4337 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -1,6 +1,6 @@ use crate::traits::DeviceTrait; use crate::{ - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceIdError, + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -146,7 +146,7 @@ impl DeviceTrait for Device { Ok(self.name.clone()) } - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 19a464ebd..9630bc509 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -2,7 +2,11 @@ use std::time::Duration; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError + BuildStreamError, Data, DefaultStreamConfigError, DeviceId, + DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, + OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError }; #[derive(Default)] @@ -45,7 +49,7 @@ impl DeviceTrait for Device { } #[inline] - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 7db74cdb7..47bcd47c8 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -1,6 +1,6 @@ use crate::FrameCount; use crate::{ - BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceNameError, DeviceIdError, + BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, DevicesError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, COMMON_SAMPLE_RATES, @@ -58,8 +58,8 @@ impl DeviceTrait for Device { Device::name(self) } - fn device_id(&self) -> Result { - Device::device_id(self) + fn id(&self) -> Result { + Device::id(self) } fn supports_input(&self) -> bool { @@ -325,7 +325,7 @@ impl Device { } } - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 0802ed344..af030bd9b 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -8,7 +8,7 @@ use self::web_sys::{AudioContext, AudioContextOptions}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceNameError, DeviceIdError,DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + DeviceNameError, DeviceId, DeviceIdError,DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; @@ -85,7 +85,7 @@ impl Device { } #[inline] - fn device_id(&self) -> Result { + fn id(&self) -> Result { Err(DeviceIdError::UnsupportedOS) } @@ -148,8 +148,8 @@ impl DeviceTrait for Device { } #[inline] - fn device_id(&self) -> Result { - Device::device_id(self) + fn id(&self) -> Result { + Device::id(self) } #[inline] diff --git a/src/lib.rs b/src/lib.rs index c3c299490..7ee0465d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,6 +219,7 @@ where /// The desired number of frames for the hardware buffer. pub type FrameCount = u32; +#[derive(Clone, Debug, Eq, PartialEq)] pub enum DeviceId { MacOS(u32), Windows(String), diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 783ba4fe6..503f17e01 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -314,11 +314,11 @@ macro_rules! impl_platform_host { } } - fn device_id(&self) -> Result { + fn id(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? - DeviceInner::$HostVariant(ref d) => d.device_id(), + DeviceInner::$HostVariant(ref d) => d.id(), )* } } diff --git a/src/traits.rs b/src/traits.rs index c824e3ae2..38612670c 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -3,10 +3,7 @@ use std::time::Duration; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceIdError, DeviceNameError, DevicesError, - InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, PauseStreamError, - PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError }; /// A [`Host`] provides access to the available audio devices on the system. @@ -90,7 +87,7 @@ pub trait DeviceTrait { fn name(&self) -> Result; /// The device-id of the device. (For supported OS's only) - fn device_id(&self) -> Result; + fn id(&self) -> Result; /// True if the device supports audio input, otherwise false fn supports_input(&self) -> bool { From 60c3c26c21e179a80278edc46ba275295cd0df23 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:09:51 -0700 Subject: [PATCH 05/30] docs: add description for DeviceId --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 7ee0465d5..0dc9eb89a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,6 +219,8 @@ where /// The desired number of frames for the hardware buffer. pub type FrameCount = u32; +/// The device ID of the audio device, on supported OSs +/// Currently only supports macOS #[derive(Clone, Debug, Eq, PartialEq)] pub enum DeviceId { MacOS(u32), From 2a93a70264b0c8fbb9b2c68a524a1fa4deed9572 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+xephyris@users.noreply.github.com> Date: Fri, 19 Sep 2025 22:01:50 -0700 Subject: [PATCH 06/30] feat: implemented device id for windows wasapi --- src/error.rs | 4 +++- src/host/wasapi/device.rs | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index 1a5886793..05cb5cf52 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,13 +71,15 @@ pub enum DeviceIdError { /// See the [`BackendSpecificError`] docs for more information about this error variant. BackendSpecific { err: BackendSpecificError }, UnsupportedOS, + ParseError, } impl Display for DeviceIdError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::BackendSpecific { err } => err.fmt(f), - Self::UnsupportedOS => {f.write_str("Device ids are unsupported for this OS")} + Self::UnsupportedOS => {f.write_str("Device ids are unsupported for this OS")}, + Self::ParseError => {f.write_str("Failed to parse the device_id")}, } } } diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 47bcd47c8..76e81a0f4 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -326,7 +326,23 @@ impl Device { } fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + unsafe { + match self.device.GetId() { + Ok(pwstr) => { + match pwstr.to_string() { + Ok(id_str) => { + Ok(DeviceId::Windows(id_str)) + }, + Err(_e) => { + Err(DeviceIdError::ParseError) + } + } + }, + Err(e) => { + Err(DeviceIdError::BackendSpecific { err: e.into() }) + } + } + } } #[inline] From fb1efd07d841790d56592f7a39bc4a4178671466 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:22:24 -0700 Subject: [PATCH 07/30] docs: update description --- src/lib.rs | 2 +- src/traits.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0dc9eb89a..01a918a8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -220,7 +220,7 @@ where pub type FrameCount = u32; /// The device ID of the audio device, on supported OSs -/// Currently only supports macOS +/// Currently only supports macOS and Windows (WASAPI) #[derive(Clone, Debug, Eq, PartialEq)] pub enum DeviceId { MacOS(u32), diff --git a/src/traits.rs b/src/traits.rs index 38612670c..9ab0389fb 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -86,7 +86,7 @@ pub trait DeviceTrait { /// The human-readable name of the device. fn name(&self) -> Result; - /// The device-id of the device. (For supported OS's only) + /// The device-id of the device. (For supported OSs only) fn id(&self) -> Result; /// True if the device supports audio input, otherwise false From 626b8230694c7f9e95d4b74b36624515ee955272 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:04:04 -0700 Subject: [PATCH 08/30] fix: reformat existing code and fix android build error --- src/error.rs | 8 +++++--- src/host/aaudio/mod.rs | 10 +++++----- src/host/alsa/mod.rs | 8 ++++---- src/host/asio/device.rs | 2 +- src/host/asio/mod.rs | 6 +++--- src/host/coreaudio/ios/mod.rs | 9 +++++---- src/host/coreaudio/macos/mod.rs | 10 +++++++--- src/host/coreaudio/mod.rs | 5 +---- src/host/emscripten/mod.rs | 8 ++++---- src/host/jack/device.rs | 8 ++++---- src/host/null/mod.rs | 9 ++++----- src/host/wasapi/device.rs | 22 +++++++--------------- src/host/webaudio/mod.rs | 9 +++++---- src/traits.rs | 5 ++++- 14 files changed, 59 insertions(+), 60 deletions(-) diff --git a/src/error.rs b/src/error.rs index 05cb5cf52..7e7de1234 100644 --- a/src/error.rs +++ b/src/error.rs @@ -69,7 +69,9 @@ impl From for DevicesError { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DeviceIdError { /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, + BackendSpecific { + err: BackendSpecificError, + }, UnsupportedOS, ParseError, } @@ -78,8 +80,8 @@ impl Display for DeviceIdError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::BackendSpecific { err } => err.fmt(f), - Self::UnsupportedOS => {f.write_str("Device ids are unsupported for this OS")}, - Self::ParseError => {f.write_str("Failed to parse the device_id")}, + Self::UnsupportedOS => f.write_str("Device ids are unsupported for this OS"), + Self::ParseError => f.write_str("Failed to parse the device_id"), } } } diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 9faf604a4..c14c435a2 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -11,10 +11,10 @@ use java_interface::{AudioDeviceDirection, AudioDeviceInfo}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceNameError, DeviceId, DeviceIdError, DevicesError, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, - OutputStreamTimestamp, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, - SizedSample, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, + DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, InputStreamTimestamp, + OutputCallbackInfo, OutputStreamTimestamp, PauseStreamError, PlayStreamError, SampleFormat, + SampleRate, SizedSample, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; @@ -210,7 +210,7 @@ fn configure_for_device( config: &StreamConfig, ) -> ndk::audio::AudioStreamBuilder { let mut builder = if let Some(info) = &device.0 { - builder.id(info.id) + builder.device_id(info.id) } else { builder }; diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 389408f8c..c1054d4c3 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -16,10 +16,10 @@ pub use self::enumerate::{default_input_device, default_output_device, Devices}; use crate::{ traits::{DeviceTrait, HostTrait, StreamTrait}, BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, DevicesError, FrameCount, InputCallbackInfo, - OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, - StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount, + InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, + SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, }; pub type SupportedInputConfigs = VecIntoIter; diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 95a87755a..bd091223a 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -4,9 +4,9 @@ pub type SupportedOutputConfigs = std::vec::IntoIter use super::sys; use crate::BackendSpecificError; use crate::DefaultStreamConfigError; -use crate::DeviceNameError; use crate::DeviceId; use crate::DeviceIdError; +use crate::DeviceNameError; use crate::DevicesError; use crate::SampleFormat; use crate::SampleRate; diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 34d6fe553..5c56b155f 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -2,9 +2,9 @@ extern crate asio_sys as sys; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, DevicesError, - InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, - StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigsError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, + DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigsError, }; pub use self::device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs}; diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 7f587adde..ffaabefa1 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -18,10 +18,11 @@ use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, - PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, + DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, + PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, }; use self::enumerate::{ diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index aa650643e..608427595 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -4,7 +4,11 @@ use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_ use super::OSStatus; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, + DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, + InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, + SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, }; use coreaudio::audio_unit::render_callback::{self, data}; use coreaudio::audio_unit::{AudioUnit, Element, Scope}; @@ -87,8 +91,8 @@ impl DeviceTrait for Device { Device::name(self) } - fn id(&self) -> Result{ - Device::id(self) + fn id(&self) -> Result { + Device::id(self) } fn supported_input_configs( diff --git a/src/host/coreaudio/mod.rs b/src/host/coreaudio/mod.rs index a22b86ee6..985167f60 100644 --- a/src/host/coreaudio/mod.rs +++ b/src/host/coreaudio/mod.rs @@ -20,10 +20,7 @@ pub use self::ios::{ }; #[cfg(target_os = "macos")] -pub use self::macos::{ - enumerate::{Devices, SupportedInputConfigs, SupportedOutputConfigs}, - Device, Host, Stream, -}; +pub use self::macos::Host; // Common helper methods used by both macOS and iOS diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 2bec598ba..440ac64f5 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -7,10 +7,10 @@ use web_sys::AudioContext; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, DevicesError, - InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, - SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, + BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; // The emscripten backend currently works by instantiating an `AudioContext` object per `Stream`. diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 31fbd4337..68ef79ef2 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -1,9 +1,9 @@ use crate::traits::DeviceTrait; use crate::{ - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, - InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, + DeviceIdError, DeviceNameError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, }; use std::hash::{Hash, Hasher}; use std::time::Duration; diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 9630bc509..24d8d68cc 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -2,11 +2,10 @@ use std::time::Duration; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceId, - DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, - OutputCallbackInfo, PauseStreamError, PlayStreamError, - SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError + BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, + DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, }; #[derive(Default)] diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 76e81a0f4..1fe9d1267 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -1,8 +1,8 @@ use crate::FrameCount; use crate::{ - BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceNameError, DeviceId, DeviceIdError, - DevicesError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, + StreamConfig, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, COMMON_SAMPLE_RATES, }; use std::ffi::OsString; @@ -328,19 +328,11 @@ impl Device { fn id(&self) -> Result { unsafe { match self.device.GetId() { - Ok(pwstr) => { - match pwstr.to_string() { - Ok(id_str) => { - Ok(DeviceId::Windows(id_str)) - }, - Err(_e) => { - Err(DeviceIdError::ParseError) - } - } + Ok(pwstr) => match pwstr.to_string() { + Ok(id_str) => Ok(DeviceId::Windows(id_str)), + Err(_e) => Err(DeviceIdError::ParseError), }, - Err(e) => { - Err(DeviceIdError::BackendSpecific { err: e.into() }) - } + Err(e) => Err(DeviceIdError::BackendSpecific { err: e.into() }), } } } diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index af030bd9b..49f05267d 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -7,10 +7,11 @@ use self::wasm_bindgen::JsCast; use self::web_sys::{AudioContext, AudioContextOptions}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceNameError, DeviceId, DeviceIdError,DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, - PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, + DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, + PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, }; use std::ops::DerefMut; use std::sync::{Arc, Mutex, RwLock}; diff --git a/src/traits.rs b/src/traits.rs index 9ab0389fb..0c0b0303e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -3,7 +3,10 @@ use std::time::Duration; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError + BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, + DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, + PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; /// A [`Host`] provides access to the available audio devices on the system. From c9d6b056aac03e818f4abcbfaba45d0afb8d4826 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+xephyris@users.noreply.github.com> Date: Wed, 24 Sep 2025 01:59:28 -0400 Subject: [PATCH 09/30] feat: add ALSA support to device id() function --- src/host/alsa/mod.rs | 2 +- src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index c1054d4c3..e63e583c1 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -313,7 +313,7 @@ impl Device { #[inline] fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + Ok(DeviceId::ALSA(self.pcm_id.clone())) } fn supported_configs( diff --git a/src/lib.rs b/src/lib.rs index 01a918a8c..4315904ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,6 +225,7 @@ pub type FrameCount = u32; pub enum DeviceId { MacOS(u32), Windows(String), + ALSA(String), } /// The buffer size used by the device. From 24ef5ac2c07738e80045b4d826eaaff3e913a2dd Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:43:21 -0700 Subject: [PATCH 10/30] feat: add support for jack and aaudio (untested). fix naming structures, and more --- src/error.rs | 4 +-- src/host/aaudio/mod.rs | 5 +++- src/host/asio/device.rs | 2 +- src/host/coreaudio/ios/mod.rs | 2 +- src/host/coreaudio/macos/device.rs | 2 +- src/host/emscripten/mod.rs | 2 +- src/host/jack/device.rs | 6 +++- src/host/null/mod.rs | 2 +- src/host/wasapi/device.rs | 2 +- src/host/webaudio/mod.rs | 2 +- src/lib.rs | 46 ++++++++++++++++++++++++++++-- src/traits.rs | 9 ++++++ 12 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/error.rs b/src/error.rs index 7e7de1234..21d649f52 100644 --- a/src/error.rs +++ b/src/error.rs @@ -72,7 +72,7 @@ pub enum DeviceIdError { BackendSpecific { err: BackendSpecificError, }, - UnsupportedOS, + UnsupportedPlatform, ParseError, } @@ -80,7 +80,7 @@ impl Display for DeviceIdError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::BackendSpecific { err } => err.fmt(f), - Self::UnsupportedOS => f.write_str("Device ids are unsupported for this OS"), + Self::UnsupportedPlatform => f.write_str("Device ids are unsupported for this OS"), Self::ParseError => f.write_str("Failed to parse the device_id"), } } diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index c14c435a2..c308b5b57 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -328,7 +328,10 @@ impl DeviceTrait for Device { } fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + match &self.0 { + None => Ok(DeviceId::aaudio(-1)), // Default device + Some(info) => Ok(DeviceId::aaudio(info.id)), + } } fn supported_input_configs( diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index bd091223a..cdd80bd40 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -57,7 +57,7 @@ impl Device { } fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + Err(DeviceIdError::UnsupportedPlatform) } /// Gets the supported input configs. diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index ffaabefa1..d9d2d573d 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -76,7 +76,7 @@ impl Device { } fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + Err(DeviceIdError::UnsupportedPlatform) } #[inline] diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index a3fa9b1bc..b20429040 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -397,7 +397,7 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::MacOS(self.audio_device_id)) + Ok(DeviceId::CoreAudio(self.audio_device_id)) } // Logic re-used between `supported_input_configs` and `supported_output_configs`. diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 440ac64f5..480c101cd 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -71,7 +71,7 @@ impl Device { #[inline] fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + Err(DeviceIdError::UnsupportedPlatform) } #[inline] diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 68ef79ef2..3856d28f6 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -64,6 +64,10 @@ impl Device { } } + fn id(&self) -> Result { + Ok(DeviceId::jack(self.name.clone())) + } + pub fn default_output_device( name: &str, connect_ports_automatically: bool, @@ -147,7 +151,7 @@ impl DeviceTrait for Device { } fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + Device::id(self) } fn supported_input_configs( diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 24d8d68cc..031911ba3 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -49,7 +49,7 @@ impl DeviceTrait for Device { #[inline] fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + Ok(DeviceId::Null) } #[inline] diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index abc328b45..bf1bcd23f 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -330,7 +330,7 @@ impl Device { unsafe { match self.device.GetId() { Ok(pwstr) => match pwstr.to_string() { - Ok(id_str) => Ok(DeviceId::Windows(id_str)), + Ok(id_str) => Ok(DeviceId::WASAPI(id_str)), Err(_e) => Err(DeviceIdError::ParseError), }, Err(e) => Err(DeviceIdError::BackendSpecific { err: e.into() }), diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 49f05267d..29cd5baf7 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -87,7 +87,7 @@ impl Device { #[inline] fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedOS) + Err(DeviceIdError::UnsupportedPlatform) } #[inline] diff --git a/src/lib.rs b/src/lib.rs index 8971e4c3c..80ce09b0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,9 +223,51 @@ pub type FrameCount = u32; /// Currently only supports macOS and Windows (WASAPI) #[derive(Clone, Debug, Eq, PartialEq)] pub enum DeviceId { - MacOS(u32), - Windows(String), + CoreAudio(u32), + WASAPI(String), ALSA(String), + AAudio(String), + Jack(String), + Null, +} + +impl std::fmt::Display for DeviceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeviceId::WASAPI(guid) => write!(f, "wasapi:{}", guid), + DeviceId::CoreAudio(uid) => write!(f, "coreaudio:{}", uid), + DeviceId::ALSA(pcm_id) => write!(f, "alsa:{}", pcm_id), + DeviceId::AAudio(id) => write!(f, "aaudio:{}", id), + DeviceId::Jack(name) => write!(f, "jack:{}", name), + DeviceId::Null => write!(f, "null"), + } + } +} + +impl std::str::FromStr for DeviceId { + type Err = DeviceIdError; + + fn from_str(s: &str) -> Result { + let (platform, data) = s.split_once(':').ok_or(DeviceIdError::ParseError)?; + + match platform { + "wasapi" => Ok(DeviceId::WASAPI(data.to_string())), + "coreaudio" => { + if let Ok(id) = data.parse::() { + Ok(DeviceId::CoreAudio(id)) + } else { + Err(DeviceIdError::ParseError) + } + } + "alsa" => Ok(DeviceId::ALSA(data.to_string())), + "aaudio" => { + let id = data.parse().map_err(|_| DeviceIdError::ParseError)?; + Ok(DeviceId::AAudio(id)) + } + "jack" => Ok(DeviceId::Jack(data.to_string())), + _ => Err(DeviceIdError::UnsupportedPlatform), + } + } } /// The buffer size used by the device. diff --git a/src/traits.rs b/src/traits.rs index 0c0b0303e..5b702f3bc 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -44,6 +44,15 @@ pub trait HostTrait { /// Can be empty if the system does not support audio in general. fn devices(&self) -> Result; + /// Fetches a [`Device`](DeviceTrait) based on a [`DeviceId`](DeviceId) if available + /// + /// Returns `None` if no device matching the id is found + fn device_by_id(&self, id: &DeviceId) -> Option { + self.devices() + .ok()? + .find(|device| device.id().ok().as_ref() == Some(id)) + } + /// The default input audio device on the system. /// /// Returns `None` if no input device is available. From 8b5b8d0c0c875185c5070a53e256e0b9a2a1b64b Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:46:39 -0700 Subject: [PATCH 11/30] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c78be07..d769854b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Add `DeviceId` enum supporting most APIs that returns a system device id for the audio device, allowing it to be reselected using the `HostTrait::device_by_id()` function - Add `Sample::bits_per_sample` method. - ALSA: Fix `BufferSize::Fixed` by selecting the nearest supported frame count. - ALSA: Change `BufferSize::Default` to use the device defaults. From 068e171f14b59222d90dd74ba023e92e19c2e120 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:49:58 -0700 Subject: [PATCH 12/30] fix: fix function names causing compile errors --- src/host/aaudio/mod.rs | 4 ++-- src/host/jack/device.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index c308b5b57..226e9becb 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -329,8 +329,8 @@ impl DeviceTrait for Device { fn id(&self) -> Result { match &self.0 { - None => Ok(DeviceId::aaudio(-1)), // Default device - Some(info) => Ok(DeviceId::aaudio(info.id)), + None => Ok(DeviceId::AAudio(-1)), // Default device + Some(info) => Ok(DeviceId::AAudio(info.id)), } } diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 3856d28f6..3d7175d98 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -65,7 +65,7 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::jack(self.name.clone())) + Ok(DeviceId::Jack(self.name.clone())) } pub fn default_output_device( From f9301b8b8ef246ce7026cf3aa1f7e15256538b9a Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:56:17 -0700 Subject: [PATCH 13/30] fix: fix aaudio DeviceId type --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 80ce09b0b..8c5bfb15a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,7 +226,7 @@ pub enum DeviceId { CoreAudio(u32), WASAPI(String), ALSA(String), - AAudio(String), + AAudio(i32), Jack(String), Null, } From 2972b52388b47d97d9d9f9a7ed1eef57de6d6cf3 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:26:47 -0700 Subject: [PATCH 14/30] feat: implement macos DeviceId to use kAudioDevicePropertyDeviceUID instead of u32 --- src/host/coreaudio/macos/device.rs | 38 +++++++++++++++++++++++++++++- src/lib.rs | 10 ++------ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index b20429040..94fcf274e 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -10,12 +10,18 @@ use crate::{ OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; +use objc2_core_foundation::Type; +use objc2_core_foundation::{ + CFString +}; use coreaudio::audio_unit::render_callback::{self, data}; use coreaudio::audio_unit::{AudioUnit, Element, Scope}; use objc2_audio_toolbox::{ kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat, }; +use objc2_core_audio::kAudioDevicePropertyDeviceUID; +use objc2_core_audio::kAudioObjectPropertyElementMain; use objc2_core_audio::{ kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyDeviceIsAlive, @@ -30,6 +36,7 @@ use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, }; + pub use super::enumerate::{ default_input_device, default_output_device, SupportedInputConfigs, SupportedOutputConfigs, }; @@ -44,6 +51,9 @@ use std::time::{Duration, Instant}; use super::property_listener::AudioObjectPropertyListener; use coreaudio::audio_unit::macos_helpers::get_device_name; + +type CFStringRef = *mut std::os::raw::c_void; + /// Attempt to set the device sample rate to the provided rate. /// Return an error if the requested sample rate is not supported by the device. fn set_sample_rate( @@ -397,7 +407,33 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::CoreAudio(self.audio_device_id)) + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceUID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + let mut name: CFStringRef = std::ptr::null_mut(); + let data_size = size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut name).cast(), + ) + }; + if status == 0 { + let name_string = unsafe {CFString::wrap_under_get_rule(name as *mut CFString).to_string()}; + Ok(DeviceId::CoreAudio(name_string)) + } else { + Err(DeviceIdError::BackendSpecific { + err: BackendSpecificError { + description: "Device UID not found".to_string(), + } + }) + } } // Logic re-used between `supported_input_configs` and `supported_output_configs`. diff --git a/src/lib.rs b/src/lib.rs index 8c5bfb15a..e51842a01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,7 +223,7 @@ pub type FrameCount = u32; /// Currently only supports macOS and Windows (WASAPI) #[derive(Clone, Debug, Eq, PartialEq)] pub enum DeviceId { - CoreAudio(u32), + CoreAudio(String), WASAPI(String), ALSA(String), AAudio(i32), @@ -252,13 +252,7 @@ impl std::str::FromStr for DeviceId { match platform { "wasapi" => Ok(DeviceId::WASAPI(data.to_string())), - "coreaudio" => { - if let Ok(id) = data.parse::() { - Ok(DeviceId::CoreAudio(id)) - } else { - Err(DeviceIdError::ParseError) - } - } + "coreaudio" => Ok(DeviceId::CoreAudio(data.to_string())), "alsa" => Ok(DeviceId::ALSA(data.to_string())), "aaudio" => { let id = data.parse().map_err(|_| DeviceIdError::ParseError)?; From 99fc8b7d619f1d87057a70faa088466ce93a0b48 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:28:02 -0700 Subject: [PATCH 15/30] fmt: reformat with rustfmt --- src/host/coreaudio/macos/device.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 94fcf274e..4ca3eef17 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -10,10 +10,6 @@ use crate::{ OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; -use objc2_core_foundation::Type; -use objc2_core_foundation::{ - CFString -}; use coreaudio::audio_unit::render_callback::{self, data}; use coreaudio::audio_unit::{AudioUnit, Element, Scope}; use objc2_audio_toolbox::{ @@ -35,7 +31,8 @@ use objc2_core_audio::{ use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, }; - +use objc2_core_foundation::CFString; +use objc2_core_foundation::Type; pub use super::enumerate::{ default_input_device, default_output_device, SupportedInputConfigs, SupportedOutputConfigs, @@ -425,13 +422,14 @@ impl Device { ) }; if status == 0 { - let name_string = unsafe {CFString::wrap_under_get_rule(name as *mut CFString).to_string()}; + let name_string = + unsafe { CFString::wrap_under_get_rule(name as *mut CFString).to_string() }; Ok(DeviceId::CoreAudio(name_string)) } else { - Err(DeviceIdError::BackendSpecific { + Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { description: "Device UID not found".to_string(), - } + }, }) } } From 43bc3e01b317b10676540f8fc2a627f0dd6112b0 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:00:21 -0700 Subject: [PATCH 16/30] docs: update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d769854b1..5fb2fed48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- Add `DeviceId` enum supporting most APIs that returns a system device id for the audio device, allowing it to be reselected using the `HostTrait::device_by_id()` function +- Add `HostTrait::id` method that returns a stable audio device ID. +- Add `HostTrait::device_by_id` to select a device by its stable ID. - Add `Sample::bits_per_sample` method. - ALSA: Fix `BufferSize::Fixed` by selecting the nearest supported frame count. - ALSA: Change `BufferSize::Default` to use the device defaults. From 6e93a1b1bc8d53091afda57ccabce609bb3ff876 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:26:53 -0700 Subject: [PATCH 17/30] feat: add asio support and reformat macos id function --- src/host/asio/device.rs | 2 +- src/host/coreaudio/macos/device.rs | 13 +++++++------ src/lib.rs | 3 +++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index cdd80bd40..d38d28c70 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -57,7 +57,7 @@ impl Device { } fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedPlatform) + Ok(DeviceId::ASIO(self.driver.name().to_string())) } /// Gets the supported input configs. diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 4ca3eef17..9f3b00271 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -409,7 +409,7 @@ impl Device { mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain, }; - let mut name: CFStringRef = std::ptr::null_mut(); + let mut uid: CFStringRef = std::ptr::null_mut(); let data_size = size_of::() as u32; let status = unsafe { AudioObjectGetPropertyData( @@ -418,13 +418,14 @@ impl Device { 0, null(), NonNull::from(&data_size), - NonNull::from(&mut name).cast(), + NonNull::from(&mut uid).cast(), ) }; - if status == 0 { - let name_string = - unsafe { CFString::wrap_under_get_rule(name as *mut CFString).to_string() }; - Ok(DeviceId::CoreAudio(name_string)) + check_os_status(status)?; + if !uid.is_null(){ + let uid_string = + unsafe { CFString::wrap_under_get_rule(uid as *mut CFString).to_string() }; + Ok(DeviceId::CoreAudio(uid_string)) } else { Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { diff --git a/src/lib.rs b/src/lib.rs index e51842a01..fe3d2c975 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,6 +225,7 @@ pub type FrameCount = u32; pub enum DeviceId { CoreAudio(String), WASAPI(String), + ASIO(String), ALSA(String), AAudio(i32), Jack(String), @@ -235,6 +236,7 @@ impl std::fmt::Display for DeviceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DeviceId::WASAPI(guid) => write!(f, "wasapi:{}", guid), + DeviceId::ASIO(guid) => write!(f, "asio:{}", guid), DeviceId::CoreAudio(uid) => write!(f, "coreaudio:{}", uid), DeviceId::ALSA(pcm_id) => write!(f, "alsa:{}", pcm_id), DeviceId::AAudio(id) => write!(f, "aaudio:{}", id), @@ -252,6 +254,7 @@ impl std::str::FromStr for DeviceId { match platform { "wasapi" => Ok(DeviceId::WASAPI(data.to_string())), + "asio" => Ok(DeviceId::ASIO(data.to_string())), "coreaudio" => Ok(DeviceId::CoreAudio(data.to_string())), "alsa" => Ok(DeviceId::ALSA(data.to_string())), "aaudio" => { From 59f4a9f6dc84633bfaa14064b970881d9e61899e Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:28:27 -0700 Subject: [PATCH 18/30] fmt: reformat code --- src/host/asio/device.rs | 2 +- src/host/coreaudio/macos/device.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index d38d28c70..85d851039 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -57,7 +57,7 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::ASIO(self.driver.name().to_string())) + Ok(DeviceId::ASIO(self.driver.name().to_string())) } /// Gets the supported input configs. diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 9f3b00271..fdfc13d4d 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -422,7 +422,7 @@ impl Device { ) }; check_os_status(status)?; - if !uid.is_null(){ + if !uid.is_null() { let uid_string = unsafe { CFString::wrap_under_get_rule(uid as *mut CFString).to_string() }; Ok(DeviceId::CoreAudio(uid_string)) From b811266dade146943763e476f2877ba9d619c01f Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:51:46 -0700 Subject: [PATCH 19/30] feat: return default for ios, emscripten, and webaudio --- src/host/coreaudio/ios/mod.rs | 2 +- src/host/emscripten/mod.rs | 2 +- src/host/webaudio/mod.rs | 2 +- src/lib.rs | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index d9d2d573d..58d75fa8e 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -76,7 +76,7 @@ impl Device { } fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedPlatform) + Ok(DeviceId::IOS("default".to_string())) } #[inline] diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 480c101cd..b3cdd54d0 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -71,7 +71,7 @@ impl Device { #[inline] fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedPlatform) + Ok(DeviceId::Emscripten("default".to_string())) } #[inline] diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 29cd5baf7..e9152ed3f 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -87,7 +87,7 @@ impl Device { #[inline] fn id(&self) -> Result { - Err(DeviceIdError::UnsupportedPlatform) + Ok(DeviceId::WebAudio("default".to_string())) } #[inline] diff --git a/src/lib.rs b/src/lib.rs index fe3d2c975..9b35c16b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,6 +229,9 @@ pub enum DeviceId { ALSA(String), AAudio(i32), Jack(String), + WebAudio(String), + Emscripten(String), + IOS(String), Null, } @@ -241,6 +244,9 @@ impl std::fmt::Display for DeviceId { DeviceId::ALSA(pcm_id) => write!(f, "alsa:{}", pcm_id), DeviceId::AAudio(id) => write!(f, "aaudio:{}", id), DeviceId::Jack(name) => write!(f, "jack:{}", name), + DeviceId::WebAudio(default) => write!(f, "webaudio:{}", default), + DeviceId::Emscripten(default) => write!(f, "emscripten:{}", default), + DeviceId::IOS(default) => write!(f, "ios:{}", default), DeviceId::Null => write!(f, "null"), } } @@ -262,6 +268,9 @@ impl std::str::FromStr for DeviceId { Ok(DeviceId::AAudio(id)) } "jack" => Ok(DeviceId::Jack(data.to_string())), + "webaudio" => Ok(DeviceId::WebAudio(data.to_string())), + "emscripten" => Ok(DeviceId::Emscripten(data.to_string())), + "ios" => Ok(DeviceId::IOS(data.to_string())), _ => Err(DeviceIdError::UnsupportedPlatform), } } From f40bb9dcb79c831aa6c66e41bf29992f7f5894ce Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Sun, 28 Sep 2025 23:16:35 -0700 Subject: [PATCH 20/30] feat: add in rest of audio APIs to from_str() --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 4274a6a0a..e3e6eac3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -272,6 +272,7 @@ impl std::str::FromStr for DeviceId { "webaudio" => Ok(DeviceId::WebAudio(data.to_string())), "emscripten" => Ok(DeviceId::Emscripten(data.to_string())), "ios" => Ok(DeviceId::IOS(data.to_string())), + "null" => Ok(DeviceId::Null), _ => Err(DeviceIdError::UnsupportedPlatform), } } From 30645a597163f766502eafea0023855c1f143895 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:27:43 -0700 Subject: [PATCH 21/30] fix: change catch-all to todo! --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 91b158f1c..451800a35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -273,7 +273,7 @@ impl std::str::FromStr for DeviceId { "emscripten" => Ok(DeviceId::Emscripten(data.to_string())), "ios" => Ok(DeviceId::IOS(data.to_string())), "null" => Ok(DeviceId::Null), - _ => Err(DeviceIdError::UnsupportedPlatform), + &_ => todo!("implement DeviceId::FromStr for {platform}"), } } } From 44ec96be987601381d7194a91c2905b2dc5e3624 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:44:26 -0700 Subject: [PATCH 22/30] fix: resolve parse error on null and deviceid implementations --- src/error.rs | 2 +- src/lib.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/error.rs b/src/error.rs index 21d649f52..d39b071d7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,7 +66,7 @@ impl From for DevicesError { } /// An error that may occur while attempting to retrieve a device id. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum DeviceIdError { /// See the [`BackendSpecificError`] docs for more information about this error variant. BackendSpecific { diff --git a/src/lib.rs b/src/lib.rs index 451800a35..eca1e963c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -222,7 +222,8 @@ pub type FrameCount = u32; /// The device ID of the audio device, on supported OSs /// Currently only supports macOS and Windows (WASAPI) -#[derive(Clone, Debug, Eq, PartialEq)] + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DeviceId { CoreAudio(String), WASAPI(String), @@ -248,7 +249,7 @@ impl std::fmt::Display for DeviceId { DeviceId::WebAudio(default) => write!(f, "webaudio:{}", default), DeviceId::Emscripten(default) => write!(f, "emscripten:{}", default), DeviceId::IOS(default) => write!(f, "ios:{}", default), - DeviceId::Null => write!(f, "null"), + DeviceId::Null => write!(f, "null:null"), } } } From 01a240d41c46f834db67d2156a932f7620c771dc Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:20:25 -0700 Subject: [PATCH 23/30] docs: fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f172f649..bcd691440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -- Add `HostTrait::id` method that returns a stable audio device ID. +- Add `DeviceTrait::id` method that returns a stable audio device ID. - Add `HostTrait::device_by_id` to select a device by its stable ID. - Add `Sample::bits_per_sample` method. - Update `audio_thread_priority` to 0.34. From 6eb9e7a50e30572ccf7cc8ac668c433c4c0e2b9a Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:47:46 -0700 Subject: [PATCH 24/30] refactor: improve safety of get_id() function, remove CFStringRef --- src/host/coreaudio/macos/device.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index ca969fee5..7f8d9652a 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -49,8 +49,6 @@ use std::time::{Duration, Instant}; use super::property_listener::AudioObjectPropertyListener; use coreaudio::audio_unit::macos_helpers::get_device_name; -type CFStringRef = *mut std::os::raw::c_void; - /// Attempt to set the device sample rate to the provided rate. /// Return an error if the requested sample rate is not supported by the device. fn set_sample_rate( @@ -412,8 +410,13 @@ impl Device { mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain, }; - let mut uid: CFStringRef = std::ptr::null_mut(); - let data_size = size_of::() as u32; + + // CFString is retained by the audio object, use wrap_under_get_rule + let mut uid: *mut CFString = std::ptr::null_mut(); + let data_size = size_of::<*mut CFString>() as u32; + + // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer + // for kAudioDevicePropertyDeviceUID. We check the status code before use. let status = unsafe { AudioObjectGetPropertyData( self.audio_device_id, @@ -425,14 +428,16 @@ impl Device { ) }; check_os_status(status)?; + + // SAFETY: We verified uid is non-null and the status was successful if !uid.is_null() { let uid_string = - unsafe { CFString::wrap_under_get_rule(uid as *mut CFString).to_string() }; + unsafe { CFString::wrap_under_get_rule(uid).to_string() }; Ok(DeviceId::CoreAudio(uid_string)) } else { Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { - description: "Device UID not found".to_string(), + description: "Device UID is null".to_string(), }, }) } From d516476b5b1fa2772e6d1719fc43a01ac7ba9d3b Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:48:43 -0700 Subject: [PATCH 25/30] fmt: run rustfmt --- src/host/coreaudio/macos/device.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 7f8d9652a..fdf503c9a 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -411,11 +411,11 @@ impl Device { mElement: kAudioObjectPropertyElementMain, }; - // CFString is retained by the audio object, use wrap_under_get_rule + // CFString is retained by the audio object, use wrap_under_get_rule let mut uid: *mut CFString = std::ptr::null_mut(); let data_size = size_of::<*mut CFString>() as u32; - // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer + // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer // for kAudioDevicePropertyDeviceUID. We check the status code before use. let status = unsafe { AudioObjectGetPropertyData( @@ -428,11 +428,10 @@ impl Device { ) }; check_os_status(status)?; - - // SAFETY: We verified uid is non-null and the status was successful + + // SAFETY: We verified uid is non-null and the status was successful if !uid.is_null() { - let uid_string = - unsafe { CFString::wrap_under_get_rule(uid).to_string() }; + let uid_string = unsafe { CFString::wrap_under_get_rule(uid).to_string() }; Ok(DeviceId::CoreAudio(uid_string)) } else { Err(DeviceIdError::BackendSpecific { From 5c56868e469cb11422217c8cd54c134cf9a5ce68 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:12:14 -0700 Subject: [PATCH 26/30] fix: resolve wrap_under rule error for CFString --- src/host/coreaudio/macos/device.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index fdf503c9a..8b44ae67f 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -411,7 +411,7 @@ impl Device { mElement: kAudioObjectPropertyElementMain, }; - // CFString is retained by the audio object, use wrap_under_get_rule + // CFString is copied from the audio object, use wrap_under_create_rule let mut uid: *mut CFString = std::ptr::null_mut(); let data_size = size_of::<*mut CFString>() as u32; @@ -431,7 +431,7 @@ impl Device { // SAFETY: We verified uid is non-null and the status was successful if !uid.is_null() { - let uid_string = unsafe { CFString::wrap_under_get_rule(uid).to_string() }; + let uid_string = unsafe { CFString::wrap_under_create_rule(uid).to_string() }; Ok(DeviceId::CoreAudio(uid_string)) } else { Err(DeviceIdError::BackendSpecific { From 82b4abdacc59ec6fbe257823f90c06060feca4e9 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:25:08 -0700 Subject: [PATCH 27/30] fix: Merge branch 'master' into master --- CHANGELOG.md | 12 +- Cargo.toml | 15 +- src/host/aaudio/mod.rs | 6 + src/host/alsa/mod.rs | 25 ++ src/host/asio/stream.rs | 20 +- src/host/coreaudio/ios/enumerate.rs | 4 +- src/host/coreaudio/ios/mod.rs | 557 +++++++++++++++----------- src/host/coreaudio/macos/device.rs | 169 ++++---- src/host/coreaudio/macos/enumerate.rs | 3 - src/host/coreaudio/macos/mod.rs | 166 +++++++- src/host/coreaudio/mod.rs | 5 + src/host/emscripten/mod.rs | 11 +- src/host/jack/device.rs | 17 + src/host/jack/stream.rs | 3 + src/host/mod.rs | 10 + src/host/null/mod.rs | 3 + src/host/wasapi/device.rs | 4 + src/host/wasapi/stream.rs | 7 + src/host/webaudio/mod.rs | 11 +- src/lib.rs | 7 +- 20 files changed, 689 insertions(+), 366 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd691440..ac726bfe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Add `Sample::bits_per_sample` method. - Update `audio_thread_priority` to 0.34. - AAudio: Configure buffer to ensure consistent callback buffer sizes. -- ALSA: Improve `BufferSize::Fixed` latency precision and audio callback performance. +- ALSA: Improve `BufferSize::Fixed` precision and audio callback performance. - ALSA: Change `BufferSize::Default` to use the device defaults. - ALSA: Change card enumeration to work like `aplay -L` does. - ALSA: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes). @@ -20,15 +20,23 @@ - CoreAudio: Change `Device::supported_configs` to return a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values. - CoreAudio: Change default audio device detection to be lazy when building a stream, instead of during device enumeration. - CoreAudio: Add `i8`, `i32` and `I24` sample format support (24-bit samples stored in 4 bytes). -- CoreAudio: Add support for loopback recording (recording system audio output) on macOS. +- CoreAudio: Add support for loopback recording (recording system audio output) on macOS > 14.6. - CoreAudio: Update `mach2` to 0.5. - CoreAudio: Configure device buffer to ensure predictable callback buffer sizes. - CoreAudio: Fix timestamp accuracy. +- CoreAudio: Make `Stream` implement `Send`. +- CoreAudio: Remove `Clone` impl from `Stream`. +- Emscripten: Add `BufferSize::Fixed` validation against supported range. - iOS: Fix example by properly activating audio session. +- iOS: Add complete AVAudioSession integration for device enumeration and buffer size control. +- JACK: Add `BufferSize::Fixed` validation to reject requests that don't match server buffer size. - WASAPI: Expose `IMMDevice` from WASAPI host Device. - WASAPI: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes). - WASAPI: Update `windows` to >= 0.58, <= 0.62. +- WASAPI: Make `Stream` implement `Send`. - Wasm: Removed optional `wee-alloc` feature for security reasons. +- Wasm: Make `Stream` implement `Send`. +- WebAudio: Add `BufferSize::Fixed` validation against supported range. # Version 0.16.0 (2025-06-07) diff --git a/Cargo.toml b/Cargo.toml index 2f80d858c..0ff38048c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "cpal" version = "0.16.0" description = "Low-level cross-platform audio I/O library in pure Rust." -repository = "https://github.com/rustaudio/cpal" +repository = "https://github.com/RustAudio/cpal" documentation = "https://docs.rs/cpal" license = "Apache-2.0" keywords = ["audio", "sound"] @@ -15,9 +15,6 @@ asio = [ "num-traits", ] # Only available on Windows. See README for setup instructions. -# Deprecated, the `oboe` backend has been removed -oboe-shared-stdcxx = [] - [dependencies] dasp_sample = "0.11" @@ -54,10 +51,8 @@ libc = "0.2" audio_thread_priority = { version = "0.34.0", optional = true } jack = { version = "0.13.0", optional = true } -[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -mach2 = "0.5" # For access to mach_timebase type. - [target.'cfg(target_vendor = "apple")'.dependencies] +mach2 = "0.5" coreaudio-rs = { version = "0.13.0", default-features = false, features = [ "core_audio", "audio_toolbox", @@ -82,6 +77,12 @@ objc2-core-foundation = { version = "0.3.1" } objc2-foundation = { version = "0.3.1" } objc2 = { version = "0.6.2" } +[target.'cfg(target_os = "ios")'.dependencies] +objc2-avf-audio = { version = "0.3.1", default-features = false, features = [ + "std", + "AVAudioSession", +] } + [target.'cfg(target_os = "emscripten")'.dependencies] wasm-bindgen = { version = "0.2.89" } wasm-bindgen-futures = "0.4.33" diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index d6880b4b2..3da8753ef 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -49,6 +49,9 @@ pub enum Stream { // TODO: Is this still in-progress? https://github.com/rust-mobile/ndk/pull/497 unsafe impl Send for Stream {} +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + pub type SupportedInputConfigs = VecIntoIter; pub type SupportedOutputConfigs = VecIntoIter; pub type Devices = VecIntoIter; @@ -215,6 +218,9 @@ fn configure_for_device( builder }; builder = builder.sample_rate(config.sample_rate.0.try_into().unwrap()); + + // Note: Buffer size validation is not needed - the native AAudio API validates buffer sizes + // when `open_stream()` is called. match &config.buffer_size { BufferSize::Default => builder, BufferSize::Fixed(size) => builder diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index b2b9ca384..6cbb004b0 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -283,6 +283,28 @@ impl Device { sample_format: SampleFormat, stream_type: alsa::Direction, ) -> Result { + // Validate buffer size if Fixed is specified. This is necessary because + // `set_period_size_near()` with `ValueOr::Nearest` will accept ANY value and return the + // "nearest" supported value, which could be wildly different (e.g., requesting 4096 frames + // might return 512 frames if that's "nearest"). + if let BufferSize::Fixed(requested_size) = conf.buffer_size { + // Note: We use `default_input_config`/`default_output_config` to get the buffer size + // range. This queries the CURRENT device (`self.pcm_id`), not the default device. The + // buffer size range is the same across all format configurations for a given device + // (see `supported_configs()`). + let supported_config = match stream_type { + alsa::Direction::Capture => self.default_input_config(), + alsa::Direction::Playback => self.default_output_config(), + }; + if let Ok(config) = supported_config { + if let SupportedBufferSize::Range { min, max } = config.buffer_size { + if !(min..=max).contains(&requested_size) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + } + } + let handle_result = self .handles .lock() @@ -611,6 +633,9 @@ pub struct Stream { trigger: TriggerSender, } +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + struct StreamWorkerContext { descriptors: Box<[libc::pollfd]>, transfer_buffer: Box<[u8]>, diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs index 445310c68..b2f76b9ae 100644 --- a/src/host/asio/stream.rs +++ b/src/host/asio/stream.rs @@ -22,6 +22,9 @@ pub struct Stream { callback_id: sys::CallbackId, } +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + impl Stream { pub fn play(&self) -> Result<(), PlayStreamError> { self.playing.store(true, Ordering::SeqCst); @@ -661,7 +664,7 @@ fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Dura /// Check whether or not the desired config is supported by the stream. /// -/// Checks sample rate, data type and then finally the number of channels. +/// Checks sample rate, data type, number of channels, and buffer size. fn check_config( driver: &sys::Driver, config: &StreamConfig, @@ -671,8 +674,21 @@ fn check_config( let StreamConfig { channels, sample_rate, - buffer_size: _, + buffer_size, } = config; + + // Validate buffer size if `Fixed` is specified. This is necessary because ASIO's + // `create_buffers` only validates the upper bound (returns `InvalidBufferSize` if > max) but + // does NOT validate the lower bound. Passing a buffer size below min would be accepted but + // behavior is unspecified. + if let BufferSize::Fixed(requested_size) = buffer_size { + let (min, max) = driver.buffersize_range().map_err(build_stream_err)?; + let requested_size_i32 = *requested_size as i32; + if !(min..=max).contains(&requested_size_i32) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + // Try and set the sample rate to what the user selected. let sample_rate = sample_rate.0.into(); if sample_rate != driver.sample_rate().map_err(build_stream_err)? { diff --git a/src/host/coreaudio/ios/enumerate.rs b/src/host/coreaudio/ios/enumerate.rs index 44479297a..0bfc7f3e9 100644 --- a/src/host/coreaudio/ios/enumerate.rs +++ b/src/host/coreaudio/ios/enumerate.rs @@ -12,8 +12,8 @@ pub type SupportedOutputConfigs = ::std::vec::IntoIter); impl Devices { - pub fn new() -> Result { - Ok(Self::default()) + pub fn new() -> Self { + Self::default() } } diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 3b034170f..aba697804 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -1,32 +1,14 @@ -//! -//! CoreAudio implementation for iOS using RemoteIO Audio Units. -//! -//! ## Implementation Details -//! -//! This implementation uses **RemoteIO Audio Units** to interface with iOS audio hardware: -//! -//! - **RemoteIO**: A special Audio Unit that acts as a proxy to the actual hardware -//! - **Direct queries**: Buffer sizes are queried directly from the RemoteIO unit -//! - **System control**: iOS controls buffer sizes, sample rates, and device routing -//! - **Single device model**: iOS presents audio as a single system-managed device -//! -//! ## Limitations -//! -//! - **No device enumeration**: iOS doesn't allow direct hardware device access -//! - **No fixed buffer sizes**: `BufferSize::Fixed` returns `StreamConfigNotSupported` -//! - **System-determined parameters**: Buffer sizes and sample rates are set by iOS - -// TODO: -// - Use AVAudioSession to enumerate buffer size / sample rate / number of channels and set -// buffer size. - -use std::cell::RefCell; +//! CoreAudio implementation for iOS using AVAudioSession and RemoteIO Audio Units. + +use std::sync::Mutex; use coreaudio::audio_unit::render_callback::data; use coreaudio::audio_unit::{render_callback, AudioUnit, Element, Scope}; use objc2_audio_toolbox::{kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat}; use objc2_core_audio::kAudioDevicePropertyBufferFrameSize; -use objc2_core_audio_types::{AudioBuffer, AudioStreamBasicDescription}; +use objc2_core_audio_types::AudioBuffer; + +use objc2_avf_audio::AVAudioSession; use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; @@ -71,7 +53,7 @@ impl HostTrait for Host { } fn devices(&self) -> Result { - Devices::new() + Ok(Devices::new()) } fn default_input_device(&self) -> Option { @@ -97,55 +79,32 @@ impl Device { fn supported_input_configs( &self, ) -> Result { - // TODO: query AVAudioSession for parameters, some values like sample rate and buffer size - // probably need to actually be set to see if it works, but channels can be enumerated. - - let asbd: AudioStreamBasicDescription = default_input_asbd()?; - let stream_config = stream_config_from_asbd(asbd); - Ok(vec![SupportedStreamConfigRange { - channels: stream_config.channels, - min_sample_rate: stream_config.sample_rate, - max_sample_rate: stream_config.sample_rate, - buffer_size: stream_config.buffer_size.clone(), - sample_format: SUPPORTED_SAMPLE_FORMAT, - }] - .into_iter()) + Ok(get_supported_stream_configs(true)) } #[inline] fn supported_output_configs( &self, ) -> Result { - // TODO: query AVAudioSession for parameters, some values like sample rate and buffer size - // probably need to actually be set to see if it works, but channels can be enumerated. - - let asbd: AudioStreamBasicDescription = default_output_asbd()?; - let stream_config = stream_config_from_asbd(asbd); - - let configs: Vec<_> = (1..=asbd.mChannelsPerFrame as u16) - .map(|channels| SupportedStreamConfigRange { - channels, - min_sample_rate: stream_config.sample_rate, - max_sample_rate: stream_config.sample_rate, - buffer_size: stream_config.buffer_size.clone(), - sample_format: SUPPORTED_SAMPLE_FORMAT, - }) - .collect(); - Ok(configs.into_iter()) + Ok(get_supported_stream_configs(false)) } #[inline] fn default_input_config(&self) -> Result { - let asbd: AudioStreamBasicDescription = default_input_asbd()?; - let stream_config = stream_config_from_asbd(asbd); - Ok(stream_config) + // Get the primary (exact channel count) config from supported configs + get_supported_stream_configs(true) + .next() + .map(|range| range.with_max_sample_rate()) + .ok_or_else(|| DefaultStreamConfigError::StreamTypeNotSupported) } #[inline] fn default_output_config(&self) -> Result { - let asbd: AudioStreamBasicDescription = default_output_asbd()?; - let stream_config = stream_config_from_asbd(asbd); - Ok(stream_config) + // Get the maximum channel count config from supported configs + get_supported_stream_configs(false) + .last() + .map(|range| range.with_max_sample_rate()) + .ok_or_else(|| DefaultStreamConfigError::StreamTypeNotSupported) } } @@ -192,83 +151,29 @@ impl DeviceTrait for Device { &self, config: &StreamConfig, sample_format: SampleFormat, - mut data_callback: D, - mut error_callback: E, + data_callback: D, + error_callback: E, _timeout: Option, ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - // The scope and element for working with a device's input stream. - let scope = Scope::Output; - let element = Element::Input; - - let mut audio_unit = create_audio_unit()?; - audio_unit.uninitialize()?; - configure_for_recording(&mut audio_unit)?; - audio_unit.initialize()?; - - // Set the stream in interleaved mode. - let asbd = asbd_from_config(config, sample_format); - audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; - - // Set the buffersize - match config.buffer_size { - BufferSize::Fixed(_) => { - return Err(BuildStreamError::StreamConfigNotSupported); - } - BufferSize::Default => (), - } - - // Query the actual device buffer size for more accurate latency calculation. On iOS, - // BufferSize::Fixed is not supported, so this always gets the current device buffer size. - let device_buffer_frames = get_device_buffer_frame_size(&audio_unit).ok(); - - // Register the callback that is being called by coreaudio whenever it needs data to be - // fed to the audio buffer. - let bytes_per_channel = sample_format.sample_size(); - let sample_rate = config.sample_rate; - type Args = render_callback::Args; - audio_unit.set_input_callback(move |args: Args| unsafe { - let ptr = (*args.data.data).mBuffers.as_ptr() as *const AudioBuffer; - let len = (*args.data.data).mNumberBuffers as usize; - let buffers: &[AudioBuffer] = slice::from_raw_parts(ptr, len); - - // There is only 1 buffer when using interleaved channels - let AudioBuffer { - mNumberChannels: channels, - mDataByteSize: data_byte_size, - mData: data, - } = buffers[0]; - - let data = data as *mut (); - let len = (data_byte_size as usize / bytes_per_channel) as usize; - let data = Data::from_parts(data, len, sample_format); - - let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { - Err(err) => { - error_callback(err.into()); - return Err(()); - } - Ok(cb) => cb, - }; - let buffer_frames = len / channels as usize; - // Use device buffer size for latency calculation if available - let latency_frames = device_buffer_frames.unwrap_or( - // Fallback to callback buffer size if device buffer size is unknown - buffer_frames, - ); - let delay = frames_to_duration(latency_frames, sample_rate); - let capture = callback - .sub(delay) - .expect("`capture` occurs before origin of alsa `StreamInstant`"); - let timestamp = crate::InputStreamTimestamp { callback, capture }; - - let info = InputCallbackInfo { timestamp }; - data_callback(&data, &info); - Ok(()) - })?; + // Configure buffer size and create audio unit + let mut audio_unit = setup_stream_audio_unit(config, sample_format, true)?; + + // Query device buffer size for latency calculation + let device_buffer_frames = Some(get_device_buffer_frames()); + + // Set up input callback + setup_input_callback( + &mut audio_unit, + sample_format, + config.sample_rate, + device_buffer_frames, + data_callback, + error_callback, + )?; audio_unit.start()?; @@ -283,77 +188,29 @@ impl DeviceTrait for Device { &self, config: &StreamConfig, sample_format: SampleFormat, - mut data_callback: D, - mut error_callback: E, + data_callback: D, + error_callback: E, _timeout: Option, ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - match config.buffer_size { - BufferSize::Fixed(_) => { - return Err(BuildStreamError::StreamConfigNotSupported); - } - BufferSize::Default => (), - }; - - let mut audio_unit = create_audio_unit()?; - - // The scope and element for working with a device's output stream. - let scope = Scope::Input; - let element = Element::Output; - - // Set the stream in interleaved mode. - let asbd = asbd_from_config(config, sample_format); - audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; - - // Query the actual device buffer size for more accurate latency calculation. On iOS, - // BufferSize::Fixed is not supported, so this always gets the current device buffer size. - let device_buffer_frames = get_device_buffer_frame_size(&audio_unit).ok(); - - // Register the callback that is being called by coreaudio whenever it needs data to be - // fed to the audio buffer. - let bytes_per_channel = sample_format.sample_size(); - let sample_rate = config.sample_rate; - type Args = render_callback::Args; - audio_unit.set_render_callback(move |args: Args| unsafe { - // If `run()` is currently running, then a callback will be available from this list. - // Otherwise, we just fill the buffer with zeroes and return. - - let AudioBuffer { - mNumberChannels: channels, - mDataByteSize: data_byte_size, - mData: data, - } = (*args.data.data).mBuffers[0]; - - let data = data as *mut (); - let len = (data_byte_size as usize / bytes_per_channel) as usize; - let mut data = Data::from_parts(data, len, sample_format); - - let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { - Err(err) => { - error_callback(err.into()); - return Err(()); - } - Ok(cb) => cb, - }; - let buffer_frames = len / channels as usize; - // Use device buffer size for latency calculation if available - let latency_frames = device_buffer_frames.unwrap_or( - // Fallback to callback buffer size if device buffer size is unknown - buffer_frames, - ); - let delay = frames_to_duration(latency_frames, sample_rate); - let playback = callback - .add(delay) - .expect("`playback` occurs beyond representation supported by `StreamInstant`"); - let timestamp = crate::OutputStreamTimestamp { callback, playback }; - - let info = OutputCallbackInfo { timestamp }; - data_callback(&mut data, &info); - Ok(()) - })?; + // Configure buffer size and create audio unit + let mut audio_unit = setup_stream_audio_unit(config, sample_format, false)?; + + // Query device buffer size for latency calculation + let device_buffer_frames = Some(get_device_buffer_frames()); + + // Set up output callback + setup_output_callback( + &mut audio_unit, + sample_format, + config.sample_rate, + device_buffer_frames, + data_callback, + error_callback, + )?; audio_unit.start()?; @@ -365,20 +222,27 @@ impl DeviceTrait for Device { } pub struct Stream { - inner: RefCell, + inner: Mutex, } impl Stream { fn new(inner: StreamInner) -> Self { Self { - inner: RefCell::new(inner), + inner: Mutex::new(inner), } } } impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { - let mut stream = self.inner.borrow_mut(); + let mut stream = self + .inner + .lock() + .map_err(|_| PlayStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; if !stream.playing { if let Err(e) = stream.audio_unit.start() { @@ -392,7 +256,14 @@ impl StreamTrait for Stream { } fn pause(&self) -> Result<(), PauseStreamError> { - let mut stream = self.inner.borrow_mut(); + let mut stream = self + .inner + .lock() + .map_err(|_| PauseStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; if stream.playing { if let Err(e) = stream.audio_unit.stop() { @@ -438,47 +309,263 @@ fn configure_for_recording(audio_unit: &mut AudioUnit) -> Result<(), coreaudio:: Ok(()) } -fn default_output_asbd() -> Result { - let audio_unit = create_audio_unit()?; - let id = kAudioUnitProperty_StreamFormat; - let asbd: AudioStreamBasicDescription = - audio_unit.get_property(id, Scope::Output, Element::Output)?; - Ok(asbd) +/// Configure AVAudioSession with the requested buffer size. +/// +/// Note: iOS may not honor the exact request due to system constraints. +fn set_audio_session_buffer_size( + buffer_size: u32, + sample_rate: crate::SampleRate, +) -> Result<(), BuildStreamError> { + // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton + let audio_session = unsafe { AVAudioSession::sharedInstance() }; + + // Calculate preferred buffer duration in seconds + let buffer_duration = buffer_size as f64 / sample_rate.0 as f64; + + // Set the preferred IO buffer duration + // SAFETY: setPreferredIOBufferDuration_error is safe to call with valid duration + unsafe { + audio_session + .setPreferredIOBufferDuration_error(buffer_duration) + .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; + } + + Ok(()) +} + +/// Get the actual buffer size from AVAudioSession. +/// +/// This queries the current IO buffer duration from AVAudioSession and converts +/// it to frames based on the current sample rate. +fn get_device_buffer_frames() -> usize { + // SAFETY: AVAudioSession methods are safe to call on the singleton instance + unsafe { + let audio_session = AVAudioSession::sharedInstance(); + let buffer_duration = audio_session.IOBufferDuration(); + let sample_rate = audio_session.sampleRate(); + (buffer_duration * sample_rate) as usize + } } -fn default_input_asbd() -> Result { - let mut audio_unit = create_audio_unit()?; - audio_unit.uninitialize()?; - configure_for_recording(&mut audio_unit)?; - audio_unit.initialize()?; - - let id = kAudioUnitProperty_StreamFormat; - let asbd: AudioStreamBasicDescription = - audio_unit.get_property(id, Scope::Input, Element::Input)?; - Ok(asbd) +/// Get supported stream config ranges for input (is_input=true) or output (is_input=false). +fn get_supported_stream_configs(is_input: bool) -> std::vec::IntoIter { + // SAFETY: AVAudioSession methods are safe to call on the singleton instance + let (sample_rate, max_channels) = unsafe { + let audio_session = AVAudioSession::sharedInstance(); + let sample_rate = audio_session.sampleRate() as u32; + let max_channels = if is_input { + audio_session.inputNumberOfChannels() as u16 + } else { + audio_session.outputNumberOfChannels() as u16 + }; + (sample_rate, max_channels) + }; + + // Typical iOS hardware buffer frame limits according to Apple Technical Q&A QA1631. + let buffer_size = SupportedBufferSize::Range { + min: 256, + max: 4096, + }; + + // For input, only return the exact channel count (no flexibility) + // For output, support flexible channel counts up to the hardware maximum + let min_channels = if is_input { max_channels } else { 1 }; + + let configs: Vec<_> = (min_channels..=max_channels) + .map(|channels| SupportedStreamConfigRange { + channels, + min_sample_rate: SampleRate(sample_rate), + max_sample_rate: SampleRate(sample_rate), + buffer_size: buffer_size.clone(), + sample_format: SUPPORTED_SAMPLE_FORMAT, + }) + .collect(); + + configs.into_iter() } -fn stream_config_from_asbd(asbd: AudioStreamBasicDescription) -> SupportedStreamConfig { - let buffer_size = SupportedBufferSize::Range { min: 0, max: 0 }; - SupportedStreamConfig { - channels: asbd.mChannelsPerFrame as u16, - sample_rate: SampleRate(asbd.mSampleRate as u32), - buffer_size: buffer_size.clone(), - sample_format: SUPPORTED_SAMPLE_FORMAT, +/// Setup audio unit with common configuration for input or output streams. +fn setup_stream_audio_unit( + config: &StreamConfig, + sample_format: SampleFormat, + is_input: bool, +) -> Result { + // Configure buffer size via AVAudioSession + if let BufferSize::Fixed(buffer_size) = config.buffer_size { + set_audio_session_buffer_size(buffer_size, config.sample_rate)?; + } + + let mut audio_unit = create_audio_unit()?; + + if is_input { + audio_unit.uninitialize()?; + configure_for_recording(&mut audio_unit)?; + audio_unit.initialize()?; } + + // Set the stream format in interleaved mode + // For input: Output scope of Input element (data coming out of input) + // For output: Input scope of Output element (data going into output) + let (scope, element) = if is_input { + (Scope::Output, Element::Input) + } else { + (Scope::Input, Element::Output) + }; + + let asbd = asbd_from_config(config, sample_format); + audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; + + Ok(audio_unit) } -/// Query the current device buffer frame size from CoreAudio. +/// Extract AudioBuffer and convert to Data, handling differences between input and output. /// -/// On iOS, this queries the RemoteIO audio unit which acts as a proxy to the hardware. -/// RemoteIO uses Global scope because it represents the system-wide audio session, -/// not a specific hardware device like on macOS. -fn get_device_buffer_frame_size(audio_unit: &AudioUnit) -> Result { - // For iOS RemoteIO, we query the global scope since RemoteIO represents - // the system audio session rather than direct hardware access - audio_unit.get_property::( - kAudioDevicePropertyBufferFrameSize, - Scope::Global, - Element::Output, - ) +/// # Safety +/// +/// Caller must ensure: +/// - `args.data.data` points to valid AudioBufferList +/// - For input: AudioBufferList has at least one buffer +/// - Buffer data remains valid for the callback duration +#[inline] +unsafe fn extract_audio_buffer( + args: &render_callback::Args, + bytes_per_channel: usize, + sample_format: SampleFormat, + is_input: bool, +) -> (AudioBuffer, Data) { + let buffer = if is_input { + // Input: access through buffer array + let ptr = (*args.data.data).mBuffers.as_ptr() as *const AudioBuffer; + let len = (*args.data.data).mNumberBuffers as usize; + let buffers: &[AudioBuffer] = slice::from_raw_parts(ptr, len); + buffers[0] + } else { + // Output: direct access + (*args.data.data).mBuffers[0] + }; + + let data = buffer.mData as *mut (); + let len = (buffer.mDataByteSize as usize / bytes_per_channel) as usize; + let data = Data::from_parts(data, len, sample_format); + + (buffer, data) +} + +/// Setup input callback with proper latency calculation. +fn setup_input_callback( + audio_unit: &mut AudioUnit, + sample_format: SampleFormat, + sample_rate: SampleRate, + device_buffer_frames: Option, + mut data_callback: D, + mut error_callback: E, +) -> Result<(), BuildStreamError> +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + let bytes_per_channel = sample_format.sample_size(); + type Args = render_callback::Args; + + audio_unit.set_input_callback(move |args: Args| { + // SAFETY: CoreAudio provides valid AudioBufferList for the callback duration + let (buffer, data) = + unsafe { extract_audio_buffer(&args, bytes_per_channel, sample_format, true) }; + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + error_callback(err.into()); + return Err(()); + } + Ok(cb) => cb, + }; + + let latency_frames = + device_buffer_frames.unwrap_or_else(|| data.len() / buffer.mNumberChannels as usize); + let delay = frames_to_duration(latency_frames, sample_rate); + let capture = callback + .sub(delay) + .expect("`capture` occurs before origin of alsa `StreamInstant`"); + let timestamp = crate::InputStreamTimestamp { callback, capture }; + + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); + Ok(()) + })?; + + Ok(()) +} + +/// Setup output callback with proper latency calculation. +fn setup_output_callback( + audio_unit: &mut AudioUnit, + sample_format: SampleFormat, + sample_rate: SampleRate, + device_buffer_frames: Option, + mut data_callback: D, + mut error_callback: E, +) -> Result<(), BuildStreamError> +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + let bytes_per_channel = sample_format.sample_size(); + type Args = render_callback::Args; + + audio_unit.set_render_callback(move |args: Args| { + // SAFETY: CoreAudio provides valid AudioBufferList for the callback duration + let (buffer, mut data) = + unsafe { extract_audio_buffer(&args, bytes_per_channel, sample_format, false) }; + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + error_callback(err.into()); + return Err(()); + } + Ok(cb) => cb, + }; + + let latency_frames = + device_buffer_frames.unwrap_or_else(|| data.len() / buffer.mNumberChannels as usize); + let delay = frames_to_duration(latency_frames, sample_rate); + let playback = callback + .add(delay) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + Ok(()) + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::{BufferSize, SampleRate, StreamConfig}; + + #[test] + fn test_ios_fixed_buffer_size() { + let host = crate::default_host(); + let device = host.default_output_device().unwrap(); + + let config = StreamConfig { + channels: 2, + sample_rate: SampleRate(48000), + buffer_size: BufferSize::Fixed(512), + }; + + let result = device.build_output_stream( + &config, + |_data: &mut [f32], _info: &crate::OutputCallbackInfo| {}, + |_err| {}, + None, + ); + + assert!( + result.is_ok(), + "BufferSize::Fixed should be supported on iOS via AVAudioSession" + ); + } } diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 8b44ae67f..337bb0f0c 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -20,13 +20,12 @@ use objc2_core_audio::kAudioDevicePropertyDeviceUID; use objc2_core_audio::kAudioObjectPropertyElementMain; use objc2_core_audio::{ kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyBufferFrameSize, - kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyDeviceIsAlive, - kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertyStreamConfiguration, - kAudioDevicePropertyStreamFormat, kAudioObjectPropertyElementMaster, - kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, AudioDeviceID, AudioObjectGetPropertyData, - AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress, - AudioObjectPropertyScope, AudioObjectSetPropertyData, + kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyNominalSampleRate, + kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyStreamFormat, + kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, AudioDeviceID, + AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID, + AudioObjectPropertyAddress, AudioObjectPropertyScope, AudioObjectSetPropertyData, }; use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, @@ -40,12 +39,12 @@ pub use super::enumerate::{ use std::fmt; use std::mem::{self}; use std::ptr::{null, NonNull}; -use std::rc::Rc; use std::slice; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use super::invoke_error_callback; use super::property_listener::AudioObjectPropertyListener; use coreaudio::audio_unit::macos_helpers::get_device_name; @@ -258,45 +257,6 @@ fn get_io_buffer_frame_size_range( }) } -/// Register the on-disconnect callback. -/// This will both stop the stream and call the error callback with DeviceNotAvailable. -/// This function should only be called once per stream. -fn add_disconnect_listener( - stream: &Stream, - error_callback: Arc>, -) -> Result<(), BuildStreamError> -where - E: FnMut(StreamError) + Send + 'static, -{ - let stream_inner_weak = Rc::downgrade(&stream.inner); - let mut stream_inner = stream.inner.borrow_mut(); - stream_inner._disconnect_listener = Some(AudioObjectPropertyListener::new( - stream_inner.device_id, - AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyDeviceIsAlive, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMaster, - }, - move || { - if let Some(stream_inner_strong) = stream_inner_weak.upgrade() { - match stream_inner_strong.try_borrow_mut() { - Ok(mut stream_inner) => { - let _ = stream_inner.pause(); - } - Err(_) => { - // Could not acquire mutable borrow. This can occur if there are - // overlapping borrows, if the stream is already in use, or if a panic - // occurred during a previous borrow. Still notify about device - // disconnection even if we can't pause. - } - } - (error_callback.lock().unwrap())(StreamError::DeviceNotAvailable); - } - }, - )?); - Ok(()) -} - impl DeviceTrait for Device { type SupportedInputConfigs = SupportedInputConfigs; type SupportedOutputConfigs = SupportedOutputConfigs; @@ -757,16 +717,14 @@ impl Device { type Args = render_callback::Args; audio_unit.set_input_callback(move |args: Args| unsafe { - let ptr = (*args.data.data).mBuffers.as_ptr(); - let len = (*args.data.data).mNumberBuffers as usize; - let buffers: &[AudioBuffer] = slice::from_raw_parts(ptr, len); - - // TODO: Perhaps loop over all buffers instead? + // SAFETY: We configure the stream format as interleaved (via asbd_from_config which + // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has + // exactly one buffer containing all channels, so mBuffers[0] is always valid. let AudioBuffer { mNumberChannels: channels, mDataByteSize: data_byte_size, mData: data, - } = buffers[0]; + } = (*args.data.data).mBuffers[0]; let data = data as *mut (); let len = data_byte_size as usize / bytes_per_channel; @@ -774,7 +732,7 @@ impl Device { let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { Err(err) => { - (error_callback.lock().unwrap())(err.into()); + invoke_error_callback(&error_callback, err.into()); return Err(()); } Ok(cb) => cb, @@ -797,21 +755,36 @@ impl Device { Ok(()) })?; - let stream = Stream::new(StreamInner { - playing: true, - _disconnect_listener: None, - audio_unit, - device_id: self.audio_device_id, - _loopback_device: loopback_aggregate, - }); - - // If we didn't request the default device, stop the stream if the - // device disconnects. - if !is_default_device(self) { - add_disconnect_listener(&stream, error_callback_disconnect)?; - } + // Create error callback for stream - either dummy or real based on device type + let error_callback_for_stream: super::ErrorCallback = if is_default_device(self) { + Box::new(|_: StreamError| {}) + } else { + let error_callback_clone = error_callback_disconnect.clone(); + Box::new(move |err: StreamError| { + invoke_error_callback(&error_callback_clone, err); + }) + }; + + let stream = Stream::new( + StreamInner { + playing: true, + audio_unit, + device_id: self.audio_device_id, + _loopback_device: loopback_aggregate, + }, + error_callback_for_stream, + )?; - stream.inner.borrow_mut().audio_unit.start()?; + stream + .inner + .lock() + .map_err(|_| BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })? + .audio_unit + .start()?; Ok(stream) } @@ -847,9 +820,9 @@ impl Device { type Args = render_callback::Args; audio_unit.set_render_callback(move |args: Args| unsafe { - // If `run()` is currently running, then a callback will be available from this list. - // Otherwise, we just fill the buffer with zeroes and return. - + // SAFETY: We configure the stream format as interleaved (via asbd_from_config which + // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has + // exactly one buffer containing all channels, so mBuffers[0] is always valid. let AudioBuffer { mNumberChannels: channels, mDataByteSize: data_byte_size, @@ -862,7 +835,7 @@ impl Device { let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { Err(err) => { - (error_callback.lock().unwrap())(err.into()); + invoke_error_callback(&error_callback, err.into()); return Err(()); } Ok(cb) => cb, @@ -885,21 +858,36 @@ impl Device { Ok(()) })?; - let stream = Stream::new(StreamInner { - playing: true, - _disconnect_listener: None, - audio_unit, - device_id: self.audio_device_id, - _loopback_device: None, - }); - - // If we didn't request the default device, stop the stream if the - // device disconnects. - if !is_default_device(self) { - add_disconnect_listener(&stream, error_callback_disconnect)?; - } + // Create error callback for stream - either dummy or real based on device type + let error_callback_for_stream: super::ErrorCallback = if is_default_device(self) { + Box::new(|_: StreamError| {}) + } else { + let error_callback_clone = error_callback_disconnect.clone(); + Box::new(move |err: StreamError| { + invoke_error_callback(&error_callback_clone, err); + }) + }; - stream.inner.borrow_mut().audio_unit.start()?; + let stream = Stream::new( + StreamInner { + playing: true, + audio_unit, + device_id: self.audio_device_id, + _loopback_device: None, + }, + error_callback_for_stream, + )?; + + stream + .inner + .lock() + .map_err(|_| BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })? + .audio_unit + .start()?; Ok(stream) } @@ -910,7 +898,6 @@ impl Device { /// This handles the common setup tasks for both input and output streams: /// - Sets the stream format (ASBD) /// - Configures buffer size for Fixed buffer size requests -/// - Validates buffer size ranges fn configure_stream_format_and_buffer( audio_unit: &mut AudioUnit, config: &StreamConfig, @@ -926,14 +913,6 @@ fn configure_stream_format_and_buffer( // Configure device buffer size if requested if let BufferSize::Fixed(buffer_size) = config.buffer_size { - let buffer_size_range = get_io_buffer_frame_size_range(audio_unit)?; - - if let SupportedBufferSize::Range { min, max } = buffer_size_range { - if !(min..=max).contains(&buffer_size) { - return Err(BuildStreamError::StreamConfigNotSupported); - } - } - // IMPORTANT: Buffer frame size is a DEVICE-LEVEL property, not stream-specific. // Unlike stream format above, we ALWAYS use Scope::Global + Element::Output // for device properties, regardless of whether this is an input or output stream. diff --git a/src/host/coreaudio/macos/enumerate.rs b/src/host/coreaudio/macos/enumerate.rs index 806044883..4f337352f 100644 --- a/src/host/coreaudio/macos/enumerate.rs +++ b/src/host/coreaudio/macos/enumerate.rs @@ -73,9 +73,6 @@ impl Devices { } } -unsafe impl Send for Devices {} -unsafe impl Sync for Devices {} - impl Iterator for Devices { type Item = Device; fn next(&mut self) -> Option { diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index e29d05768..a7a025166 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -7,11 +7,14 @@ use crate::traits::{HostTrait, StreamTrait}; use crate::{BackendSpecificError, DevicesError, PauseStreamError, PlayStreamError}; use coreaudio::audio_unit::AudioUnit; use objc2_core_audio::AudioDeviceID; -use std::cell::RefCell; -use std::rc::Rc; +use std::sync::{mpsc, Arc, Mutex, Weak}; pub use self::enumerate::{default_input_device, default_output_device, Devices}; +use objc2_core_audio::{ + kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyElementMain, + kAudioObjectPropertyScopeGlobal, AudioObjectPropertyAddress, +}; use property_listener::AudioObjectPropertyListener; mod device; @@ -52,11 +55,123 @@ impl HostTrait for Host { } } +/// Type alias for the error callback to reduce complexity +type ErrorCallback = Box; + +/// Invoke error callback, recovering from poisoned mutex if needed. +/// Returns true if callback was invoked, false if skipped due to WouldBlock. +#[inline] +fn invoke_error_callback(error_callback: &Arc>, err: crate::StreamError) -> bool +where + E: FnMut(crate::StreamError) + Send, +{ + match error_callback.try_lock() { + Ok(mut cb) => { + cb(err); + true + } + Err(std::sync::TryLockError::Poisoned(guard)) => { + // Recover from poisoned lock to still report this error + guard.into_inner()(err); + true + } + Err(std::sync::TryLockError::WouldBlock) => { + // Skip if callback is busy + false + } + } +} + +/// Manages device disconnection listener on a dedicated thread to ensure the +/// AudioObjectPropertyListener is always created and dropped on the same thread. +/// This avoids potential threading issues with CoreAudio APIs. +/// +/// When a device disconnects, this manager: +/// 1. Attempts to pause the stream to stop audio I/O +/// 2. Calls the error callback with `StreamError::DeviceNotAvailable` +/// +/// The dedicated thread architecture ensures `Stream` can implement `Send`. +struct DisconnectManager { + _shutdown_tx: mpsc::Sender<()>, +} + +impl DisconnectManager { + /// Create a new DisconnectManager that monitors device disconnection on a dedicated thread + fn new( + device_id: AudioDeviceID, + stream_weak: Weak>, + error_callback: Arc>, + ) -> Result { + let (shutdown_tx, shutdown_rx) = mpsc::channel(); + let (disconnect_tx, disconnect_rx) = mpsc::channel(); + let (ready_tx, ready_rx) = mpsc::channel(); + + // Spawn dedicated thread to own the AudioObjectPropertyListener + let disconnect_tx_clone = disconnect_tx.clone(); + std::thread::spawn(move || { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceIsAlive, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + // Create the listener on this dedicated thread + match AudioObjectPropertyListener::new(device_id, property_address, move || { + let _ = disconnect_tx_clone.send(()); + }) { + Ok(_listener) => { + let _ = ready_tx.send(Ok(())); + // Drop the listener on this thread after receiving a shutdown signal + let _ = shutdown_rx.recv(); + } + Err(e) => { + let _ = ready_tx.send(Err(e)); + } + } + }); + + // Wait for listener creation to complete or fail + ready_rx + .recv() + .map_err(|_| crate::BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: "Disconnect listener thread terminated unexpectedly".to_string(), + }, + })??; + + // Handle disconnect events on the main thread pool + let stream_weak_clone = stream_weak.clone(); + let error_callback_clone = error_callback.clone(); + std::thread::spawn(move || { + while disconnect_rx.recv().is_ok() { + // Check if stream still exists + if let Some(stream_arc) = stream_weak_clone.upgrade() { + // First, try to pause the stream to stop playback + if let Ok(mut stream_inner) = stream_arc.try_lock() { + let _ = stream_inner.pause(); + } + + // Always try to notify about device disconnection + invoke_error_callback( + &error_callback_clone, + crate::StreamError::DeviceNotAvailable, + ); + } else { + // Stream is gone, exit the handler thread + break; + } + } + }); + + Ok(DisconnectManager { + _shutdown_tx: shutdown_tx, + }) + } +} + struct StreamInner { playing: bool, audio_unit: AudioUnit, - /// Manage the lifetime of the closure that handles device disconnection. - _disconnect_listener: Option, // Track the device with which the audio unit was spawned. // // We must do this so that we can avoid changing the device sample rate if there is already @@ -94,28 +209,55 @@ impl StreamInner { } } -#[derive(Clone)] pub struct Stream { - inner: Rc>, + inner: Arc>, + // Manages the device disconnection listener separately to allow Stream to be Send. + // The DisconnectManager contains the non-Send AudioObjectPropertyListener. + _disconnect_manager: DisconnectManager, } impl Stream { - fn new(inner: StreamInner) -> Self { - Self { - inner: Rc::new(RefCell::new(inner)), - } + fn new( + inner: StreamInner, + error_callback: ErrorCallback, + ) -> Result { + let device_id = inner.device_id; + let inner_arc = Arc::new(Mutex::new(inner)); + let weak_inner = Arc::downgrade(&inner_arc); + + let error_callback = Arc::new(Mutex::new(error_callback)); + let disconnect_manager = DisconnectManager::new(device_id, weak_inner, error_callback)?; + + Ok(Self { + inner: inner_arc, + _disconnect_manager: disconnect_manager, + }) } } impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { - let mut stream = self.inner.borrow_mut(); + let mut stream = self + .inner + .lock() + .map_err(|_| PlayStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; stream.play() } fn pause(&self) -> Result<(), PauseStreamError> { - let mut stream = self.inner.borrow_mut(); + let mut stream = self + .inner + .lock() + .map_err(|_| PauseStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; stream.pause() } diff --git a/src/host/coreaudio/mod.rs b/src/host/coreaudio/mod.rs index 0c09249fe..45f92661c 100644 --- a/src/host/coreaudio/mod.rs +++ b/src/host/coreaudio/mod.rs @@ -68,6 +68,7 @@ fn asbd_from_config( } } +#[inline] fn host_time_to_stream_instant( m_host_time: u64, ) -> Result { @@ -81,6 +82,7 @@ fn host_time_to_stream_instant( } // Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +#[inline] fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { let secsf = frames as f64 / rate.0 as f64; let secs = secsf as u64; @@ -121,3 +123,6 @@ impl From for DefaultStreamConfigError { } pub(crate) type OSStatus = i32; + +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index b3cdd54d0..b8efb3bc5 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -34,6 +34,12 @@ pub struct Stream { audio_ctxt: AudioContext, } +// WASM runs in a single-threaded environment, so Send is safe by design. +unsafe impl Send for Stream {} + +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + // Index within the `streams` array of the events loop. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct StreamId(usize); @@ -206,11 +212,10 @@ impl DeviceTrait for Device { let buffer_size_frames = match config.buffer_size { BufferSize::Fixed(v) => { - if v == 0 { + if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { return Err(BuildStreamError::StreamConfigNotSupported); - } else { - v as usize } + v as usize } BufferSize::Default => DEFAULT_BUFFER_SIZE, }; diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 3d7175d98..adedc288a 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -139,6 +139,20 @@ impl Device { pub fn is_output(&self) -> bool { matches!(self.device_type, DeviceType::OutputDevice) } + + /// Validate buffer size if Fixed is specified. This is necessary because JACK buffer size + /// is controlled by the JACK server and cannot be changed by clients. Without validation, + /// cpal would silently use the server's buffer size even if a different value was requested. + fn validate_buffer_size(&self, conf: &StreamConfig) -> Result<(), BuildStreamError> { + if let crate::BufferSize::Fixed(requested_size) = conf.buffer_size { + if let SupportedBufferSize::Range { min, max } = self.buffer_size { + if !(min..=max).contains(&requested_size) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + } + Ok(()) + } } impl DeviceTrait for Device { @@ -199,6 +213,8 @@ impl DeviceTrait for Device { if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { return Err(BuildStreamError::StreamConfigNotSupported); } + self.validate_buffer_size(conf)?; + // The settings should be fine, create a Client let client_options = super::get_client_options(self.start_server_automatically); let client; @@ -238,6 +254,7 @@ impl DeviceTrait for Device { if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { return Err(BuildStreamError::StreamConfigNotSupported); } + self.validate_buffer_size(conf)?; // The settings should be fine, create a Client let client_options = super::get_client_options(self.start_server_automatically); diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 0d48530aa..a77c3240e 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -21,6 +21,9 @@ pub struct Stream { output_port_names: Vec, } +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + impl Stream { // TODO: Return error messages pub fn new_input( diff --git a/src/host/mod.rs b/src/host/mod.rs index 0c61a5910..1aecf93ab 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -28,3 +28,13 @@ pub(crate) mod null; pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] pub(crate) mod webaudio; + +/// Compile-time assertion that a type implements Send. +/// Use this macro in each host module to ensure Stream is Send. +#[macro_export] +macro_rules! assert_stream_send { + ($t:ty) => { + const fn _assert_stream_send() {} + const _: () = _assert_stream_send::<$t>(); + }; +} diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 031911ba3..ba71635b1 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -19,6 +19,9 @@ pub struct Host; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Stream; +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + #[derive(Clone)] pub struct SupportedInputConfigs; #[derive(Clone)] diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index c83583f34..3086582f6 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -575,6 +575,8 @@ impl Device { } }; + // Note: Buffer size validation is not needed here - `IAudioClient::Initialize` + // will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported. let buffer_duration = buffer_size_to_duration(&config.buffer_size, config.sample_rate.0); @@ -690,6 +692,8 @@ impl Device { .build_audioclient() .map_err(windows_err_to_cpal_err::)?; + // Note: Buffer size validation is not needed here - `IAudioClient::Initialize` + // will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported. let buffer_duration = buffer_size_to_duration(&config.buffer_size, config.sample_rate.0); diff --git a/src/host/wasapi/stream.rs b/src/host/wasapi/stream.rs index 6b2d1c207..81f73d53c 100644 --- a/src/host/wasapi/stream.rs +++ b/src/host/wasapi/stream.rs @@ -31,6 +31,13 @@ pub struct Stream { pending_scheduled_event: Foundation::HANDLE, } +// Windows Event HANDLEs are safe to send between threads - they are designed for synchronization. +// See: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa +unsafe impl Send for Stream {} + +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + struct RunContext { // Streams that have been created in this event loop. stream: StreamInner, diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index e9152ed3f..29be46b80 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -32,6 +32,12 @@ pub struct Stream { buffer_size_frames: usize, } +// WASM runs in a single-threaded environment, so Send is safe by design. +unsafe impl Send for Stream {} + +// Compile-time assertion that Stream is Send +crate::assert_stream_send!(Stream); + pub type SupportedInputConfigs = ::std::vec::IntoIter; pub type SupportedOutputConfigs = ::std::vec::IntoIter; @@ -214,11 +220,10 @@ impl DeviceTrait for Device { let buffer_size_frames = match config.buffer_size { BufferSize::Fixed(v) => { - if v == 0 { + if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { return Err(BuildStreamError::StreamConfigNotSupported); - } else { - v as usize } + v as usize } BufferSize::Default => DEFAULT_BUFFER_SIZE, }; diff --git a/src/lib.rs b/src/lib.rs index eca1e963c..149bae1c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,11 +182,14 @@ pub mod platform; mod samples_formats; pub mod traits; +/// Iterator of devices wrapped in a filter to only include certain device types +pub type DevicesFiltered = std::iter::Filter::Item) -> bool>; + /// A host's device iterator yielding only *input* devices. -pub type InputDevices = std::iter::Filter::Item) -> bool>; +pub type InputDevices = DevicesFiltered; /// A host's device iterator yielding only *output* devices. -pub type OutputDevices = std::iter::Filter::Item) -> bool>; +pub type OutputDevices = DevicesFiltered; /// Number of channels. pub type ChannelCount = u16; From ab3b8a2b9fbda128d31350467a5e363758069c2e Mon Sep 17 00:00:00 2001 From: xephyris <114379647+GoeyGreen@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:26:04 -0700 Subject: [PATCH 28/30] fix: add Stream import to coreaudio --- src/host/coreaudio/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/host/coreaudio/mod.rs b/src/host/coreaudio/mod.rs index 45f92661c..f49d2d63b 100644 --- a/src/host/coreaudio/mod.rs +++ b/src/host/coreaudio/mod.rs @@ -3,6 +3,7 @@ use objc2_core_audio_types::{ kAudioFormatLinearPCM, AudioStreamBasicDescription, }; +use crate::host::coreaudio::macos::Stream; use crate::DefaultStreamConfigError; use crate::{BuildStreamError, SupportedStreamConfigsError}; From 7bc3c4fb38065308f3d8b0e4868fbb72586a1307 Mon Sep 17 00:00:00 2001 From: xephyris <114379647+xephyris@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:58:25 -0700 Subject: [PATCH 29/30] fix: return backend error on parse error for PWSTR --- src/host/wasapi/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 3086582f6..967855c70 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -331,7 +331,7 @@ impl Device { match self.device.GetId() { Ok(pwstr) => match pwstr.to_string() { Ok(id_str) => Ok(DeviceId::WASAPI(id_str)), - Err(_e) => Err(DeviceIdError::ParseError), + Err(e) => Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { description: format!("Failed to convert device ID to string: {}", e) }, }), }, Err(e) => Err(DeviceIdError::BackendSpecific { err: e.into() }), } From 3580a676ad5bf88b22f8c6b32dcebabb4ebf983c Mon Sep 17 00:00:00 2001 From: xephyris <114379647+xephyris@users.noreply.github.com> Date: Mon, 13 Oct 2025 23:01:39 -0700 Subject: [PATCH 30/30] fmt: run cargo fmt --- src/host/wasapi/device.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 967855c70..a3a60b967 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -331,7 +331,11 @@ impl Device { match self.device.GetId() { Ok(pwstr) => match pwstr.to_string() { Ok(id_str) => Ok(DeviceId::WASAPI(id_str)), - Err(e) => Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { description: format!("Failed to convert device ID to string: {}", e) }, }), + Err(e) => Err(DeviceIdError::BackendSpecific { + err: BackendSpecificError { + description: format!("Failed to convert device ID to string: {}", e), + }, + }), }, Err(e) => Err(DeviceIdError::BackendSpecific { err: e.into() }), }