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 player’s 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 don’t 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 button’s configuration. First, you’ve connected the Touch Down event, instead of the more common Touch Up Inside event. That’s because you want to receive the -bang: actionmessage the instant the user touches the button. Normally, buttons don’t 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 didn’t 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 doesn’t 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 thatbutton’s AVAudioPlayer object.
Once you have the button’s AVAudioPlayer, you first send it a –pause message. This will suspend playback of the sound if it’s currently playing. If not, it does nothing.
Then the currentTime property is set to 0. This property is the player’s logical “play head,” indicating the position (in seconds) where the player is currently playing, or will begin playing. Setting it to 0 “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
It’s 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 app’s 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 couldn’t be activated (which means your app can’t use any audio), then youdestroy any AVAudioPlayer objects you previously created and disable all of the sound effect buttons in the interface.
Since you don’t 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 app’s 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 app’s audio session is interrupted, iOS fades out your audio and deactivates your session. The usurping session then
takes over and begins playing the user’s 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 isn’t 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. Don’t make any assumptions on how long the interruption willlast, just wait for iOS to notify your app when it’s 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 isn’t a strict requirement, but it’srecommended. It gives your app a chance to catch the (very rare) situation where your audio session can’t 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, there’ssurprisingly little work to do, as most of the default music and audio player behavior is exactly what you want. Nevertheless, there’s 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; there’s 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 that’s playing (so it doesn’t 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 don’t 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 DrumDub’s only non-standard behavior is to silence any playing percussion sounds when an interruption arrives. That’s so the “tail end” of the sound bite doesn’t 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 session’s 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 session’s 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, it’s 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 Apple’s 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 DrumDub’s 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 notification’s 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 that’s 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 didn’t 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 provides an 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 Xcode’s Documentation and API Reference.
Here’s 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. It’s 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 play and control audio from the iPod library as well as resource files bundled in your app. You understand the importance of configuring your audio session, andintelligently handing interruptions and audio route changes. “Playing nice” with other audio sources creates the kind of experience that users enjoy, and will want to use again and 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