#if os(macOS) import CoreAudio import Foundation public enum OutputDeviceError: Error { case noDevice, unsupportedRate, writeFailed, timedOut } public enum OutputDevice { private static let pollStep: Duration = .milliseconds(20) private static let pollLimit = 50 private static let settle: Duration = .milliseconds(100) public static func current() -> OutputDeviceInfo? { guard let id = defaultDevice() else { return nil } return OutputDeviceInfo( id: id, name: read(kAudioObjectPropertyName, from: id, as: CFString.self).map { $0 as String } ?? "", nominalRate: read(kAudioDevicePropertyNominalSampleRate, from: id, as: Float64.self) ?? 0, availableRates: availableRates(of: id) ) } public static func setNominalRate(_ rate: Double) async throws { guard let device = current() else { throw OutputDeviceError.noDevice } guard device.nominalRate != rate else { return } guard device.availableRates.contains(rate) else { throw OutputDeviceError.unsupportedRate } var address = address(kAudioDevicePropertyNominalSampleRate) var value = rate let size = UInt32(MemoryLayout.size) guard AudioObjectSetPropertyData(device.id, &address, 0, nil, size, &value) == noErr else { throw OutputDeviceError.writeFailed } for _ in 0 ..< pollLimit { if read(kAudioDevicePropertyNominalSampleRate, from: device.id, as: Float64.self) == rate { try await Task.sleep(for: settle) return } try await Task.sleep(for: pollStep) } throw OutputDeviceError.timedOut } public static func changes() -> AsyncStream { AsyncStream { continuation in continuation.yield(current()) let listener = Listener { continuation.yield(current()) } continuation.onTermination = { _ in listener.stop() } } } fileprivate static func defaultDevice() -> AudioDeviceID? { let system = AudioObjectID(kAudioObjectSystemObject) let id = read(kAudioHardwarePropertyDefaultOutputDevice, from: system, as: AudioDeviceID.self) return id.flatMap { $0 == kAudioObjectUnknown ? nil : $0 } } fileprivate static func address(_ selector: AudioObjectPropertySelector) -> AudioObjectPropertyAddress { AudioObjectPropertyAddress( mSelector: selector, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) } private static func read(_ selector: AudioObjectPropertySelector, from object: AudioObjectID, as _: T.Type) -> T? { var address = address(selector) var size = UInt32(MemoryLayout.size) let buffer = UnsafeMutablePointer.allocate(capacity: 1) defer { buffer.deallocate() } guard AudioObjectGetPropertyData(object, &address, 0, nil, &size, buffer) == noErr else { return nil } return buffer.move() } private static func availableRates(of device: AudioDeviceID) -> [Double] { var address = address(kAudioDevicePropertyAvailableNominalSampleRates) var size: UInt32 = 0 guard AudioObjectGetPropertyDataSize(device, &address, 0, nil, &size) == noErr else { return [] } let count = Int(size) / MemoryLayout.stride var ranges = [AudioValueRange](repeating: AudioValueRange(), count: count) guard AudioObjectGetPropertyData(device, &address, 0, nil, &size, &ranges) == noErr else { return [] } return Array(Set(ranges.flatMap { [$0.mMinimum, $0.mMaximum] })).sorted() } } private final class Listener: @unchecked Sendable { private let queue = DispatchQueue(label: "Amphlify.OutputDevice") private let system = AudioObjectID(kAudioObjectSystemObject) private var device: AudioDeviceID = kAudioObjectUnknown private var systemAddress = OutputDevice.address(kAudioHardwarePropertyDefaultOutputDevice) private var rateAddress = OutputDevice.address(kAudioDevicePropertyNominalSampleRate) private var onSystemChange: AudioObjectPropertyListenerBlock = { _, _ in } private var onRateChange: AudioObjectPropertyListenerBlock = { _, _ in } init(onChange: @escaping @Sendable () -> Void) { onRateChange = { _, _ in onChange() } onSystemChange = { [weak self] _, _ in self?.follow() onChange() } queue.sync { AudioObjectAddPropertyListenerBlock(system, &systemAddress, queue, onSystemChange) follow() } } func stop() { queue.sync { AudioObjectRemovePropertyListenerBlock(system, &systemAddress, queue, onSystemChange) unfollow() } } private func follow() { unfollow() device = OutputDevice.defaultDevice() ?? kAudioObjectUnknown guard device != kAudioObjectUnknown else { return } AudioObjectAddPropertyListenerBlock(device, &rateAddress, queue, onRateChange) } private func unfollow() { guard device != kAudioObjectUnknown else { return } AudioObjectRemovePropertyListenerBlock(device, &rateAddress, queue, onRateChange) device = kAudioObjectUnknown } } #endif