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.
Musubi/Packages/MusubiKit/Sources/AudioMatrix/AudioDevices.swift

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)
}
}

Powered by TurnKey Linux.