Core Audio has a very neat feature, enabling you to create virtual audio input and output devices called aggregate devices, which join together and synchronise the clocks of multiple real devices. You can then use these aggregate devices for audio input and output in the same way as real devices.

One way to create a new aggregate device is via the Audio MIDI Setup application included with Mac OS X (in /Applications/Utilities/). However, this doesn’t help you if you want to create one programmatically in your audio application.

This article describes the process of creating and destroying an aggregate device in code, and describes the known issues when working with aggregate devices. It also provides sample code for device creation and destruction.

Problems when creating aggregate devices

Before you start creating devices, you should know that (as of Mac OS 10.5.4) there are several problems when working with aggregate devices. Here are the ones I know about, together with workarounds.

Most of this information is gleaned from Apple’s Core Audio API mailing list, where Jeff Moore of Apple has posted many helpful emails on the subject of aggregate devices. The main source of documentation for aggregate devices is the AudioHardware.h header file inside the Core Audio framework.

Devices can’t be private

If you try and create a private device with kAudioAggregateDeviceIsPrivateKey (to make a device that is only usable if you know its UID), then it simply won’t work. The only workaround is to make all aggregate devices public. This does mean that they are available to any application, and can be viewed / modified / deleted by a user in Audio MIDI Setup. Not great.

Custom channel layouts are reused for the same UID

If you create an aggregate device, and a user changes the channel layout for the device via Audio MIDI Setup’s “Configure Speakers” window, then the amended channel layout will be re-used whenever you create an aggregate device with the same UID – even after a restart. The only workaround to this is to use a different UID for every aggregate device you create.

Edit: this particular bug has been fixed in Mac OS X 10.5.6.

Setting the sub-device list when first creating the aggregate device does not work correctly

I have found that the only way to reliably create an aggregate device is to create a blank device, and then manually set the sub-device list afterwards.

The new aggregate device “disappears” sometimes immediately after creation

This can result in an incorrectly initialised device. The only workaround is to pause for a short while after device creation, to give CoreAudio chance to catch up. There’s no way to check when the device is definitely created – all you can do is to pause via CFRunLoopRunInMode for a bit, and wait for CoreAudio to catch up. It seems to work.

The new aggregate device “disappears” again sometimes after adding the sub-device list

Same as above – the only workaround is to pause via CFRunLoopRunInMode to give CoreAudio chance to catch up.

Sample code to create an aggregate device

Here’s an edited version of the code I use to create aggregate devices. This code creates a simple aggregate for one device. Not much use in practice, but this is the simplest example. To add more devices, simply include more device UIDs to the sub-device array.

Note that you will need to have the Unique ID (UID) for each audio device you wish to add to the aggregate. I haven’t included code for this, but you can convert a given AudioDeviceID into a UID by calling AudioDeviceGetProperty for the kAudioDevicePropertyDeviceUID property, to retrieve the UID.

In order to use this code, you’ll need to link your XCode project against the CoreFoundation and CoreAudio frameworks, and include CoreFoundation/CoreFoundation.h and CoreAudio/CoreAudio.h.

OSStatus CreateAggregateDevice() {

OSStatus osErr = noErr;
UInt32 outSize;
Boolean outWritable;

//———————–
// Start to create a new aggregate by getting the base audio hardware plugin
//———————–

osErr = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyPlugInForBundleID, &outSize, &outWritable);
if (osErr != noErr) return osErr;

AudioValueTranslation pluginAVT;

CFStringRef inBundleRef = CFSTR(“com.apple.audio.CoreAudio”);
AudioObjectID pluginID;

pluginAVT.mInputData = &inBundleRef;
pluginAVT.mInputDataSize = sizeof(inBundleRef);
pluginAVT.mOutputData = &pluginID;
pluginAVT.mOutputDataSize = sizeof(pluginID);

osErr = AudioHardwareGetProperty(kAudioHardwarePropertyPlugInForBundleID, &outSize, &pluginAVT);
if (osErr != noErr) return osErr;

//———————–
// Create a CFDictionary for our aggregate device
//———————–

CFMutableDictionaryRef aggDeviceDict = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

CFStringRef AggregateDeviceNameRef = CFSTR(“Your Aggregate Name”);
CFStringRef AggregateDeviceUIDRef = CFSTR(“com.yourcompany.yourchoiceofuid”);

// add the name of the device to the dictionary
CFDictionaryAddValue(aggDeviceDict, CFSTR(kAudioAggregateDeviceNameKey), AggregateDeviceNameRef);

// add our choice of UID for the aggregate device to the dictionary
CFDictionaryAddValue(aggDeviceDict, CFSTR(kAudioAggregateDeviceUIDKey), AggregateDeviceUIDRef);

//———————–
// Create a CFMutableArray for our sub-device list
//———————–

