Core Audio Device Discovery: Enumerating and Tracking macOS Audio Hardware
Enumerate Core Audio devices safely, distinguish available hardware from default routes, and respond to hot-plug changes without blocking audio I/O.
Core Audio exposes audio hardware through an object and property model. The system object represents process-global audio state and owns the devices currently available to the system. Enumerating that device list is a different question from asking which device is currently the default input or output. Applications that collapse those concepts tend to break when a user changes the default route or connects a headset.
The list can change while an application is running. A robust monitor treats enumeration as a snapshot, listens for relevant property changes, then re-reads the property instead of assuming a notification contains a complete replacement list.
Enumerate the system device list
The Core Audio system object exposes its current devices as an array of AudioHardwareDevice values. The system-level list is distinct from the default input and output properties, and each device exposes its own ID, name, stream configuration, and other properties.
import CoreAudio
func listAudioDevices() {
do {
for device in try AudioHardwareSystem.shared.devices {
let name = try device.name
print("Audio device ID: \(device.id), name: \(name)")
}
} catch {
print("Could not enumerate Core Audio devices: \(error)")
}
}
This uses the Swift Core Audio object API. Check capabilities separately and handle property errors independently: a device can disappear between enumeration and a later query. Older code can use AudioObjectGetPropertyDataSize and AudioObjectGetPropertyData with the system object, global scope, and device-list selector.
Track both inventory and routing
Register a property listener on the system object for kAudioHardwarePropertyDevices to learn when inventory may have changed. If the application follows the user’s selected route, separately observe the default-input or default-output property. A device can remain available while no longer being default, and the default can change without a device being unplugged.
Use the listener as an invalidation signal: schedule a fresh enumeration on the application’s control queue, reconcile the resulting IDs with the application’s current state, and update UI or capture/playback configuration there. Retain the listener’s state for as long as it is registered and remove it during shutdown. Do not perform disk I/O, allocate large buffers, or reconfigure an audio render callback from a property notification path.
AudioDeviceID is an object handle for the current HAL topology, not a display name. Resolve properties again after change notifications and use the documented device UID when a stable identity is required across enumerations. Names alone are not unique, and a USB device can return with a different runtime object ID.
Avoid assumptions about stream shape
An audio device may expose multiple streams, input and output scopes, channel configurations, sample-rate ranges, or no usable direction for a particular task. Query the stream configuration and supported formats rather than assuming every listed device is stereo output. Aggregate devices and virtual devices also appear in the HAL object model, so filtering should use the properties relevant to the application rather than a hard-coded device name.
Test a monitor by connecting and disconnecting a USB interface, changing the default route while it remains connected, sleeping and waking the Mac, and removing the device during an active stream. The expected behavior is a refreshed inventory, a deliberate route transition, and a recoverable error rather than a crash in a real-time callback.
Related:
- macOS Disk Arbitration: Observing and Approving Mounts Without Racing Finder
- Unified Logging: How os_log Replaced syslog on macOS
Sources: