From 986146ef1c3ff23fa8b84bd6ae2de0520bafc516 Mon Sep 17 00:00:00 2001 From: Jonathan PHILIPPE Date: Sun, 30 Aug 2026 14:45:51 +0200 Subject: [PATCH] feat(coreaudio): report device transport as InterfaceType interface_type() was only set for aggregate devices on macOS; everything else, including built-in microphones and USB interfaces, reported Unknown. kAudioDevicePropertyTransportType names the connection, and InterfaceType already has variants for nearly every value it returns. The WASAPI backend fills the same field from its endpoint properties, so this closes an asymmetry rather than adding an API. is_aggregate_device() stays as a fallback: TransportType reports "grup" for aggregates too, and the two agree where both apply. --- CHANGELOG.md | 1 + src/host/coreaudio/macos/device.rs | 71 +++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 679a956ef..b82b0d826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `InputStreamTimestamp`/`OutputStreamTimestamp` merged into `StreamTimestamp`; `capture`/`playback` renamed `device`. - Renamed the `wasm-beep` and `audioworklet-beep` examples to `webaudio` and `audioworklet`. - **ALSA**: Update `alsa` dependency to 0.12. +- **CoreAudio**: `DeviceDescription::interface_type()` now reports the device transport (built-in, USB, Bluetooth, Thunderbolt, HDMI, and others) instead of only marking aggregate devices. - **Linux**: `realtime` can now promote threads without requiring `realtime-dbus`. ### Deprecated diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index bde36af6b..f146f74e6 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -30,9 +30,9 @@ use objc2_core_audio::{ kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyDeviceUID, kAudioDevicePropertyLatency, kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertySafetyOffset, kAudioDevicePropertyStreamConfiguration, - kAudioDevicePropertyStreamFormat, kAudioObjectPropertyClass, kAudioObjectPropertyElementMain, - kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, + kAudioDevicePropertyStreamFormat, kAudioDevicePropertyTransportType, kAudioObjectPropertyClass, + kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, }; use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, @@ -405,6 +405,64 @@ impl Device { status == 0 && class_id == kAudioAggregateDeviceClassID } + /// `None` when the property is unavailable or names a transport with no + /// `InterfaceType` counterpart, so the field is left unset rather than wrong. + fn transport_interface_type(&self) -> Option { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyTransportType, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut transport: u32 = 0; + let data_size = size_of::() as u32; + + // SAFETY: AudioObjectGetPropertyData writes a UInt32 for + // kAudioDevicePropertyTransportType. The status is checked before use. + let status = unsafe { + AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut transport).cast(), + ) + }; + if status != 0 { + return None; + } + + // Four-character codes from AudioHardwareBase.h. + const BUILT_IN: u32 = u32::from_be_bytes(*b"bltn"); + const USB: u32 = u32::from_be_bytes(*b"usb "); + const BLUETOOTH: u32 = u32::from_be_bytes(*b"blue"); + const BLUETOOTH_LE: u32 = u32::from_be_bytes(*b"blea"); + const VIRTUAL: u32 = u32::from_be_bytes(*b"virt"); + const AGGREGATE: u32 = u32::from_be_bytes(*b"grup"); + const THUNDERBOLT: u32 = u32::from_be_bytes(*b"thun"); + const HDMI: u32 = u32::from_be_bytes(*b"hdmi"); + const DISPLAY_PORT: u32 = u32::from_be_bytes(*b"dprt"); + const FIREWIRE: u32 = u32::from_be_bytes(*b"1394"); + const PCI: u32 = u32::from_be_bytes(*b"pci "); + const AIRPLAY: u32 = u32::from_be_bytes(*b"airp"); + + match transport { + BUILT_IN => Some(InterfaceType::BuiltIn), + USB => Some(InterfaceType::Usb), + BLUETOOTH | BLUETOOTH_LE => Some(InterfaceType::Bluetooth), + VIRTUAL => Some(InterfaceType::Virtual), + AGGREGATE => Some(InterfaceType::Aggregate), + THUNDERBOLT => Some(InterfaceType::Thunderbolt), + HDMI => Some(InterfaceType::Hdmi), + DISPLAY_PORT => Some(InterfaceType::DisplayPort), + FIREWIRE => Some(InterfaceType::FireWire), + PCI => Some(InterfaceType::Pci), + AIRPLAY => Some(InterfaceType::Network), + _ => None, + } + } + fn description(&self) -> Result { let name = get_device_name(self.audio_device_id).context("Failed to get device name")?; @@ -422,8 +480,11 @@ impl Device { let mut builder = DeviceDescriptionBuilder::new(name).direction(direction); - // Check if this is an aggregate device - if self.is_aggregate_device() { + // TransportType also reports "grup" for aggregates; the class check + // remains for devices that do not expose the property. + if let Some(interface_type) = self.transport_interface_type() { + builder = builder.interface_type(interface_type); + } else if self.is_aggregate_device() { builder = builder.interface_type(InterfaceType::Aggregate); }