Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8cc6f25
feat: add device_id support for macOS to DeviceTrait
xephyris Sep 14, 2025
9161bbb
fix: fix import statements for other APIs
xephyris Sep 15, 2025
845baac
wip: use DeviceId enum instead to work around conflicting device_id i…
xephyris Sep 16, 2025
bd9b071
feat: transition to using DeviceId for enum for better cross compatib…
xephyris Sep 17, 2025
60c3c26
docs: add description for DeviceId
xephyris Sep 18, 2025
2a93a70
feat: implemented device id for windows wasapi
xephyris Sep 20, 2025
fb1efd0
docs: update description
xephyris Sep 23, 2025
626b823
fix: reformat existing code and fix android build error
xephyris Sep 24, 2025
c9d6b05
feat: add ALSA support to device id() function
xephyris Sep 24, 2025
6f8587f
fix: resolve merge conflicts
xephyris Sep 25, 2025
24ef5ac
feat: add support for jack and aaudio (untested). fix naming structur…
xephyris Sep 25, 2025
8b5b8d0
docs: update changelog
xephyris Sep 25, 2025
068e171
fix: fix function names causing compile errors
xephyris Sep 25, 2025
f9301b8
fix: fix aaudio DeviceId type
xephyris Sep 25, 2025
2972b52
feat: implement macos DeviceId to use kAudioDevicePropertyDeviceUID …
xephyris Sep 26, 2025
99fc8b7
fmt: reformat with rustfmt
xephyris Sep 26, 2025
43bc3e0
docs: update changelog
xephyris Sep 27, 2025
6e93a1b
feat: add asio support and reformat macos id function
xephyris Sep 27, 2025
59f4a9f
fmt: reformat code
xephyris Sep 27, 2025
b811266
feat: return default for ios, emscripten, and webaudio
xephyris Sep 27, 2025
eefc6ef
fix: Merge branch 'master' into master
xephyris Sep 29, 2025
f40bb9d
feat: add in rest of audio APIs to from_str()
xephyris Sep 29, 2025
492a98e
fix: Merge branch 'master' into master
xephyris Sep 30, 2025
30645a5
fix: change catch-all to todo!
xephyris Oct 1, 2025
44ec96b
fix: resolve parse error on null and deviceid implementations
xephyris Oct 3, 2025
01a240d
docs: fix changelog
xephyris Oct 7, 2025
6eb9e7a
refactor: improve safety of get_id() function, remove CFStringRef
xephyris Oct 10, 2025
d516476
fmt: run rustfmt
xephyris Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

- 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.
- AAudio: Configure buffer to ensure consistent callback buffer sizes.
Expand Down
29 changes: 29 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ impl From<BackendSpecificError> for DevicesError {
}
}

/// An error that may occur while attempting to retrieve a device id.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DeviceIdError {
/// See the [`BackendSpecificError`] docs for more information about this error variant.
BackendSpecific {
err: BackendSpecificError,
},
UnsupportedPlatform,
ParseError,
}

impl Display for DeviceIdError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BackendSpecific { err } => err.fmt(f),
Self::UnsupportedPlatform => f.write_str("Device ids are unsupported for this OS"),
Self::ParseError => f.write_str("Failed to parse the device_id"),
}
}
}

impl Error for DeviceIdError {}

impl From<BackendSpecificError> 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 {
Expand Down
15 changes: 11 additions & 4 deletions src/host/aaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ use java_interface::{AudioDeviceDirection, AudioDeviceInfo};

use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::{
BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError,
DeviceNameError, 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,
};

Expand Down Expand Up @@ -329,6 +329,13 @@ impl DeviceTrait for Device {
}
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
match &self.0 {
None => Ok(DeviceId::AAudio(-1)), // Default device
Some(info) => Ok(DeviceId::AAudio(info.id)),
}
}

fn supported_input_configs(
&self,
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
Expand Down
15 changes: 12 additions & 3 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ 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,
OutputCallbackInfo, PauseStreamError, PlayStreamError, Sample, SampleFormat, SampleRate,
StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig,
DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount,
InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, Sample, SampleFormat,
SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig,
SupportedStreamConfigRange, SupportedStreamConfigsError, I24, U24,
};

Expand Down Expand Up @@ -96,6 +96,10 @@ impl DeviceTrait for Device {
Device::name(self)
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Device::id(self)
}

