Tuesday, 6 May 2014

Creating AVAudioPlayer objects [Sweet, Sweet Music]

You’ll play the sound sample files using AVAudioPlayer objects. You’ll need four. Rather than creating four AVAudioPlayer variables and writing four play actions, create one array to hold all of the objects and one method to play any of them. Start with the AVAudioPlayer objects.
Find the private @interface DDViewController  () section in DDViewController.m, and add the code in bold:

#define kNumberOfPlayers 4
static NSString  *SoundName[kNumberOfPlayers]  = { @"snare", @"bass", @"tambourine", @"maraca"  };

@interface DDViewController  ()
{
MPMusicPlayerController *music;
AVAudioPlayer                     *players[kNumberOfPlayers];
}
@property  (readonly,nonatomic) MPMusicPlayerController *musicPlayer;
-  (void)playingItemDidChangeNotification:(NSNotification*)notification;
-  (void)playbackStateDidChangeNotification:(NSNotification*)notification;
-  (void)createAudioPlayers;
-  (void)destroyAudioPlayers;
@end

The kNumberOfPlayers constant defines the number of sounds, sound player objects, and sound buttons that
this app uses. Also defined is a static array of string constant objects.
Each element is the name of a sound sampleresource file. The players instance variable is an array of AVAudioPlayer objects.

The methods -createAudioPlayers and -destroyAudioPlayers create and destroy all four audio player objects at once. Add them to the end of your @implementation  section:

-  (void)createAudioPlayers
{
for ( NSUInteger  i=0;  i<kNumberOfPlayers; i++)
{
NSURL *soundURL   = [[NSBundle  mainBundle]   URLForResource:SoundName[i] withExtension:@"m4a"];
players[i] = [[AVAudioPlayer   alloc] initWithContentsOfURL:soundURL error:NULL];
players[i].delegate = self; [players[i] prepareToPlay];
}
}
-  (void)destroyAudioPlayers
{
for ( NSUInteger  i=0;  i<kNumberOfPlayers; i++) players[i] =nil;
}
createAudioPlayers loops through the array of sound name constants (SoundName[i]) and uses that to create a URL that refers to the m4a sound resource file that you added earlier. This URL is used to create and initialize a newAVAudioPlayer object that will play that sound file.

Your controller object is set to be the sound players delegate (you’ll use that later). Finally, some optimization is applied. The -prepareToPlay message is sent to the sound player. This preps the player object so that it is immediately ready to play its sound.

The -destroyAudioPlayers method is self-explanatory, and you dont need it yet. It will come into “play” later.

Next up are the buttons to play these sounds and the action method to make that happen. Get the action declaration, and a few odds and ends, out of the way first. Switch to your DDViewController.h file. Underneath the #importstatements, add a new one:

#import  <AVFoundation/AVFoundation.h>

This imports the definitions for the AVAudioPlayer and related classes. Next, make your controller an
AVAudioPlayerDelegate:

@interface DDViewController  : UIViewController <MPMediaPickerControllerDelegate,
AVAudioPlayerDelegate>

Add a new action method to the @interface:

-  (IBAction)bang:(id)sender;

Now you’re ready to design the interface.

Adding the Sound Buttons

Return to your Main.storyboard Interface Builder file. Drag in a new UIButton object. Select it and do the following (see Figure 9-15):

n     Use the size inspector to set both its width and height to 100 pixels.

n     Use the attributes inspector to:

n     Set its type  property to Custom.

n     Clear its title text property (deleting “Button”).

n     Set its image property to snare.

n     Scroll down to its tag  property and change it from 0 to 1.

n     Choose Editor  Pin  Width.
n     Select the button again and choose Editor  Pin  Height.

n     Use the connections inspector to connect its Touch Down event to the new
-bang: action of the View Controller object.


Figure 9-15. Creating the first bang button

There are a couple of noteworthy aspects to this buttons configuration. First, you’ve connected the Touch Down event, instead of the more common Touch Up  Inside event. Thats because you want to receive the -bang: actionmessage the instant the user touches the button. Normally, buttons dont send their action message until the user touches them and releases again, with their finger still inside the button. Thus, the action name Touch UpInside.”

