Detecting when an iPad Headphone or Docking Connector is Unplugged (Xcode 4)

From Techotopia
Revision as of 20:01, 27 October 2016 by Neil (Talk | contribs) (Text replacement - "<table border="0" cellspacing="0">" to "<table border="0" cellspacing="0" width="100%">")

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
PreviousTable of Contents
Recording Audio on an iPad with AVAudioRecorder (Xcode 4)


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


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.


Contents


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. 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:

#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

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:

- (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];
   }
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

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:

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

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:

if (inPropertyID != kAudioSessionProperty_AudioRouteChange) 
   return;

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:

audioViewController *controller = (audioViewController *) inUserData;

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:

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;

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:

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];
    }
}

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

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];
    }
  }
}

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 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.


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book



PreviousTable of Contents
Recording Audio on an iPad with AVAudioRecorder (Xcode 4)