You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
95 lines
4.7 KiB
95 lines
4.7 KiB
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<AudioDeviceID>.size)
|
|
guard AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &ids) == noErr else { return [] }
|
|
return ids.compactMap(device(for:))
|
|
}
|
|
|
|
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<Float64>.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<CFString>?
|
|
var size = UInt32(MemoryLayout<Unmanaged<CFString>?>.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<AudioBufferList>.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)
|
|
}
|
|
}
|