import CoreAudio import Foundation /// A Core Audio device as shown in the UI. public struct AudioDevice: Identifiable, Hashable, Sendable { public var id: String { uid } public let deviceID: AudioDeviceID public let uid: String public let name: String public let inputChannels: Int public let outputChannels: Int public let sampleRate: Double } public enum AudioDevices { public static func all() -> [AudioDevice] { var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) var size: UInt32 = 0 guard AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size) == noErr 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.compactMap(device(for:)) } /// The input device selected in System Settings › Sound. public static func defaultInput() -> AudioDevice? { var address = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) var id = AudioDeviceID(0) var size = UInt32(MemoryLayout.size) guard AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &id) == noErr else { return nil } return device(for: id) } public static func device(uid: String) -> AudioDevice? { all().first { $0.uid == uid } } static func device(for id: AudioDeviceID) -> AudioDevice? { guard let uid = string(id, kAudioDevicePropertyDeviceUID), let name = string(id, kAudioObjectPropertyName) else { return nil } return AudioDevice( deviceID: id, uid: uid, name: name, inputChannels: channelCount(id, scope: kAudioObjectPropertyScopeInput), outputChannels: channelCount(id, scope: kAudioObjectPropertyScopeOutput), sampleRate: nominalSampleRate(id) ) } static func nominalSampleRate(_ id: AudioDeviceID) -> Double { var address = AudioObjectPropertyAddress(mSelector: kAudioDevicePropertyNominalSampleRate, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) var rate: Float64 = 0 var size = UInt32(MemoryLayout.size) return AudioObjectGetPropertyData(id, &address, 0, nil, &size, &rate) == noErr ? rate : 0 } /// Calls `handler` on the main queue whenever devices are added or removed. /// Keep the returned token alive as long as you want notifications. public static func observeChanges(_ handler: @escaping @MainActor @Sendable () -> Void) -> AnyObject { DeviceListObserver(handler: handler) } private static func string(_ id: AudioDeviceID, _ selector: AudioObjectPropertySelector) -> String? { var address = AudioObjectPropertyAddress(mSelector: selector, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) var value: Unmanaged? var size = UInt32(MemoryLayout?>.size) guard AudioObjectGetPropertyData(id, &address, 0, nil, &size, &value) == noErr, let value else { return nil } return value.takeRetainedValue() as String } 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 list = UnsafeMutableAudioBufferListPointer(raw.assumingMemoryBound(to: AudioBufferList.self)) return list.reduce(0) { $0 + Int($1.mNumberChannels) } } } private final class DeviceListObserver { private var address = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) private let block: AudioObjectPropertyListenerBlock init(handler: @escaping @MainActor @Sendable () -> Void) { block = { _, _ in MainActor.assumeIsolated { handler() } } AudioObjectAddPropertyListenerBlock(AudioObjectID(kAudioObjectSystemObject), &address, .main, block) } deinit { AudioObjectRemovePropertyListenerBlock(AudioObjectID(kAudioObjectSystemObject), &address, .main, block) } }