Created
November 5, 2022 13:11
-
-
Save SteveTrewick/c0668ee438eb784cbc5fb4674f0c2cd1 to your computer and use it in GitHub Desktop.
Enumerate audio input and output devices using Swift on macOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import CoreAudio | |
import OSLog | |
/* | |
So you want to get a list of audio input and output devices on macOS do you? | |
Should be easy right? | |
HAHAHAHAHAHAHAHAHAHAHAHAAAA no. | |
If you just want input you can use an AVCaptureDevice.DiscoverySession | |
But if you want output devices, you're gonna have to dip into Core Audio | |
convince me I'm wrong, dear god, please convince me that is utterly unnecessary | |
sigh, so here we go then | |
*/ | |
public struct AudioDeviceEnumerator { | |
/* | |
seriously, that's all I wanted, just a list of devices with in/out | |
and an identifier for them, just like this. This is where I started | |
let's skate over for the moment that there are two seperate identifiers, | |
depending on which set of APIs of the month we want to use them with | |
that's really the least of it. | |
*/ | |
public struct Device { | |
public let name : String | |
public let manufacturer : String | |
public let uidString : String | |
public let deviceID : AudioDeviceID | |
public let output : UInt32 // number of channels | |
public let input : UInt32 | |
} | |
/* | |
Here we start to see the head of the apalling eldritch horror we're going to encounter | |
to complete this simple task. | |
Since we're dipping down to aincent C APIs, we have a load of kArgleBlargeGuffle consts | |
and I am NOT typing that shit every time I want to make an API call, fuck that noise, | |
so here I'm wrapping them up for autocompletion and call site hygeine purposes | |
each API call requires an ID and an address, actually a AudioObjectPropertyAddress | |
which we'll construct later, which conists of a selector, a scope and an element. | |
*/ | |
private enum Selector { | |
/* | |
what do we want? (and why do all consts start with k, konstant, AHAHAHAHAHAHAHHA, I GET IT) | |
*/ | |
case devices, name, manufacturer, uniqID, streamConfig | |
var value : AudioObjectPropertySelector { | |
switch self { | |
case .devices : return kAudioHardwarePropertyDevices | |
case .name : return kAudioDevicePropertyDeviceNameCFString | |
case .manufacturer : return kAudioDevicePropertyDeviceManufacturerCFString | |
case .uniqID : return kAudioDevicePropertyDeviceUID | |
case .streamConfig : return kAudioDevicePropertyStreamConfiguration | |
} | |
} | |
} | |
private enum Scope { | |
/* | |
where is it? | |
*/ | |
case global, input, output | |
var value : AudioObjectPropertyScope { | |
switch self { | |
case .global : return kAudioObjectPropertyScopeGlobal | |
case .input : return kAudioDevicePropertyScopeInput | |
case .output : return kAudioDevicePropertyScopeOutput | |
} | |
} | |
} | |
/* | |
a container for all that nonsense. look how much code there is already and we haven't | |
even called an API yet! | |
*/ | |
private struct Address { | |
let selector : Selector | |
let scope : Scope | |
let element : AudioObjectPropertyElement | |
/* | |
NB that the AudioObjectPropertyElement doesn't ever seem to need to be set to | |
anything other than 0, but since there's no actual documentation, who knows? | |
seriously, go look at : https://developer.apple.com/documentation/coreaudio/audioobjectpropertyelement | |
Not that the others are any better. Oh and don't be fooled by the types, they're all | |
just aliases to Ints, but which ints? Do we trust that not to change? we do not! | |
*/ | |
init(selector: Selector, scope: Scope, element: AudioObjectPropertyElement = 0) { | |
self.selector = selector | |
self.scope = scope | |
self.element = element | |
} | |
/* | |
construct the kageleblagles | |
*/ | |
var apivalue : AudioObjectPropertyAddress { | |
AudioObjectPropertyAddress ( | |
mSelector: selector.value, | |
mScope : scope.value, | |
mElement : element | |
) | |
} | |
} | |
/* | |
I'm just going to throw debug log messages for any bad results, because | |
they'll be param errors and also utterly inscrutable to any poor end user | |
and indeed, largely to me. Technically, that means that *this code* is in error | |
and some people would say you should therefore use assert() agressivley, but that's | |
actually really unfriendly if someone else wants to use your code, | |
isn't it Julian, you prick? | |
*/ | |
private let log = Logger(subsystem: "AudioDeviceEnumerator", category: "error") | |
/* | |
Right here we go. We're to use AudioObjectGetPropertyData which is a C API | |
of the popular form : | |
error = do_some_things([shitload of params]) | |
where one of the shitload of params is an untyped pointer, and some of them are | |
optional in some cases but which ones aren't documented | |
sigh | |
It follows then that are essentially two broad classes of thigs that these | |
untyped pointers can be pointing to, scalar values and arrays (pointers to | |
arrays, actually, as it happens, but lets' pretend thats not a thing in 2022, | |
which works right up until it doesn't) | |
since there are only two foundational calls then, let's wrap them and keep all the | |
kblargles and all the nasty pointer shit contained in there as much as possible. | |
*/ | |
private func propertyScalar<T>(id: AudioObjectID, address: Address, type: T.Type) -> T? { | |
/* | |
OK, so a scalar value, one of something, but what? Who the fuck knows? | |
Maybe if you search the headers and old mailing list posts you can find out? | |
Once you do, figure out how that maps into swift and do this. | |
Allocate some storage for your thing, figure out how big that storage is and | |
filing them at the API. And yes, you need the size or this fails | |
The return value is optional because there is not a reliable generic | |
way to provide a default value for all the things we might ask for, and because | |
if we make an oopsy, our property won't get filled anyway | |
*/ | |
var property : T? = nil | |
var size = UInt32(MemoryLayout<T>.size) | |
var apivalue = address.apivalue | |
let osresult = AudioObjectGetPropertyData(id, &apivalue, 0, nil, &size, &property) | |
if osresult != 0 { log.error("AudioObjectGetPropertyData error: \(osresult)") } | |
return property | |
} | |
/* | |
Arrays of things (actually pointers to things), we'll need to know how big these are, | |
so we have to ask first. Yes, I know, you'd think so. But no *you* have to it. | |
NB that this returns the number of bytes required for *all the things* not number of things | |
*/ | |
private func propertyDataSize(id: AudioObjectID, address: Address) -> UInt32 { | |
var bytes : UInt32 = 0 | |
var apivalue = address.apivalue | |
let osresult = AudioObjectGetPropertyDataSize(id, &apivalue, 0, nil, &bytes) | |
if osresult != 0 { log.error("AudioObjectGetPropertyDataSize error: \(osresult)") } | |
return bytes | |
} | |
/* | |
Now we know how big our list of things is, we can allocate some storage and retrieve them. | |
I say 'them', frequently in fact just 'it' but you have tyo get it like this because reasons. | |
See how Swift reminds us that this is unsafe by naming all it's methods for doing this kind | |
of shit Unsafe? Yeah. | |
*/ | |
private func propertyArray<T>(id: AudioObjectID, address: Address, bytes: UInt32, type: T.Type) -> [T] { | |
var size = bytes | |
var apivalue = address.apivalue | |
/* | |
remember, we only know how many bytes we have to compute from | |
that the number of actual things | |
*/ | |
let count = Int(bytes) / MemoryLayout<T>.size | |
let buffptr = UnsafeMutablePointer<T>.allocate(capacity: count) | |
let osresult = AudioObjectGetPropertyData(id, &apivalue, 0, nil, &size, buffptr) | |
if osresult != 0 { log.error("AudioObjectGetPropertyData error: \(osresult)") } | |
/* | |
For an array of things [T], there is in fact a sensible default value, [], | |
the empty list, so if we get 0 here we just return an empty list, | |
if this isn't matching your expectations, check your logs for inscruatble | |
error codes. | |
*/ | |
return Array(UnsafeBufferPointer(start: buffptr, count: count)) | |
} | |
/* | |
Many of the things we want are atrings, well, actually they are CFStrings, | |
but anyhoo, they are in the global scope and a sensible default for a atring | |
is the empty string and I'm not typing this however many times it turns out to be | |
as this no doubt expands into some monsterous CA wrapper. | |
*/ | |
private func globalDeviceString(id: AudioDeviceID, selector: Selector) -> String { | |
propertyScalar ( | |
id : id, | |
address: .init(selector: selector, scope: .global), | |
type : CFString.self | |
) | |
as String? ?? "" | |
} | |
/* | |
I want to know if a device is capable of input or output, for this, we have to retrieve | |
an AudioBufferList struct. Yes, just one, yes it has to be called this way, no, I don't | |
know why. None of the devices on my machine have more than one, nor do the various USB | |
things that plug into it, so I dunno, YMMV here. | |
We pull a bufferlist for the relevant scope and (sort of) count channels, | |
I actually was doing this with just a bool, but the info is there, so why throw it away? | |
Again technically, mBuffers is a pointer, the first of a possible array, | |
I haven't encountered a device that has more than one of these, even | |
the 16 channel Existential Audio virtual device I'm using has only the one | |
If this turns out to be an issue, turn mBuffers into an array of AudioBuffer | |
and go nuts. | |
*/ | |
private func deviceIOBuffer(id: AudioDeviceID, scope: Scope) -> UInt32 { | |
let address = Address(selector: .streamConfig, scope: scope) | |
let size = propertyDataSize(id: id, address: address) | |
let buffpointer = propertyArray(id: id, address: address, bytes: size, type: AudioBufferList.self) | |
for bufferlist in buffpointer { | |
return bufferlist.mBuffers.mNumberChannels | |
} | |
return 0 | |
} | |
/* | |
Oh yeah, to get info on a device, we must first get a list of the devices | |
Core Audio IDs | |
*/ | |
private func queryAudioDeviceIDs() -> [AudioDeviceID] { | |
let address = Address(selector: .devices, scope: .global) | |
let id = AudioObjectID(kAudioObjectSystemObject) | |
let size = propertyDataSize(id: id, address: address) | |
return propertyArray(id: id, address: address, bytes: size, type: AudioDeviceID.self) | |
} | |
/* | |
And now *FINALLY* we can actually get a list of devices, capabilities, names, manufacturers | |
and the two IDs tha we need for the various APIs | |
Sheesh | |
*/ | |
public func listDevices() -> [Device] { | |
var devices : [Device] = [] | |
let result = queryAudioDeviceIDs() | |
for deviceID in result { | |
let name = globalDeviceString(id: deviceID, selector: .name) | |
let manf = globalDeviceString(id: deviceID, selector: .manufacturer) | |
let uniq = globalDeviceString(id: deviceID, selector: .uniqID) | |
let input = deviceIOBuffer(id: deviceID, scope: .input) | |
let output = deviceIOBuffer(id: deviceID, scope: .output) | |
devices += [ | |
Device ( | |
name : name, | |
manufacturer: manf, | |
uidString : uniq, | |
deviceID : deviceID, | |
output : output, | |
input : input | |
) | |
] | |
} | |
return devices | |
} | |
} | |
let ade = AudioDeviceEnumerator() | |
let devices = ade.listDevices() | |
devices.forEach { print($0) } | |
/* | |
Device(name: "Built-in Microphone", manufacturer: "Apple Inc.", uidString: "AppleHDAEngineInput:1B,0,1,0:1", deviceID: 58, output: 0, input: 2) | |
Device(name: "Built-in Output", manufacturer: "Apple Inc.", uidString: "AppleHDAEngineOutput:1B,0,1,1:0", deviceID: 65, output: 2, input: 0) | |
Device(name: "HDMI", manufacturer: "Apple Inc.", uidString: "AppleHDAEngineOutputDP:3,0,1,0:0:{7204-058E-8460829F}", deviceID: 75, output: 2, input: 0) | |
Device(name: "USB Audio Device", manufacturer: "C-Media Electronics Inc.", uidString: "AppleUSBAudioEngine:C-Media Electronics Inc.:USB Audio Device:14200000:2,1", deviceID: 79, output: 2, input: 1) | |
Device(name: "BlackHole 16ch", manufacturer: "Existential Audio Inc.", uidString: "BlackHole_UID", deviceID: 49, output: 16, input: 16) | |
Device(name: "Multi-Output Device", manufacturer: "Apple Inc.", uidString: "~:AMS2_StackedOutput:0", deviceID: 45, output: 2, input: 0) | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment