1import Foundation
2import CoreAudio
3
4struct AudioInputDevice: Identifiable, Hashable {
5 let id: AudioDeviceID
6 let name: String
7}
8
9/// Thin wrapper over the CoreAudio HAL for enumerating and identifying
10/// hardware input devices.
11enum CoreAudioDevices {
12
13 static func inputDevices() -> [AudioInputDevice] {
14 var address = AudioObjectPropertyAddress(
15 mSelector: kAudioHardwarePropertyDevices,
16 mScope: kAudioObjectPropertyScopeGlobal,
17 mElement: kAudioObjectPropertyElementMain)
18
19 var dataSize: UInt32 = 0
20 guard AudioObjectGetPropertyDataSize(
21 AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &dataSize) == noErr
22 else { return [] }
23
24 let count = Int(dataSize) / MemoryLayout<AudioDeviceID>.size
25 var ids = [AudioDeviceID](repeating: 0, count: count)
26 guard AudioObjectGetPropertyData(
27 AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &dataSize, &ids) == noErr
28 else { return [] }
29
30 return ids.compactMap { id in
31 guard hasInputChannels(id) else { return nil }
32 return AudioInputDevice(id: id, name: name(of: id) ?? "Device \(id)")
33 }
34 }
35
36 static func defaultInputDevice() -> AudioDeviceID? {
37 var address = AudioObjectPropertyAddress(
38 mSelector: kAudioHardwarePropertyDefaultInputDevice,
39 mScope: kAudioObjectPropertyScopeGlobal,
40 mElement: kAudioObjectPropertyElementMain)
41 var deviceID: AudioDeviceID = 0
42 var size = UInt32(MemoryLayout<AudioDeviceID>.size)
43 guard AudioObjectGetPropertyData(
44 AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &deviceID) == noErr,
45 deviceID != 0
46 else { return nil }
47 return deviceID
48 }
49
50 // MARK: - Helpers
51
52 private static func hasInputChannels(_ id: AudioDeviceID) -> Bool {
53 var address = AudioObjectPropertyAddress(
54 mSelector: kAudioDevicePropertyStreamConfiguration,
55 mScope: kAudioObjectPropertyScopeInput,
56 mElement: kAudioObjectPropertyElementMain)
57
58 var size: UInt32 = 0
59 guard AudioObjectGetPropertyDataSize(id, &address, 0, nil, &size) == noErr, size > 0
60 else { return false }
61
62 let bufferList = UnsafeMutableRawPointer.allocate(
63 byteCount: Int(size), alignment: MemoryLayout<AudioBufferList>.alignment)
64 defer { bufferList.deallocate() }
65
66 guard AudioObjectGetPropertyData(id, &address, 0, nil, &size, bufferList) == noErr
67 else { return false }
68
69 let list = UnsafeMutableAudioBufferListPointer(
70 bufferList.assumingMemoryBound(to: AudioBufferList.self))
71 var channels = 0
72 for buffer in list { channels += Int(buffer.mNumberChannels) }
73 return channels > 0
74 }
75
76 private static func name(of id: AudioDeviceID) -> String? {
77 var address = AudioObjectPropertyAddress(
78 mSelector: kAudioObjectPropertyName,
79 mScope: kAudioObjectPropertyScopeGlobal,
80 mElement: kAudioObjectPropertyElementMain)
81 var name: Unmanaged<CFString>?
82 var size = UInt32(MemoryLayout<Unmanaged<CFString>?>.size)
83 guard AudioObjectGetPropertyData(id, &address, 0, nil, &size, &name) == noErr,
84 let value = name?.takeRetainedValue()
85 else { return nil }
86 return value as String
87 }
88}