Files
meetingnotes/meetingnotes/ProcessTap/CoreAudioUtils.swift
T

352 lines
14 KiB
Swift

import Foundation
import AudioToolbox
// MARK: - Constants
extension AudioObjectID {
/// Convenience for `kAudioObjectSystemObject`.
static let system = AudioObjectID(kAudioObjectSystemObject)
/// Convenience for `kAudioObjectUnknown`.
static let unknown = kAudioObjectUnknown
/// `true` if this object has the value of `kAudioObjectUnknown`.
var isUnknown: Bool { self == .unknown }
/// `false` if this object has the value of `kAudioObjectUnknown`.
var isValid: Bool { !isUnknown }
}
// MARK: - Concrete Property Helpers
extension AudioObjectID {
/// Reads the value for `kAudioHardwarePropertyDefaultSystemOutputDevice`.
static func readDefaultSystemOutputDevice() throws -> AudioDeviceID {
try AudioObjectID.system.readDefaultSystemOutputDevice()
}
static func readProcessList() throws -> [AudioObjectID] {
try AudioObjectID.system.readProcessList()
}
/// Reads `kAudioHardwarePropertyTranslatePIDToProcessObject` for the specific pid.
static func translatePIDToProcessObjectID(pid: pid_t) throws -> AudioObjectID {
try AudioObjectID.system.translatePIDToProcessObjectID(pid: pid)
}
/// Reads `kAudioHardwarePropertyProcessObjectList`.
func readProcessList() throws -> [AudioObjectID] {
try requireSystemObject()
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyProcessObjectList,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var dataSize: UInt32 = 0
var err = AudioObjectGetPropertyDataSize(self, &address, 0, nil, &dataSize)
guard err == noErr else { throw "Error reading data size for \(address): \(err)" }
var value = [AudioObjectID](repeating: .unknown, count: Int(dataSize) / MemoryLayout<AudioObjectID>.size)
err = AudioObjectGetPropertyData(self, &address, 0, nil, &dataSize, &value)
guard err == noErr else { throw "Error reading array for \(address): \(err)" }
return value
}
/// Reads `kAudioHardwarePropertyTranslatePIDToProcessObject` for the specific pid, should only be called on the system object.
func translatePIDToProcessObjectID(pid: pid_t) throws -> AudioObjectID {
try requireSystemObject()
let processObject = try read(
kAudioHardwarePropertyTranslatePIDToProcessObject,
defaultValue: AudioObjectID.unknown,
qualifier: pid
)
guard processObject.isValid else {
throw "Invalid process identifier: \(pid)"
}
return processObject
}
func readProcessBundleID() -> String? {
if let result = try? readString(kAudioProcessPropertyBundleID) {
result.isEmpty ? nil : result
} else {
nil
}
}
func readProcessIsRunning() -> Bool {
(try? readBool(kAudioProcessPropertyIsRunning)) ?? false
}
/*
public var kAudioProcessPropertyPID: AudioObjectPropertySelector { get }
public var kAudioProcessPropertyBundleID: AudioObjectPropertySelector { get }
public var kAudioProcessPropertyDevices: AudioObjectPropertySelector { get }
public var kAudioProcessPropertyIsRunning: AudioObjectPropertySelector { get }
public var kAudioProcessPropertyIsRunningInput: AudioObjectPropertySelector { get }
public var kAudioProcessPropertyIsRunningOutput: AudioObjectPropertySelector { get }
*/
/// Reads the value for `kAudioHardwarePropertyDefaultSystemOutputDevice`, should only be called on the system object.
func readDefaultSystemOutputDevice() throws -> AudioDeviceID {
try requireSystemObject()
return try read(kAudioHardwarePropertyDefaultSystemOutputDevice, defaultValue: AudioDeviceID.unknown)
}
/// Reads the value for `kAudioDevicePropertyDeviceUID` for the device represented by this audio object ID.
func readDeviceUID() throws -> String { try readString(kAudioDevicePropertyDeviceUID) }
/// Reads the value for `kAudioTapPropertyFormat` for the device represented by this audio object ID.
func readAudioTapStreamBasicDescription() throws -> AudioStreamBasicDescription {
try read(kAudioTapPropertyFormat, defaultValue: AudioStreamBasicDescription())
}
/// Reads the virtual format of the first input stream exposed by this device.
///
/// An aggregate device can adapt a tap to the active output hardware. Its
/// input stream format is therefore the format delivered to the I/O proc,
/// which can differ from the tap object's originally advertised format.
func readInputStreamBasicDescription() throws -> AudioStreamBasicDescription {
var streamsAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreams,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var dataSize: UInt32 = 0
var status = AudioObjectGetPropertyDataSize(self, &streamsAddress, 0, nil, &dataSize)
guard status == noErr else {
throw "Error reading device stream list size: \(status)"
}
var streamIDs = [AudioObjectID](
repeating: .unknown,
count: Int(dataSize) / MemoryLayout<AudioObjectID>.size
)
status = AudioObjectGetPropertyData(self, &streamsAddress, 0, nil, &dataSize, &streamIDs)
guard status == noErr else {
throw "Error reading device stream list: \(status)"
}
for streamID in streamIDs {
let direction: UInt32 = try streamID.read(
kAudioStreamPropertyDirection,
defaultValue: 0
)
guard direction == 1 else { continue }
return try streamID.read(
kAudioStreamPropertyVirtualFormat,
defaultValue: AudioStreamBasicDescription()
)
}
throw "Device has no input stream."
}
private func requireSystemObject() throws {
if self != .system { throw "Only supported for the system object." }
}
}
// MARK: - Generic Property Access
extension AudioObjectID {
func read<T, Q>(_ selector: AudioObjectPropertySelector,
scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal,
element: AudioObjectPropertyElement = kAudioObjectPropertyElementMain,
defaultValue: T,
qualifier: Q) throws -> T
{
try read(AudioObjectPropertyAddress(mSelector: selector, mScope: scope, mElement: element), defaultValue: defaultValue, qualifier: qualifier)
}
func read<T>(_ selector: AudioObjectPropertySelector,
scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal,
element: AudioObjectPropertyElement = kAudioObjectPropertyElementMain,
defaultValue: T) throws -> T
{
try read(AudioObjectPropertyAddress(mSelector: selector, mScope: scope, mElement: element), defaultValue: defaultValue)
}
func read<T, Q>(_ address: AudioObjectPropertyAddress, defaultValue: T, qualifier: Q) throws -> T {
var inQualifier = qualifier
let qualifierSize = UInt32(MemoryLayout<Q>.size(ofValue: qualifier))
return try withUnsafeMutablePointer(to: &inQualifier) { qualifierPtr in
try read(address, defaultValue: defaultValue, inQualifierSize: qualifierSize, inQualifierData: qualifierPtr)
}
}
func read<T>(_ address: AudioObjectPropertyAddress, defaultValue: T) throws -> T {
try read(address, defaultValue: defaultValue, inQualifierSize: 0, inQualifierData: nil)
}
func readString(_ selector: AudioObjectPropertySelector, scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal, element: AudioObjectPropertyElement = kAudioObjectPropertyElementMain) throws -> String {
try read(AudioObjectPropertyAddress(mSelector: selector, mScope: scope, mElement: element), defaultValue: "" as CFString) as String
}
func readBool(_ selector: AudioObjectPropertySelector, scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal, element: AudioObjectPropertyElement = kAudioObjectPropertyElementMain) throws -> Bool {
let value: Int = try read(AudioObjectPropertyAddress(mSelector: selector, mScope: scope, mElement: element), defaultValue: 0)
return value == 1
}
private func read<T>(_ inAddress: AudioObjectPropertyAddress, defaultValue: T, inQualifierSize: UInt32 = 0, inQualifierData: UnsafeRawPointer? = nil) throws -> T {
var address = inAddress
var dataSize: UInt32 = 0
var err = AudioObjectGetPropertyDataSize(self, &address, inQualifierSize, inQualifierData, &dataSize)
guard err == noErr else {
throw "Error reading data size for \(inAddress): \(err)"
}
var value: T = defaultValue
err = withUnsafeMutablePointer(to: &value) { ptr in
AudioObjectGetPropertyData(self, &address, inQualifierSize, inQualifierData, &dataSize, ptr)
}
guard err == noErr else {
throw "Error reading data for \(inAddress): \(err)"
}
return value
}
}
// MARK: - Debugging Helpers
private extension UInt32 {
var fourCharString: String {
String(cString: [
UInt8((self >> 24) & 0xFF),
UInt8((self >> 16) & 0xFF),
UInt8((self >> 8) & 0xFF),
UInt8(self & 0xFF),
0
])
}
}
extension AudioObjectPropertyAddress: @retroactive CustomStringConvertible {
public var description: String {
let elementDescription = mElement == kAudioObjectPropertyElementMain ? "main" : mElement.fourCharString
return "\(mSelector.fourCharString)/\(mScope.fourCharString)/\(elementDescription)"
}
}
public struct AudioInputDevice: Identifiable, Hashable {
public let id: AudioDeviceID // AudioDeviceID (UInt32) is Hashable and can serve as Identifiable's id.
public let uid: String // Unique identifier for the device (persistent across reboots)
public let name: String
public init(id: AudioDeviceID, uid: String, name: String) {
self.id = id
self.uid = uid
self.name = name
}
}
extension AudioObjectID {
static func getAllInputDevices() throws -> [AudioInputDevice] {
let allDeviceIDs = try AudioObjectID.system.getAllHardwareDevices()
var inputDevices: [AudioInputDevice] = []
for deviceID in allDeviceIDs {
do {
let inputChannelCount = try deviceID.getTotalInputChannelCount()
if inputChannelCount > 0 {
let deviceName = try deviceID.getDeviceName()
let deviceUID = try deviceID.readDeviceUID()
inputDevices.append(AudioInputDevice(id: deviceID, uid: deviceUID, name: deviceName))
}
} catch {
print("CoreAudioUtils: Could not fully query device \(deviceID): \(error)")
}
}
return inputDevices
}
func getAllHardwareDevices() throws -> [AudioDeviceID] {
try requireSystemObject()
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var dataSize: UInt32 = 0
var err = AudioObjectGetPropertyDataSize(self, &address, 0, nil, &dataSize)
guard err == noErr else {
throw "CoreAudioUtils: Error reading data size for \(kAudioHardwarePropertyDevices.fourCharString): \(err)"
}
let deviceCount = Int(dataSize) / MemoryLayout<AudioDeviceID>.size
if deviceCount == 0 {
return []
}
var deviceIDs = [AudioDeviceID](repeating: .unknown, count: deviceCount)
err = AudioObjectGetPropertyData(self, &address, 0, nil, &dataSize, &deviceIDs)
guard err == noErr else {
throw "CoreAudioUtils: Error reading device array for \(kAudioHardwarePropertyDevices.fourCharString): \(err)"
}
return deviceIDs
}
func getTotalInputChannelCount() throws -> UInt32 {
var address = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreamConfiguration,
mScope: kAudioDevicePropertyScopeInput,
mElement: kAudioObjectPropertyElementMain
)
var dataSize: UInt32 = 0
var err = AudioObjectGetPropertyDataSize(self, &address, 0, nil, &dataSize)
if err == kAudioHardwareUnknownPropertyError || dataSize == 0 {
return 0
}
guard err == noErr else {
throw "CoreAudioUtils: Error reading data size for input stream configuration on device \(self): \(err)"
}
let bufferListPtr = UnsafeMutableRawPointer.allocate(byteCount: Int(dataSize), alignment: MemoryLayout<AudioBufferList>.alignment)
defer { bufferListPtr.deallocate() }
err = AudioObjectGetPropertyData(self, &address, 0, nil, &dataSize, bufferListPtr)
guard err == noErr else {
throw "CoreAudioUtils: Error reading input stream configuration for device \(self): \(err)"
}
let audioBufferList = bufferListPtr.assumingMemoryBound(to: AudioBufferList.self)
var totalInputChannels: UInt32 = 0
let buffers = UnsafeMutableAudioBufferListPointer(audioBufferList)
for i in 0..<Int(buffers.count) {
totalInputChannels += buffers[i].mNumberChannels
}
return totalInputChannels
}
func getDeviceName() throws -> String {
return try readString(kAudioDevicePropertyDeviceNameCFString)
}
}