Secondly, you didnt create an outlet to connect to this button. You’re going to identify, and access, the object via its tag  property. All UIView objects have an integer tag  property. It exists solely for your use in identifying views; iOS doesnt use it for anything else. You’re going to use the tag  to determine which sound to play, and later to obtain the UIButton object in the interface.

Duplicate the new button three times, to create four buttons in all. You can do this either using the clipboard, or by holding down the Option key and dragging out new copies of the button. Arrange them in a group, as shownin Figure 9-16, and then center that group in the interface. Choose Editor  Resolve Auto Layout Issues  Add Missing Constraints in View Controller.


Figure 9-16. Duplicating the bang button

All of the buttons have the same type, image, tag, constraints, and action connection. Use the attributes inspector to change the image and tag properties of the other three, starting with
the upper-right button and working clockwise, using the following table:

Button
Image
Tag

Upper-right

bass

2
Lower-right
maraca
4
Lower-left
tambourine
3
The finished interface should look like the one in Figure 9-17.


Figure 9-17. Finished DrumDub interface

Return again to DDViewController.m and add the -bang: method to your implementation:

-  (IBAction)bang:(id)sender
{
NSInteger playerIndex  = [sender tag]-1;
if (playerIndex>=0 &&   playerIndex<kNumberOfPlayers)
{
AVAudioPlayer  *player = players[playerIndex]; [player pause];
player.currentTime = 0; [player play];
}
} 
All four buttons send the same action. You determine which button sent the message using its tag property. Your four buttons have tag  values between 1 and 4, which you will use as an index (0 through 3) to obtain thatbuttons AVAudioPlayer object.

Once you have the buttons AVAudioPlayer, you first send it a –pause  message. This will suspend playback of the sound if its currently playing. If not, it does nothing.

Then the currentTime property is set to 0. This property is the players logical “play head,” indicating the position (in seconds) where the player is currently playing, or will begin playing. Setting it to rewinds” the sound so itplays from the beginning.

Finally, the -play message starts the sound playing. The -play message is asynchronous; it starts a background task to play and manage the sound, and then returns immediately.

There are just two more details to take care of before your sounds will play.

Activating Your Audio Session

Its not strictly required, but the documentation for the AVAudioSession class recommends that your app activate the audio session when it starts, and again whenever your audio session is interrupted. You’ll take thisopportunity to prepare the audio player objects at the same time. Add an -activateAudioSession method to your DDViewController.m implementation. Start by adding a prototype to the private @interface section at the top:

-  (void)activateAudioSession;

Find the –viewDidLoad method and send the controller this message when it first loads (the new line in bold):

-  (void)viewDidLoad
{
[super  viewDidLoad];
[self  activateAudioSession];
}

And add the method to the @implementation:

-  (void)activateAudioSession
{
BOOL  active = [[AVAudioSession sharedInstance] setActive:YES error:NULL]; if (active &&   players[0]==nil)
[self createAudioPlayers]; if (!active)
[self  destroyAudioPlayers];
for ( NSUInteger  i=0;  i<kNumberOfPlayers; i++) [(UIButton*)[self.view viewWithTag:i+1] setEnabled:active];
}

The first line obtains your apps audio session object (the same one you configured back in -appli cation:didFinishLaunchingWithOptions:). You send it a -setActive:error: message to activate, or reactivate, the audio session.

The -setActive:error: message returns YES if the audio session is now active. There are a few obscure situations where this will fail (returning NO), and your app should deal with that situation gracefully.

In this app, you look to see if the session was activated and send -createAudioPlayers to prepare the AVAudioPlayer objects for playback. If the session couldnt be activated (which means your app cant use any audio), then youdestroy any AVAudioPlayer objects you previously created and disable all of the sound effect buttons in the interface.

Since you dont have an outlet connected to those buttons, you’ll get them using their tag.
The-viewWithTag: message searches the hierarchy of a view
object and returns the first subview object matching that tag. Your bang buttons are the only views with tag values of 1, 2, 3, and 4. The loop obtains each button view andenables, or disables, it.

