128 lines
5.7 KiB
Swift
128 lines
5.7 KiB
Swift
#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<Float64>.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<OutputDeviceInfo?> {
|
|
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<T>(_ selector: AudioObjectPropertySelector, from object: AudioObjectID, as _: T.Type) -> T? {
|
|
var address = address(selector)
|
|
var size = UInt32(MemoryLayout<T>.size)
|
|
let buffer = UnsafeMutablePointer<T>.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<AudioValueRange>.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
|