// CoreAudio HAL device enumeration for the Settings pickers. Devices are persisted by // UID (stable across reboots/replugs — AudioDeviceIDs are not); the empty UID means // "system default", which additionally tracks default-device changes because we then // never pin the engine to a concrete device. #if os(macOS) import CoreAudio import Foundation public struct AudioDevice: Hashable, Identifiable, Sendable { public let uid: String public let name: String public var id: String { uid } } public enum AudioDevices { /// Output-capable devices (speakers, headphones, multi-output…). public static func outputs() -> [AudioDevice] { all().filter { hasStreams($0, scope: kAudioObjectPropertyScopeOutput) } .compactMap(describe) } /// Input-capable devices (microphones, interfaces…). public static func inputs() -> [AudioDevice] { all().filter { hasStreams($0, scope: kAudioObjectPropertyScopeInput) } .compactMap(describe) } /// Resolve a persisted UID to the current AudioDeviceID — nil when unplugged. static func deviceID(forUID uid: String) -> AudioDeviceID? { all().first { id in stringProperty(id, kAudioDevicePropertyDeviceUID) == uid } } /// Input channel count of the mic the picker would use — the device with this UID, or the /// system default input when `uid` is empty. 0 when it can't be resolved. Drives the /// "Microphone channel" picker (only shown for multi-channel interfaces). public static func inputChannelCount(forUID uid: String) -> Int { let id = uid.isEmpty ? defaultInputDevice() : deviceID(forUID: uid) guard let id else { return 0 } return channelCount(id, scope: kAudioObjectPropertyScopeInput) } private static func defaultInputDevice() -> AudioDeviceID? { var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) var dev = AudioDeviceID(0) var size = UInt32(MemoryLayout.size) guard AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &dev) == noErr, dev != 0 else { return nil } return dev } /// Sum of channels across the device's streams in `scope` (its total input/output channels). private static func channelCount( _ id: AudioDeviceID, scope: AudioObjectPropertyScope ) -> Int { var address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyStreamConfiguration, mScope: scope, mElement: kAudioObjectPropertyElementMain) var size: UInt32 = 0 guard AudioObjectGetPropertyDataSize(id, &address, 0, nil, &size) == noErr, size > 0 else { return 0 } let raw = UnsafeMutableRawPointer.allocate( byteCount: Int(size), alignment: MemoryLayout.alignment) defer { raw.deallocate() } guard AudioObjectGetPropertyData(id, &address, 0, nil, &size, raw) == noErr else { return 0 } let abl = UnsafeMutableAudioBufferListPointer( raw.assumingMemoryBound(to: AudioBufferList.self)) return abl.reduce(0) { $0 + Int($1.mNumberChannels) } } private static func all() -> [AudioDeviceID] { var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) var size: UInt32 = 0 guard AudioObjectGetPropertyDataSize( AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size) == noErr, size > 0 else { return [] } var ids = [AudioDeviceID]( repeating: 0, count: Int(size) / MemoryLayout.size) guard AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &ids) == noErr else { return [] } return ids } private static func hasStreams( _ id: AudioDeviceID, scope: AudioObjectPropertyScope ) -> Bool { var address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyStreams, mScope: scope, mElement: kAudioObjectPropertyElementMain) var size: UInt32 = 0 return AudioObjectGetPropertyDataSize(id, &address, 0, nil, &size) == noErr && size > 0 } /// UID + human name for a live AudioDeviceID (nil if either property is unreadable). static func describe(_ id: AudioDeviceID) -> AudioDevice? { guard let uid = stringProperty(id, kAudioDevicePropertyDeviceUID), let name = stringProperty(id, kAudioObjectPropertyName) else { return nil } return AudioDevice(uid: uid, name: name) } private static func stringProperty( _ id: AudioDeviceID, _ selector: AudioObjectPropertySelector ) -> String? { var address = AudioObjectPropertyAddress( mSelector: selector, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) var ref: CFString? var size = UInt32(MemoryLayout.size) let status = withUnsafeMutablePointer(to: &ref) { p in AudioObjectGetPropertyData(id, &address, 0, nil, &size, p) } guard status == noErr, let ref else { return nil } return ref as String } } #endif