A long-overdue Core Audio API in pure Swift was silently introduced in macOS Sequoia. Now you can easily get the default output device and even all available controls of that device without resorting to unsafe pointers. However I didn't found a way to find which controls related to input and which to output:
let system = AudioHardwareSystem.shared
guard let defaultOutputDevice = try system.defaultOutputDevice else { return }
let allControls = try defaultOutputDevice.controls
let volumeControls = allControls.filter { (try? $0.classID) == kAudioVolumeControlClassID }
print(volumeControls) // 2 volume controls, input + output, but which is which?
One can always resort to the old way, like:
let system = AudioHardwareSystem.shared
guard let defaultOutputDevice = try system.defaultOutputDevice else { return }
let address = AudioObjectPropertyAddress(mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMain)
guard defaultOutputDevice.hasProperty(address: address) else { return }
let data = try defaultOutputDevice.propertyData(address: address)
let volume = data.withUnsafeBytes { $0.load(as: Float32.self) }
But is there any way to figure out which AudioHardwareControl
is related to input and which to output?
The question "how to know whether an audio control controls input or output (or both)"? is also valid for the lower level Core Audio API, and the answer is to query the control's kAudioControlPropertyScope
property.
Unfortunately, it looks like the author of the Swift wrapper forgot to wrap this property (along with with its sibling propertykAudioControlPropertyElement
), but that's ok because you can still access it by resorting to unsafe pointers:
extension AudioHardwareControl {
var forgottenScope: AudioObjectPropertyScope {
get throws {
let scopePropAddr = AudioObjectPropertyAddress(
mSelector: kAudioControlPropertyScope, mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain)
let scopeData = try self.propertyData(address: scopePropAddr)
return scopeData.withUnsafeBytes {
rawBuffer in
rawBuffer.load(as: AudioObjectPropertyScope.self)
}
}
}
}
with which you can query just output-only volume controls like so:
let outputVolumeControls = allControls.filter {
(try? $0.classID) == kAudioVolumeControlClassID
&& (try? $0.forgottenScope) == kAudioObjectPropertyScopeOutput
}
Apart from this small oversight in AudioHardwareControl
(which you should file with Apple, probably), this wrapper looks pretty good! It definitely makes interacting with CoreAudio in Swift much less tiresome and interestingly, it gives a much clearer picture of the CoreAudio object hierarchy than does sifting through the endless list of CoreAudio property selectors and their comments, which for some reason are redacted from autogenerated Swift interfaces.
Nice find!