// this example assumes that you already know the UID of the device to be added
// you can find this for a given AudioDeviceID via AudioDeviceGetProperty for the kAudioDevicePropertyDeviceUID property
// obviously the example deviceUID below won’t actually work!
CFStringRef deviceUID = CFSTR(“UIDOfDeviceToBeAdded”);

// we need to append the UID for each device to a CFMutableArray, so create one here
CFMutableArrayRef subDevicesArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);

// just the one sub-device in this example, so append the sub-device’s UID to the CFArray
CFArrayAppendValue(subDevicesArray, deviceUID);

// if you need to add more than one sub-device, then keep calling CFArrayAppendValue here for the other sub-device UIDs

//———————–
// Feed the dictionary to the plugin, to create a blank aggregate device
//———————–

AudioObjectPropertyAddress pluginAOPA;
pluginAOPA.mSelector = kAudioPlugInCreateAggregateDevice;
pluginAOPA.mScope = kAudioObjectPropertyScopeGlobal;
pluginAOPA.mElement = kAudioObjectPropertyElementMaster;
UInt32 outDataSize;

osErr = AudioObjectGetPropertyDataSize(pluginID, &pluginAOPA, 0, NULL, &outDataSize);
if (osErr != noErr) return osErr;

AudioDeviceID outAggregateDevice;

osErr = AudioObjectGetPropertyData(pluginID, &pluginAOPA, sizeof(aggDeviceDict), &aggDeviceDict, &outDataSize, &outAggregateDevice);
if (osErr != noErr) return osErr;

// pause for a bit to make sure that everything completed correctly
// this is to work around a bug in the HAL where a new aggregate device seems to disappear briefly after it is created
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);

//———————–
// Set the sub-device list
//———————–

pluginAOPA.mSelector = kAudioAggregateDevicePropertyFullSubDeviceList;
pluginAOPA.mScope = kAudioObjectPropertyScopeGlobal;
pluginAOPA.mElement = kAudioObjectPropertyElementMaster;
outDataSize = sizeof(CFMutableArrayRef);
osErr = AudioObjectSetPropertyData(outAggregateDevice, &pluginAOPA, 0, NULL, outDataSize, &subDevicesArray);
if (osErr != noErr) return osErr;

// pause again to give the changes time to take effect
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);

//———————–
// Set the master device
//———————–

// set the master device manually (this is the device which will act as the master clock for the aggregate device)
// pass in the UID of the device you want to use
pluginAOPA.mSelector = kAudioAggregateDevicePropertyMasterSubDevice;
pluginAOPA.mScope = kAudioObjectPropertyScopeGlobal;
pluginAOPA.mElement = kAudioObjectPropertyElementMaster;
outDataSize = sizeof(deviceUID);
osErr = AudioObjectSetPropertyData(outAggregateDevice, &pluginAOPA, 0, NULL, outDataSize, &deviceUID);
if (osErr != noErr) return osErr;

// pause again to give the changes time to take effect
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);

//———————–
// Clean up
//———————–

// release the CF objects we have created – we don’t need them any more
CFRelease(aggDeviceDict);
CFRelease(subDevicesArray);

// release the device UID
CFRelease(deviceUID);

return noErr;

}

You can now use the AudioDeviceID “outAggregateDevice” to reference your new aggregate device.

To destroy an existing aggregate device, use a function like this one:

OSStatus DestroyAggregateDevice(AudioDeviceID inDeviceToDestroy) {

OSStatus osErr = noErr;

//———————–
// Start by getting the base audio hardware plugin
//———————–

UInt32 outSize;
Boolean outWritable;
osErr = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyPlugInForBundleID, &outSize, &outWritable);
if (osErr != noErr) return osErr;

AudioValueTranslation pluginAVT;

CFStringRef inBundleRef = CFSTR(“com.apple.audio.CoreAudio”);
AudioObjectID pluginID;

pluginAVT.mInputData = &inBundleRef;
pluginAVT.mInputDataSize = sizeof(inBundleRef);
pluginAVT.mOutputData = &pluginID;
pluginAVT.mOutputDataSize = sizeof(pluginID);

osErr = AudioHardwareGetProperty(kAudioHardwarePropertyPlugInForBundleID, &outSize, &pluginAVT);
if (osErr != noErr) return osErr;

//———————–
// Feed the AudioDeviceID to the plugin, to destroy the aggregate device
//———————–

AudioObjectPropertyAddress pluginAOPA;
pluginAOPA.mSelector = kAudioPlugInDestroyAggregateDevice;
pluginAOPA.mScope = kAudioObjectPropertyScopeGlobal;
pluginAOPA.mElement = kAudioObjectPropertyElementMaster;
UInt32 outDataSize;

osErr = AudioObjectGetPropertyDataSize(pluginID, &pluginAOPA, 0, NULL, &outDataSize);
if (osErr != noErr) return osErr;

osErr = AudioObjectGetPropertyData(pluginID, &pluginAOPA, 0, NULL, &outDataSize, &inDeviceToDestroy);
if (osErr != noErr) return osErr;

return noErr;

}