fn supported_input_configs(
&self,
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
Expand Down Expand Up @@ -347,6 +351,11 @@ impl Device {
Ok(self.to_string())
}

#[inline]
fn id(&self) -> Result<DeviceId, DeviceIdError> {
Ok(DeviceId::ALSA(self.pcm_id.clone()))
}

fn supported_configs(
&self,
stream_t: alsa::Direction,
Expand Down
6 changes: 6 additions & 0 deletions src/host/asio/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pub type SupportedOutputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>
use super::sys;
use crate::BackendSpecificError;
use crate::DefaultStreamConfigError;
use crate::DeviceId;
use crate::DeviceIdError;
use crate::DeviceNameError;
use crate::DevicesError;
use crate::SampleFormat;
Expand Down Expand Up @@ -54,6 +56,10 @@ impl Device {
Ok(self.driver.name().to_string())
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Ok(DeviceId::ASIO(self.driver.name().to_string()))
}

/// Gets the supported input configs.
/// TODO currently only supports the default.
/// Need to find all possible configs.
Expand Down
10 changes: 7 additions & 3 deletions src/host/asio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ extern crate asio_sys as sys;

use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::{
BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, 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};
Expand Down Expand Up @@ -62,6 +62,10 @@ impl DeviceTrait for Device {
Device::name(self)
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Device::id(self)
}

fn supported_input_configs(
&self,
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
Expand Down
18 changes: 14 additions & 4 deletions src/host/coreaudio/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,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,
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::{
Expand Down Expand Up @@ -88,6 +89,10 @@ impl Device {
Ok("Default Device".to_owned())
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Ok(DeviceId::IOS("default".to_string()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"default" differs from "Default Device" in name. Maybe use shared const?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly unsure about this change, as in most conventions an id would not have spaces inside of it, so I feel like "default" does a better job of this. DeviceIds and names also do not intermix so I don't believe this will be much of an issue either.

Of course if the general consensus is that "Default Device" is better than "default", I would be happy to change it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good rationale, let’s keep it like it this then.

How do you feel this’ll be for ALSA? Spaces in device names (e.g. for USB devices) would not be impossible.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we are to maintain no spaces in the id, then we could maybe just replace all the spaces with - dashes or _ underscores.

From what I've seen and researched though, it seems like the hw:CARD="CARD_ID",DEV="DEVICE_NUM" style in self.pcm_id cannot contain spaces, as they are not permitted in card ids. Do you have any examples of such a case with spaces, or does the alsa implementation in cpal use card names not card ids?

My research in this is very basic, so what I'm saying may not be completely accurate.

}

#[inline]
fn supported_input_configs(
&self,
Expand Down Expand Up @@ -154,6 +159,11 @@ impl DeviceTrait for Device {
Device::name(self)
}

#[inline]
fn id(&self) -> Result<DeviceId, DeviceIdError> {
Device::id(self)
}

#[inline]
fn supported_input_configs(
&self,
Expand Down
53 changes: 50 additions & 3 deletions src/host/coreaudio/macos/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ use crate::host::coreaudio::macos::StreamInner;
use crate::traits::DeviceTrait;
use crate::{
BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data,
DefaultStreamConfigError, DeviceNameError, InputCallbackInfo, OutputCallbackInfo, SampleFormat,
SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig,
SupportedStreamConfigRange, SupportedStreamConfigsError,
DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, InputCallbackInfo,
OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize,
SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
};
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,
Expand All @@ -29,6 +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,
Expand All @@ -44,6 +48,7 @@ use std::time::{Duration, Instant};

use super::property_listener::AudioObjectPropertyListener;
use coreaudio::audio_unit::macos_helpers::get_device_name;

/// 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(
Expand Down Expand Up @@ -301,6 +306,10 @@ impl DeviceTrait for Device {
Device::name(self)
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Device::id(self)
}

fn supported_input_configs(
&self,
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
Expand Down Expand Up @@ -395,6 +404,44 @@ impl Device {
})
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
let property_address = AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyDeviceUID,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
};

// CFString is retained by the audio object, use wrap_under_get_rule
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quote from docs:

A CFString that contains a persistent identifier for the AudioDevice. An
AudioDevice's UID is persistent across boots. The content of the UID string
is a black box and may contain information that is unique to a particular
instance of an AudioDevice's hardware or unique to the CPU. Therefore they
are not suitable for passing between CPUs or for identifying similar models
of hardware. The caller is responsible for releasing the returned CFObject.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xephyris it is opposite. Even if uid is retained by device it will create copy when you get it.

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,
NonNull::from(&property_address),
0,
null(),
NonNull::from(&data_size),
NonNull::from(&mut uid).cast(),
)
};
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).to_string() };
Ok(DeviceId::CoreAudio(uid_string))
} else {
Err(DeviceIdError::BackendSpecific {
err: BackendSpecificError {
description: "Device UID is null".to_string(),
},
})
}
}

// Logic re-used between `supported_input_configs` and `supported_output_configs`.
#[allow(clippy::cast_ptr_alignment)]
fn supported_configs(
Expand Down
5 changes: 1 addition & 4 deletions src/host/coreaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 13 additions & 4 deletions src/host/emscripten/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use web_sys::AudioContext;

use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::{
BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, 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`.
Expand Down Expand Up @@ -69,6 +69,11 @@ impl Device {
Ok("Default Device".to_owned())
}

#[inline]
fn id(&self) -> Result<DeviceId, DeviceIdError> {
Ok(DeviceId::Emscripten("default".to_string()))
}

#[inline]
fn supported_input_configs(
&self,
Expand Down Expand Up @@ -144,6 +149,10 @@ impl DeviceTrait for Device {
Device::name(self)
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Device::id(self)
}

fn supported_input_configs(
&self,
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
Expand Down
16 changes: 12 additions & 4 deletions src/host/jack/device.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::traits::DeviceTrait;
use crate::{
BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError,
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;
Expand Down Expand Up @@ -64,6 +64,10 @@ impl Device {
}
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Ok(DeviceId::Jack(self.name.clone()))
}

pub fn default_output_device(
name: &str,
connect_ports_automatically: bool,
Expand Down Expand Up @@ -146,6 +150,10 @@ impl DeviceTrait for Device {
Ok(self.name.clone())
}

fn id(&self) -> Result<DeviceId, DeviceIdError> {
Device::id(self)
}

fn supported_input_configs(
&self,
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
Expand Down
11 changes: 8 additions & 3 deletions src/host/null/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ 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,
BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError,
DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError,
SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange,
SupportedStreamConfigsError,
};

Expand Down Expand Up @@ -47,6 +47,11 @@ impl DeviceTrait for Device {
Ok("null".to_owned())
}

#[inline]
fn id(&self) -> Result<DeviceId, DeviceIdError> {
Ok(DeviceId::Null)
}

#[inline]
fn supported_input_configs(
&self,
Expand Down
Loading