Changes

New page: The iPad provides three physical options for the playback of audio. These consist of the built-in speakers, a connection to the headphone socket or via a device attached to the docking con...
The iPad provides three physical options for the playback of audio. These consist of the built-in speakers, a connection to the headphone socket or via a device attached to the docking connector. Apple’s human interface guidelines for the implementation of iPad applications recommend that when either the docking connector or headphones are unplugged during audio playback that the audio be automatically paused and then resumed when the connection is reestablished.

In this chapter, therefore, we will look at how to detect when either the headphones or a device attached to the docking connector are unplugged from an iPad, a concept referred to by Apple as an audio hardware route change.

== Detecting a Change to the Audio Hardware Route ==

In order to detect that a connection to either the iPad headphone or docking connector has been unplugged or reconnected it is necessary to configure a property listener on the kAudioSessionProperty_AudioRouteChange property of the current audio session and, in so doing, specify a callback to be triggered when a change to this property occurs.

The kAudioSessionProperty_AudioRouteChange property is actually an object (of type CFDictionary) from which it is possible to identify details such as the reason for the property change and the old route (for example if audio was playing through the speakers or the headphones prior to the route change). For example, when the headphone or dock connector is unplugged, the reason for the route change will be represented by a kAudioSessionRouteChangeReason_OldDeviceUnavailable value in the dictionary of the kAudioSessionProperty_AudioRouteChange property. Conversely, the detection of a new device is represented by kAudioSessionRouteChangeReason_NewDeviceAvailable.

The old route value is stored as a string value representing one of the audio output options, namely “Headphone”, “Speaker” or “LineOut” (the latter representing the dock connector).

== An Example iPad Headphone and Dock Connector Detection Application ==

The concepts involved in detecting audio route changes in iOS are actually quite simple and are, perhaps, best explained by demonstration. For the purposes of this tutorial we will be adding functionality to the audio application created in the chapter entitled [[Playing Audio on an iPad using AVAudioPlayer (Xcode 4)|Playing Audio on an iPad using AVAudioPlayer]]. Begin, therefore, by loading the ''audio'' project created in that chapter in Xcode.

== Adding the AudioToolBox Framework to the Project ==

The code used in this project will make use of the AudioToolBox framework. The first step, therefore, is to ensure this is included in the project. This can be achieved by selecting the product target entry from the project navigator panel (the top item named audio) and clicking on the ''Build Phases'' tab in the main panel. In the ''Link Binary with Libraries'' section click on the ‘+’ button, select the ''AudioToolBox.framework'' entry from the resulting panel and click on the ''Add'' button.

It will also be necessary to import the ''<AudioToolBox/AudioToolBox.h>'' file into the application code. To do so, select the ''audioViewController.h'' file and modify it to add the import directive and create an outlet for the audioPlayer object as follows:

<pre>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>

@interface audioViewController : UIViewController
<AVAudioPlayerDelegate>
{
AVAudioPlayer *audioPlayer;
UISlider *volumeControl;
}
@property (nonatomic, retain) IBOutlet UISlider *volumeControl;
@property (nonatomic, retain) AVAudioPlayer *audioPlayer;
-(IBAction) playAudio;
-(IBAction) stopAudio;
-(IBAction) adjustVolume;
@end
</pre>

== Configuring the Property Listener ==

As previously discussed, the application code needs to set up a property listener and specify a callback to be triggered when the audio route changes. This requires that the view controller declare itself as the delegate for the audio session. In order to implement this in our audio project we need to add some code to the viewDidLoad method of the ''audioViewController.m'' file:

<pre>
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL fileURLWithPath:
[[NSBundle mainBundle]
pathForResource:@"Kalimba"
ofType:@"mp3"]];

[[AVAudioSession sharedInstance] setDelegate: self];

AudioSessionAddPropertyListener (
kAudioSessionProperty_AudioRouteChange,
audioRouteChangeListenerCallback,
self);

NSError *error;
audioPlayer = [[AVAudioPlayer alloc]
initWithContentsOfURL:url
error:&error];

if (error)
{
NSLog(@"Error in audioPlayer: %@",
[error localizedDescription]);
} else {
audioPlayer.delegate = self;
[audioPlayer prepareToPlay];
}
}
</pre>

== Writing the Property Listener Callback ==

The property listener callback is actually a C function as opposed to an Objective-C method. As such extra steps need to be taken in the function to access the audioViewController object instance.

The function is declared as follows:

<pre>
void audioRouteChangeListenerCallback (
void *inUserData,
AudioSessionPropertyID inPropertyID,
UInt32 inPropertyValueSize,
const void *inPropertyValue)
{
// Code here
}
</pre>

The first step is to check why the callback was called. For the purposes of this example we are only interested in acting if the reason for the call was due to a change to the audio route, otherwise the function should simply return:

<pre>
if (inPropertyID != kAudioSessionProperty_AudioRouteChange)
return;
</pre>

The next step is to establish a reference to the view controller handling the audio playback. This information can be obtained from the inUserData argument passed through to the callback:

<pre>
audioViewController *controller = (audioViewController *) inUserData;
</pre>

Having obtained a reference to the view controller we can now extract information about the reason for the callback being triggered and also the previous audio route:

<pre>
CFDictionaryRef routeChangeDictionary = inPropertyValue;
CFNumberRef routeChangeReasonRef =
CFDictionaryGetValue (
routeChangeDictionary,
CFSTR (kAudioSession_AudioRouteChangeKey_Reason));

SInt32 routeChangeReason;

CFNumberGetValue (
routeChangeReasonRef,
kCFNumberSInt32Type,
&routeChangeReason);

CFStringRef oldRouteRef =
CFDictionaryGetValue (
routeChangeDictionary,
CFSTR (kAudioSession_AudioRouteChangeKey_OldRoute));

NSString *oldRouteString = (NSString *)oldRouteRef;
</pre>

On completion of execution, oldRouteString references a string containing the previous audio route and the routeChangeReason variable contains an integer value representing the reason for the change.

Now that we have information on why the callback was triggered and what the previous audio route was all we need to do is write some simple conditional code to pause and resume audio playback depending on this data:

<pre>
if (routeChangeReason == kAudioSessionRouteChangeReason_NewDeviceAvailable)
{
if ([oldRouteString isEqualToString:@"Speaker"])
{
[controller.audioPlayer play];
}
}

if (routeChangeReason == kAudioSessionRouteChangeReason_OldDeviceUnavailable) {

if ((controller.audioPlayer.playing == YES) &&
(([oldRouteString isEqualToString:@"Headphone"]) ||
([oldRouteString isEqualToString:@"LineOut"])))
{
[controller.audioPlayer pause];
}
}
</pre>

Bringing this code all together gives us a callback function that reads as follows:

<pre>
void audioRouteChangeListenerCallback (
void *inUserData,
AudioSessionPropertyID inPropertyID,
UInt32 inPropertyValueSize,
const void *inPropertyValue)
{
if (inPropertyID != kAudioSessionProperty_AudioRouteChange)
return;

audioViewController *controller =
(audioViewController *) inUserData;

CFDictionaryRef routeChangeDictionary = inPropertyValue;

CFNumberRef routeChangeReasonRef =
CFDictionaryGetValue (
routeChangeDictionary,
CFSTR (kAudioSession_AudioRouteChangeKey_Reason));

SInt32 routeChangeReason;

CFNumberGetValue (
routeChangeReasonRef,
kCFNumberSInt32Type,
&routeChangeReason);

CFStringRef oldRouteRef =
CFDictionaryGetValue (
routeChangeDictionary,
CFSTR (kAudioSession_AudioRouteChangeKey_OldRoute));

NSString *oldRouteString = (NSString *)oldRouteRef;

if (routeChangeReason == kAudioSessionRouteChangeReason_NewDeviceAvailable)
{
if ([oldRouteString isEqualToString:@"Speaker"])
{
[controller.audioPlayer play];
}
}

if (routeChangeReason ==
kAudioSessionRouteChangeReason_OldDeviceUnavailable)
{
if ((controller.audioPlayer.playing == YES) &&
(([oldRouteString isEqualToString:@"Headphone"]) ||
([oldRouteString isEqualToString:@"LineOut"])))
{
[controller.audioPlayer pause];
}
}
}
</pre>

== Testing the Application ==

In order to test the application it will be necessary to load it onto a physical iPad device since the iOS Simulator does not provide a mechanism for simulating changes to the status of the headphone or dock connectors. If the audio project is not already provisioned to run on a device, follow the steps outlined in [[Testing iOS 4 Apps on the iPad – Developer Certificates and Provisioning Profiles (Xcode 4)|Testing iOS 4 Apps on the iPad – Developer Certificates and Provisioning Profiles]] to build and install the application onto an iPad device with headphones attached. Once the application has launched, begin audio playback and then unplug the headphones. At this point, playback should pause. Reconnecting the headphones should then resume playback. The application should exhibit the same behavior when tested with the docking connector attached to an audio device.