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

105 lines
5.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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:))
}
/// 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<AudioDeviceID>.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<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.