The functional portion of your app is now finished. By functional, I mean that you can run your app, play music, and annoy anyone else in the room with cheesy percussion noises, as shown in Figure 9-18.


Figure 9-18. Working DrumDub app

Interruptions and Detours

In the “Living in a Larger World” section, I described the multitude of events and situations that conspire to complicate your apps use of audio. Most people hate interruptions or being forced to take a detour, and I suspect appdevelopers are no different. But dealing with these events gracefully is the hallmark of a finely crafted iOS app. First up are interruptions.

Dealing with Interruptions

An Interruption occurs when another app or service needs to activate its audio session. The most common sources of interruptions are incoming phone calls and alerts (triggered by alarms, messages, notification, andreminders).

Most of the work of handling interruptions is done for you. When your apps audio session is interrupted, iOS fades out your audio and deactivates your session. The usurping session then
takes over and begins playing the users ring tone or alert sound. Your app, audio, and music player delegates then receive “begin interruption” messages.

Your app should do whatever is appropriate to respond to the interruption. Often, this isnt much. You might update the interface to indicate that you’re no longer playing music. Mostly, your app should just make a note ofwhat it was doing so it can resume when the interruption ends.

Interruptions can be short: a few seconds, for alarms. Or they can be very (very) long: an hour or more, if you accept that incoming phone call from chatty aunt May. Dont make any assumptions on how long the interruption willlast, just wait for iOS to notify your app when its over.

When the interruption is over, your app will receive “end interruption” messages. This is where the work begins. First, your app should explicitly reactivate its audio session. This isnt a strict requirement, but itsrecommended. It gives your app a chance to catch the (very rare) situation where your audio session cant be reactivated.

Then you need to resume playback, reload audio objects, update your interface, or whatever else your app needs to do so it is once again running, exactly as it was before the interruption occurred. In DrumDub, theressurprisingly little work to do, as most of the default music and audio player behavior is exactly what you want. Nevertheless, theres still some rudimentary interruption handling you need to add.


Adding Your Interruption Handlers

Interruption messages can be received in a number of different ways. Your app only needs to observe those it wants and are convenient; theres no need to observe them all. Begin and end interruption messages are sent to:

n     The audio session delegate (AVAudioSessionDelegate)

n     All audio player delegates (AVAudioPlayerDelegate)

n     Any observer of music player state change notifications (MPMusicPlayerControllerPlaybackStateDidChangeNotification)

Decide how you want your app to respond to interruptions, and then implement the handlers that conveniently let you do that. When something interrupts DrumDub, you want to:

n     Pause the playback of the music.

n     Stop any percussion sound thats playing (so it doesnt resume when the interruption is over). 
When the interruption ends, you want DrumDub to:

n     Reactivate the audio session and check for problems.

n     Resume playback of the music.

Pausing and resuming the music player requires no code. The MPMusicPlayerController
class does this automatically in response to interruptions. You dont even need to add any code to update your interface. When the music player is interrupted, its playbackState changes toMPMusicPlaybackStateInterrupted and your controller receives a
-playbackStateDidChangeNotification: message, which updates your play and pause buttons.
When the interruption ends, the music player resumes playing and sends another state change notification.

So DrumDubs only non-standard behavior is to silence any playing percussion sounds when an interruption arrives. Thats so the “tail end” of the sound bite doesnt start playing again when the interruption is over. Handle that byadding this audio player delegate method to DDViewController.m:

-  (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player
{
[player pause];
}

Your controller object is already the delegate for all four of the audio players. Your controller can receive this message up to four times (once for each player).

The last task on the list is to reactivate the audio session when the interruption is over. To do that, make your DDViewController  object the audio sessions delegate and handle the -endInterruption delegate message. Start bymodifying the class declaration in DDViewController.h:

@interface DDViewController  : UIViewController <MPMediaPickerControllerDelegate, AVAudioSessionDelegate, AVAudioPlayerDelegate>

Back in DDViewController.m, locate the -viewDidLoad method and set the sessions delegate property:

-  (void)viewDidLoad
{
[super  viewDidLoad];
[[AVAudioSession  sharedInstance] setDelegate:self];
[self  activateAudioSession];
}

Finally, implement an -endInterruption method so your controller will receive this message from the audio session:

-  (void)endInterruption
{
[self  activateAudioSession];
}

You already wrote the code to (re)activate the audio session and update your interface.
All -endInterruption has to do is perform that again.

With the tricky business of interruptions taken care of, its time to deal with detours (route changes).


Dealing with Audio Route Changes

An audio route is the path that data takes to get to the eardrum of the listener. Your iPhone might be paired to the speakers in your car. When you get out of your car, your iPhone switches to its built-in speakers. When youplug in some headphones, it stops playing through its speaker and begins playing through your headphones. Each of these events is an audio route change.

You deal with audio route changes exactly the way you deal with interruptions: decide what your app should do in each situation, and then write handlers to observe those events and implement your policies. From DrumDub, you want to implement Apples recommended behavior of stopping music playback when the user unplugs their headphones, or disconnects from external speakers. If these were sound effects in a game, or something similar, it would be appropriate to let them continue playing. But DrumDubs music will stop playing when the headphones are unplugged,
so the instrument sounds should stop too.

Audio route notifications are posted by the AVAudioSession object, all you have to do is observe them. Begin by defining an audio route change notification handler, adding its prototype to the private @interfaceDDViewController  () section in DDViewController.m:

-  (void)audioRouteChangedNotification:(NSNotification*)notification;

Next, request that your DDViewController  object receive audio route change notifications. At the end of the -viewDidLoad method, add this code:

[[NSNotificationCenter defaultCenter]  addObserver:self selector:@selector(audioRouteChangedNotification:)
name:AVAudioSessionRouteChangeNotification object:nil];

Now add the method to your @implementation  section:

-  (void)audioRouteChangedNotification:(NSNotification*)notification
{
NSNumber  *changeReason  = 
notification.userInfo[AVAudioSessionRouteChangeReasonKey]; if ([changeReason integerValue]==  
AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
{
for ( NSUInteger  i=0;  i<kNumberOfPlayers; i++) [players[i]  pause];
}
}

The method begins by examining the reason for the audio route change. It gets this information from the notifications userInfo dictionary. If the value of the AVAudioSessionRouteChangeReasonKey isAVAudioSessionRouteChangeReasonOldDeviceUnavailable, it indicates that a previously active audio route is no longer available. This happens when headphones are unplugged, the device is removed from a dock connector, awireless speaker system is disconnected, and so on. If thats the case,
it stops playback of all four audio players.

That wraps up this app! Go ahead and run it again to make sure everything is working. You’ll want to test your interruption and audio route change logic by doing things like:

n     Setting an alarm to interrupt playback

n     Calling your iPhone from another phone

n     Plugging and unplugging headphones

Testing your app under as many situations as you can devise is an important part of app development.


Other Audio Topics

This chapter didnt even begin to approach the subjects of audio recording or signal processing.
To get started with these, and similar topics, start with the Multimedia Programming Guide. It providean overview and roadmap for playing, recording, and manipulating both audio and video in iOS.

If you need to perform advanced or low-level audio tasks (such as analyzing or encoding audio), refer to the Core Audio Overview. All of these documents can be found in Xcodes Documentation and API Reference.

Heres something else to look at: if you need to present audio or video in a view, want your app
to play music in the background (that is, when your app is not running), or need to handle remote events, take a look at the AVPlayer and AVPlayerLayer classes. The first is a near-universal media player for both audio andvideo, similar to MPMusicPlayerController and AVAudioPlayer. Its a little more complicated, but also more capable. It will work in conjunction with an AVPlayerLayer object to present visual content (movie) in a view, so you cancreate your own YouTube-style video player.

Summary

Sound adds a rich dimension to your app. You’ve learned how to plaancontrol audio from the iPolibraras well as resource files bundled in your app. You understand the importance of configurinyour audio session, andintelligently handing interruptions anaudio route changes. “Playing nice” with other audio sources creates the kind of experience that users enjoy, anwill want to use agaiand again.
But is there more to iOS interfaces than labels, buttons, and image views?
Join me in the next chapter to find out.

No comments:

Post a Comment