Joris Kluivers

I like to build software.

AirPlay Audio Streaming on Mountain Lion

One of Mountain Lions big features is AirPlay mirroring to send your OS X desktop to a big screen tv. However there is nothing in the press materials on streaming audio only. Something all apps on iOS devices have been able to do for a while now, but was limited to iTunes on OS X Lion and earlier.

Turns out this is perfectly possible on Mountain Lion but the documentation is a bit lacking on the subject. To encourage more applications to adopt AirPlay audio streaming I’ll explain how to add this to your application. (Yes, that’s a hint to SoundCloud and Spotify).

Spotify, Rdio or Soundcloud user? Check out Airwaves on the Mac App Store, a small utility to stream system audio to one or multiple AirPlay speakers.

This will be quite a technical read, for the impatient: a demo app is available on bitbucket.org.

System wide AirPlay streaming

This is the easy way that requires no modification to existing applications.

The Sound preferences pane in Mountain Lion now offers the ability to redirect all system sound to an AirPlay system. This will allow you to stream audio for applications other than iTunes over AirPlay.

But because this is a system wide setting your audio will be mixed with sounds from other applications. Also redirecting your users to the System preference to change some settings is not ideal.

Per application AirPlay streaming

Ideally every application would offer it’s own AirPlay configuration options like we’re used to in iTunes or on iOS. To achieve this effect you will have to redirect your audio from the system to an AirPlay enabled system somehow. On OS X you will have to use the C based CoreAudio framework for this.

1. Finding the AirPlay audio device

In CoreAudio you interact with audio devices for input and output. Each audio device is identified by a value of the CoreAudio AudioDeviceID type. There are devices for the internal speakers, the headphones audio jack and the line in. When there are nearby AirPlay systems available a single new audio device will become available.

To get the AudioDeviceID for this AirPlay device you loop over all available devices and query it’s transport type.

Browse available audio devices and find AirPlay
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
AudioObjectPropertyAddress addr;
UInt32 propsize;

// target all available audio devices
addr.mSelector = kAudioHardwarePropertyDevices;
addr.mScope = kAudioObjectPropertyScopeWildcard;
addr.mElement = kAudioObjectPropertyElementWildcard;

// get size of available data
AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &addr, 0, NULL, &propsize);

int nDevices = propsize / sizeof(AudioDeviceID);
AudioDeviceID *devids = malloc(propsize);

// get actual device id array data
AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &propsize, devids);

// target device transport type property
addr.mSelector = kAudioDevicePropertyTransportType;
addr.mScope = kAudioObjectPropertyScopeGlobal;
addr.mElement = kAudioObjectPropertyElementMaster;

unsigned int transportType = 0;
propsize = sizeof(transportType);
for (int i=0; i<nDevices; i++) {
  AudioObjectGetPropertyData(devids[i], &addr, 0, NULL, &propsize, &transportType);
  
  if (kAudioDeviceTransportTypeAirPlay == transportType) {
      // Found AirPlay audio device
  }
}

free(devids);

2. Finding all individual AirPlay destinations

Even when you have multiple AirPlay systems available only one audio device will be detected. When you start playing audio to this device only one system will output sound, the other systems available will stay silent. The CoreAudio documentation doesn’t mention anything related to AirPlay and completely fails to mention how to configure the AirPlay device and target individual AirPlay speaker systems.

It turns out that all AirPlay systems are listed in the AudioSource list property on an AudioDevice. To list all individual AirPlay speakers:

List individual AirPlay systems on a AudioDevice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// device id of airplay device found earlier
AudioDeviceID airplayDevice;
AudioObjectPropertyAddr addr;
UInt32 propsize;

// target datasources in output scope
addr.mSelector = kAudioDevicePropertyDataSources;
addr.mScope = kAudioDevicePropertyScopeOutput;
addr.mElement = kAudioObjectPropertyElementWildcard;

AudioObjectGetPropertyDataSize(airplayDevice, &addr, 0, NULL, &propsize);

// list of sourceIds identifying each available AirPlay system
UInt32 *sourceIds = malloc(propsize);

AudioObjectGetPropertyData(airplayDevice, &addr, 0, NULL, &propsize, sourceIds);

To get the name for an AudioSource:

Get AudioSource name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
AudioDeviceID airplayDevice;
UInt32 sourceID;

AudioObjectPropertyAddress nameAddr;
nameAddr.mSelector = kAudioDevicePropertyDataSourceNameForIDCFString;
nameAddr.mScope = kAudioObjectPropertyScopeOutput;
nameAddr.mElement = kAudioObjectPropertyElementMaster;

CFStringRef value = NULL;

AudioValueTranslation audioValueTranslation;
audioValueTranslation.mInputDataSize = sizeof(UInt32);
audioValueTranslation.mOutputData = (void *) &value;
audioValueTranslation.mOutputDataSize = sizeof(CFStringRef);
audioValueTranslation.mInputData = (void *) &sourceID;

UInt32 propsize = sizeof(AudioValueTranslation);

AudioObjectGetPropertyData(airplayDevice, &nameAddr, 0, NULL, &propsize, &audioValueTranslation);

NSLog(@"Source name: %@", (__bridge NSString *)value);

3. Selecting one or more AirPlay destinations to play to

Now that you know what the AirPlay audio device is, and what actual AirPlay systems are available as audio source you can configure your device to play to one or more sources. To do this you write to the audio source property, it accepts the id’s for one or more audio source.

1
2
3
4
5
6
7
8
9
10
11
AudioDeviceID airplayDevice;
UInt32 sourceID;
  
AudioObjectPropertyAddress addr;

// target the data source property
addr.mSelector = kAudioDevicePropertyDataSource;
addr.mScope = kAudioDevicePropertyScopeOutput;
addr.mElement = kAudioObjectPropertyElementMaster;

AudioObjectSetPropertyData(airplayDevice, &addr, 0, NULL, sizeof(UInt32), &sourceID);

4. Playing actual audio

There are two ways that I know of to play audio to a specific AudioDeviceID.

  • The easy way for short sounds is by using -[NSSound setPlaybackDeviceIdentifier:].
  • When using AudioQueues to play audio set the kAudioQueueProperty_CurrentDevice property.

Both methods accept a string identifier value which can be obtained from an AudioDeviceID by querying it’s kAudioDevicePropertyDeviceUID property.

Sample application

The demo applications demonstrates how to pick an AirPlay data source and redirect audio playing on an AudioQueue to the AirPlay audio device.

AirPlayStreaming demo app on bitbucket.org