Chapter 4. Sound

Sound is a frequently overlooked part of games. Even in big-name titles, sound design and programming frequently is left until late in the game development process. This is especially true on mobile devices—the user might be playing the game in a crowded, noisy environment and might not even hear the sounds and music you’ve put into it, so why bother putting in much effort?

However, sound is an incredibly important part of games. When a game sounds great, and makes noises in response to the visible parts of the game, the player gets drawn in to the world that the game’s creating.

In this chapter, you’ll learn how to use iOS’s built-in support for playing both sound effects and music. You’ll also learn how to take advantage of the speech synthesis features, which are new in iOS 7.

Sound good?[1]

Playing Sound with AVAudioPlayer

Problem

You want to play back an audio file, as simply as possible and with a minimum of work.

Solution

The simplest way to play a sound file is using AVAudioPlayer, which is a class available in the AVFoundation framework. To use this feature, you first need to add the AVFoundation framework to your project. Select the project at the top of the Project Navigator and scroll down to “Linked Frameworks and Libraries.” Click the + button and double-click on “AVFoundation.framework.”

You’ll also need to import the AVFoundation/AVFoundation.h file in each file that uses the AVFoundation files:

#import <AVFoundation/AVFoundation.h>

You create an AVAudioPlayer by providing it with the location of the file you want it to play. This should generally be done ahead of time, before the sound needs to be played, in order to avoid playback delays.

In this example, audioPlayer is an AVAudioPlayer instance variable:

NSURL* soundFileURL = [[NSBundle mainBundle] URLForResource:@"TestSound"
                       withExtension:@"wav"];
NSError* error = nil;

audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL
               error:&error];

if (error != nil) {
    NSLog(@"Failed to load the sound: %@", [error localizedDescription]);
}

[audioPlayer prepareToPlay];

To begin playback, you use the play method:

[audioPlayer play];

To make playback loop, you change the audio player’s numberOfLoops property. To make an AVAudioPlayer play one time and then stop:

player.numberOfLoops = 0;

To make an AVAudioPlayer play twice and then stop:

player.numberOfLoops = 1;

To make an AVAudioPlayer play forever, until manually stopped:

player.numberOfLoops = -1;

By default, an AVAudioPlayer will play its sound one time only. After it’s finished playing, a second call to play will rewind it and play it again. By changing the numberOfLoops property, you can make an AVAudioPlayer play its file a single time, a fixed number of times, or continuously until it’s sent a pause or stop message.

To stop playback, you use the pause or stop methods (the pause method just stops playback, and lets you resume from where you left off later; the stop method stops playback completely, and unloads the sound from memory):

[audioPlayer pause];
[audioPlayer stop];

To rewind an audio player, you change the currentTime property. This property stores how far playback has progressed, measured in seconds. If you set it to zero, playback will jump back to the start:

audioPlayer.currentTime = 0;

You can also set this property to other values, in order to jump to a specific point in the audio.

Discussion

If you use an AVAudioPlayer, you need to keep a strong reference to it (using an instance variable) to avoid it being released from memory. If that happens, the sound will stop.

If you have multiple sounds that you want to play at the same time, you need to keep references to each (or use an NSArray to contain them all). This can get cumbersome, so it’s often better to use a dedicated sound engine instead of managing each player yourself.

Preparing an AVAudioPlayer takes a little bit of preparation. You need to either know the location of a file that contains the audio you want the player to play, or have an NSData object that contains the audio data.

AVAudioPlayer supports a number of popular audio formats. The specific formats vary slightly from device to device; the iPhone 5 supports the following formats:

  • AAC (8 to 320 Kbps)
  • Protected AAC (from the iTunes Store)
  • HE-AAC
  • MP3 (8 to 320 Kbps)
  • MP3 VBR
  • Audible (formats 2, 3, 4, Audible Enhanced Audio, AAX, and AAX+)
  • Apple Lossless
  • AIFF
  • WAV

You shouldn’t generally have problems with file compatibility across devices, but it’s generally best to go with AAC, MP3, AIFF, or WAV.

In this example, it’s assumed that there’s a file called TestSound.wav in the project. You’ll want to use a different name for your game, of course.

Use the NSBundle’s URLForResource:withExtension method to get the location of a resource on disk:

NSURL* soundFileURL = [[NSBundle mainBundle] URLForResource:@"TestSound"
                       withExtension:@"wav"];

This returns an NSURL object that contains the location of the file, which you can give to your AVAudioPlayer to tell it where to find the sound file.

The initializer method for AVAudioPlayer is initWithContentsOfURL:error:. The first parameter is an NSURL that indicates where to find a sound file, and the second is a pointer to an NSError reference that allows the method to return an error object if something goes wrong.

Let’s take a closer look at how this works. First, you create an NSError variable and set it to nil:

NSError* error = nil;

Then you call initWithContentsOfURL:error: and provide the NSURL and a pointer to the NSError variable:

audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL
               error:&error];

When this method returns, audioPlayer is either a ready-to-use AVAudioPlayer object, or nil. If it’s nil, then the NSError variable will have changed to be a reference to an NSError object, which you can use to find out what went wrong:

if (error != nil) {
    NSLog(@"Failed to load the sound: %@", [error localizedDescription]);
}

Finally, the AVAudioPlayer can be told to preload the audio file before playback. If you don’t do this, it’s no big deal—when you tell it to play, it loads the file and then begins playing back. However, for large files, this can lead to a short pause before audio actually starts playing, so it’s often best to preload the sound as soon as you can. Note, however, that if you have many large sounds, preloading everything can lead to all of your available memory being consumed, so use this feature with care.

Recording Sound with AVAudioRecorder

Problem

You want to record sound made by the player, using the built-in microphone.

Solution

AVAudioRecorder is your friend here. Like its sibling AVAudioPlayer (see Playing Sound with AVAudioPlayer), AVAudioRecorder lives in the AVFoundation framework, so you’ll need to add that framework to your project and import the AVFoundation/AVFoundation.h header in any files where you want to use it. You can then create an AVAudioRecorder as follows:

NSURL* documentsURL = [[[NSFileManager defaultManager]
                       URLsForDirectory:NSDocumentDirectory
                       inDomains:NSUserDomainMask] lastObject];

NSURL* destinationURL = [documentsURL
    URLByAppendingPathComponent:@"RecordedSound.wav"];

NSURL* destinationURL = [self audioRecordingURL];
    NSError* error;

audioRecorder = [[AVAudioRecorder alloc] initWithURL:destinationURL
                 settings:nil error:&error];

if (error != nil) {
    NSLog(@"Couldn't create a recorder: %@", [error localizedDescription]);
}

[audioRecorder prepareToRecord];

To begin recording, use the record method:

[audioRecorder record];

To stop recording, use the stop method:

[audioRecorder stop];

When recording has ended, the file pointed at by the URL you used to create the AVAudioRecorder contains a sound file, which you can play using AVAudioPlayer or any other audio system.

Discussion

Like an AVAudioPlayer, an AVAudioRecorder needs to have at least one strong reference made to it in order to keep it in memory.

In order to record audio, you first need to have the location of the file where the recorded audio will end up. The AVAudioRecorder will create the file if it doesn’t already exist; if it does, the recorder will erase the file and overwrite it. So, if you want to avoid losing recorded audio, either never record to the same place twice, or move the recorded audio somewhere else when you’re done recording.

The recorded audio file needs to be stored in a location where your game is allowed to put files. A good place to use is your game’s Documents directory; any files placed in this folder will be backed up when the user’s device is synced.

To get the location of your game’s Documents folder, you can use the NSFileManager class:

NSURL* documentsURL = [[[NSFileManager defaultManager]
                       URLsForDirectory:NSDocumentDirectory
                       inDomains:NSUserDomainMask] lastObject];

Once you have the location of the directory, you can create a URL relative to it. Remember, the URL doesn’t have to point to a real file yet; one will be created when recording begins:

NSURL* destinationURL = [documentsURL
    URLByAppendingPathComponent:@"RecordedSound.wav"];

Working with Multiple Audio Players

Problem

You want to use multiple audio players, but reuse players when possible.

Solution

Create a manager object that manages a collection of AVAudioPlayers. When you want to play a sound, you ask this object to give you an AVAudioPlayer. The manager object will try to give you an AVAudioPlayer that’s not currently doing anything, but if it can’t find one, it will create one.

To create your manager object, create a file called AVAudioPlayerPool.h with the following contents:

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface AVAudioPlayerPool : NSObject

+ (AVAudioPlayer*) playerWithURL:(NSURL*)url;

@end

and a file called AVAudioPlayerPool.m with these contents:

#import "AVAudioPlayerPool.h"

NSMutableArray* _players = nil;

@implementation AVAudioPlayerPool

+ (NSMutableArray*) players {
    if (_players == nil)
        _players = [[NSMutableArray alloc] init];

    return _players;
}

+ (AVAudioPlayer *)playerWithURL:(NSURL *)url {
    NSMutableArray* availablePlayers = [[self players] mutableCopy];

    // Try and find a player that can be reused and is not playing
    [availablePlayers filterUsingPredicate:[NSPredicate
        predicateWithBlock:^BOOL(AVAudioPlayer* evaluatedObject,
        NSDictionary *bindings) {
            return evaluatedObject.playing == NO && [evaluatedObject.url
                                                     isEqual:url];
    }]];

    // If we found one, return it
    if (availablePlayers.count > 0) {
        return [availablePlayers firstObject];
    }

    // Didn't find one? Create a new one
    NSError* error = nil;
    AVAudioPlayer* newPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url
                                error:&error];

    if (newPlayer == nil) {
        NSLog(@"Couldn't load %@: %@", url, error);
        return nil;
    }

    [[self players] addObject:newPlayer];

    return newPlayer;

}

@end

You can then use it as follows:

NSURL* url = [[NSBundle mainBundle] URLForResource:@"TestSound"
              withExtension:@"wav"];
AVAudioPlayer* player = [AVAudioPlayerPool playerWithURL:url];
[player play];

Discussion

AVAudioPlayers are allowed to be played multiple times, but aren’t allowed to change the file that they’re playing. This means that if you want to reuse a single player, you have to use the same file; if you want to use a different file, you’ll need a new player.

This means that the AVAudioPlayerPool object shown in this recipe needs to know which file you want to play.

Our AVAudioPlayerPool object does the following things:

  1. It keeps a list of AVAudioPlayer objects in an NSMutableArray.
  2. When a player is requested, it checks to see if it has an available player with the right URL; if it does, it returns that.
  3. If there’s no AVAudioPlayer that it can use—either because all of the suitable AVAudioPlayers are playing, or because there’s no AVAudioPlayer with the right URL—then it creates one, prepares it with the URL provided, and adds it to the list of AVAudioPlayers. This means that when this new AVAudioPlayer is done playing, it will be able to be reused.

Cross-Fading Between Tracks

Problem

You want to blend multiple sounds by smoothly fading one out and another in.

Solution

This method slowly fades an AVAudioPlayer from a starting volume to an end volume, over a set duration:

- (void) fadePlayer:(AVAudioPlayer*)player fromVolume:(float)startVolume
    toVolume:(float)endVolume overTime:(float)time {

        // Update the volume every 1/100 of a second
        float fadeSteps = time * 100.0;

        self.audioPlayer.volume = startVolume;

        for (int step = 0; step < fadeSteps; step++) {
            double delayInSeconds = step * (time / fadeSteps);

            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
                (int64_t)(delayInSeconds * NSEC_PER_SEC));
            dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

                float fraction = ((float)step / fadeSteps);

                self.audioPlayer.volume = startVolume + (endVolume - startVolume)
                                          * fraction;

            });
    }
}

To use this method to fade in an AVAudioPlayer, use a startVolume of 0.0 and an endVolume of 1.0:

[self fadePlayer:self.audioPlayer fromVolume:0.0 toVolume:1.0 overTime:1.0];

To fade out, use a startVolume of 1.0 and an endVolume of 0.0:

[self fadePlayer:self.audioPlayer fromVolume:1.0 toVolume:0.0 overTime:1.0];

To make the fade take longer, increase the overTime parameter.

Discussion

When you want the volume of an AVAudioPlayer to slowly fade out, what you really want is for the volume to change very slightly but very often. In this recipe, we’ve created a method that uses Grand Central Dispatch to schedule the repeated, gradual adjustment of the volume of a player over time.

To determine how many individual volume changes are needed, the first step is to decide how many times per second the volume should change. In this example, we’ve chosen 100 times per second—that is, the volume will be changed 100 times for every second the fade should last:

float fadeSteps = time * 100.0;

Note

Feel free to experiment with this number. Bigger numbers will lead to smoother fades, while smaller numbers will be more efficient but might sound worse.

The next step is to ensure that the player’s current volume is set to be the start volume:

self.audioPlayer.volume = startVolume;

We then repeatedly schedule volume changes. We’re actually scheduling these changes all at once; however, each change is scheduled to take place slightly after the previous one.

To know exactly when a change should take place, all we need to know is how many steps into the fade we are, and how long the total fade should take. From there, we can calculate how far in the future a specific step should take place:

for (int step = 0; step < fadeSteps; step++) {
    double delayInSeconds = step * (time / fadeSteps);

Once this duration is known, we can get Grand Central Dispatch to schedule it:

    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
        (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

The next few lines of code are executed when the step is ready to happen. At this point, we need to know exactly what the volume of the audio player should be:

        float fraction = ((float)step / fadeSteps);
        self.audioPlayer.volume = startVolume + (endVolume - startVolume) *
                                  fraction;

When the code runs, the for loop creates and schedules multiple blocks that set the volume, with each block reducing the volume a little. The end result is that the user hears a gradual lessening in volume—in other words, a fade out!

Synthesizing Speech

Problem

You want to make your app speak.

Solution

First, add AVFoundation.framework to your project (see Playing Sound with AVAudioPlayer).

Then, create an instance of AVSpeechSynthesizer:

self.speechSynthesizer = [[AVSpeechSynthesizer alloc] init];

When you have text you want to speak, create an AVSpeechUtterance:

AVSpeechUtterance* utterance = [AVSpeechUtterance
    speechUtteranceWithString:@"Hello, I'm an iPhone!"];

You then give the utterance to your AVSpeechSynthesizer:

[self.speechSynthesizer speakUtterance:utterance];

Discussion

The voices you use with AVSpeechSynthesizer are the same ones seen in the Siri personal assistant that’s built in to all devices released since the iPhone 4S, and in the VoiceOver accessibility feature.

You can send more than one AVSpeechUtterance to an AVSpeechSynthesizer at the same time. If you call speakUtterance: while the synthesizer is already speaking, it will wait until the current utterance has finished before moving on to the next.

Warning

Don’t call speakUtterance: with the same AVSpeechUtterance twice—you’ll cause an exception.

Once you start speaking, you can instruct the AVSpeechSynthesizer to pause speaking, either immediately or at the next word:

// Stop speaking immediately
[self.speechSynthesizer pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate];

// Stop speaking after the current word
[self.speechSynthesizer pauseSpeakingAtBoundary:AVSpeechBoundaryWord];

Once you’ve paused speaking, you can resume it at any time:

[self continueSpeaking];

If you’re done with speaking, you can clear the AVSpeechSynthesizer of the current and pending AVSpeechUtterances by calling stopSpeakingAtBoundary:. This method works in the same way as pauseSpeakingAtBoundary:, but once you call it, anything the synthesizer was about to say is forgotten.

Getting Information About What the iPod Is Playing

Problem

You want to find out information about whatever song the Music application is playing.

Solution

To do this, you’ll need to add the Media Player framework to your project. Do this selecting the project at the top of the Project Navigator, scrolling down to “Linked Frameworks and Libraries,” clicking the + button, and double-clicking on “MediaPlayer.framework.”

First, get an MPMusicPlayerController from the system, which contains information about the built-in iPod. Next, get the currently playing MPMediaItem, which represents a piece of media that the iPod is currently playing. Finally, call valueForProperty: to get specific information about that media item:

MPMusicPlayerController* musicPlayer = [MPMusicPlayerController iPodMusicPlayer];

MPMediaItem* currentTrack = musicPlayer.nowPlayingItem;

NSString* title = [currentTrack valueForProperty:MPMediaItemPropertyTitle];
NSString* artist = [currentTrack valueForProperty:MPMediaItemPropertyArtist];
NSString* album = [currentTrack valueForProperty:MPMediaItemPropertyAlbumTitle];

Once you’ve got this information, you can do whatever you like with it, including displaying it in a label, showing it in-game, and more.

Discussion

An MPMusicPlayerController represents the music playback system that’s built in to every iOS device. Using this object, you can get information about the currently playing track, set the currently playing queue of music, and control the playback (such as by pausing and skipping backward and forward in the queue).

There are actually two MPMusicPlayerControllers available to your app. The first is the iPod music player, which represents the state of the built-in Music application. The iPod music player is shared across all applications, so they all have control over the same thing.

The second music player controller that’s available is the application music player. The application music player is functionally identical to the iPod music player, with a single difference: each application has its own application music player. This means that they each have their own playlist.

Only one piece of media can be playing at a single time. If an application starts using its own application music player, the iPod music player will pause and let the application take over. If you’re using an app that’s playing music out of the application music player, and you then exit that app, the music will stop.

To get information about the currently playing track, you use the nowPlayingItem property of the MPMusicPlayerController. This property returns an MPMediaItem, which is an object that represents a piece of media. Media means music, videos, audiobooks, podcasts, and more—not just music!

To get information about an MPMediaItem, you use the valueForProperty: method. This method takes one of several possible property names. Here are some examples:

MPMediaItemPropertyAlbumTitle
The name of the album
MPMediaItemPropertyArtist
The name of the artist
MPMediaItemPropertyAlbumArtist
The name of the album’s main artist (for albums with multiple artists)
MPMediaItemPropertyGenre
The genre of the music
MPMediaItemPropertyComposer
The composer of the music
MPMediaItemPropertyPlaybackDuration
The length of the music, in seconds

Warning

The media library is only available on iOS devices—it’s not available on the iOS simulator. If you try to use these features on the simulator, it just plain won’t work.

Detecting When the Currently Playing Track Changes

Problem

You want to detect when the currently playing media item changes.

Solution

First, use NSNotificationCenter to subscribe to the MPMusicPlayerControllerNowPlayingItemDidChangeNotification notification:

[[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(nowPlayingChanged:)
    name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification
    object:nil];

Next, get a reference to the MPMusicPlayerController that you want to get notifications for, and call beginGeneratingPlaybackNotifications on it:

MPMusicPlayerController* musicPlayer = [MPMusicPlayerController iPodMusicPlayer];

[musicPlayer beginGeneratingPlaybackNotifications];

Finally, implement the method that receives the notifications. In this example, we’ve called it nowPlayingChanged:, but you can call it anything you like:

- (void) nowPlayingChanged:(NSNotification*)notification {
    // Now playing item changed, do something about it
}

Discussion

Notifications regarding the current item won’t be sent unless beginGeneratingPlaybackNotifications is called. If you stop being interested in the currently playing item, call endGeneratingPlaybackNotifications.

Note that you might not receive these notifications if your application is in the background. It’s generally a good idea to manually update your interface whenever your game comes back from the background, instead of just relying on the notifications to arrive.

Controlling iPod Playback

Problem

You want to control the track that the Music application is playing.

Solution

Use the MPMusicPlayerController to control the state of the music player:

MPMusicPlayerController* musicPlayer = [MPMusicPlayerController iPodMusicPlayer];

// Start playing
[musicPlayer play];

// Stop playing, but remember the current playback position
[musicPlayer pause];

// Stop playing
[musicPlayer stop];

// Go back to the start of the current item
[musicPlayer skipToBeginning];

// Go to the start of the next item
[musicPlayer skipToNextItem];

// Go to the start of the previous item
[musicPlayer skipToPreviousItem];

// Start playing in fast-forward (use "play" to resume playback)
[musicPlayer beginSeekingForward];

// Start playing in fast-reverse.
[musicPlayer beginSeekingBackward];

Discussion

Don’t forget that if you’re using the shared iPod music player controller, any changes you make to the playback state apply to all applications. This means that the playback state of your application might get changed by other applications—usually the Music application, but possibly by other apps.

You can query the current state of the music player by asking it for the playbackState, which is one of the following values:

MPMusicPlaybackStateStopped
The music player isn’t playing anything.
MPMusicPlaybackStatePlaying
The music player is currently playing.
MPMusicPlaybackStatePaused
The music player is playing, but is paused.
MPMusicPlaybackStateInterrupted
The music player is playing, but has been interrupted (e.g., by a phone call).
MPMusicPlaybackStateSeekingForward
The music player is fast-forwarding.
MPMusicPlaybackStateSeekingBackward
The music player is fast-reversing.

You can get notified about changes in the playback state by registering for the MPMusicPlayerControllerPlaybackStateDidChangeNotification notification, in the same way MPMusicPlayerControllerNowPlayingItemDidChangeNotification allows you to get notified about changes in the currently playing item.

Allowing the User to Select Music

Problem

You want to allow the user to choose some music to play.

Solution

You can display an MPMediaPickerController to let the user select music.

First, make your view controller conform to the MPMediaPickerControllerDelegate:

@interface ViewController () <MPMediaPickerControllerDelegate>

Next, add the following code at the point where you want to display the media picker:

MPMediaPickerController* picker = [[MPMediaPickerController alloc]
                                   initWithMediaTypes:MPMediaTypeAnyAudio];

picker.allowsPickingMultipleItems = YES;
picker.showsCloudItems = NO;

picker.delegate = self;

[self presentViewController:picker animated:NO completion:nil];

Then, add the following two methods to your view controller:

- (void)mediaPicker:(MPMediaPickerController *)mediaPicker
    didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection {

        for (MPMediaItem* item in mediaItemCollection.items) {
            NSString* itemName = [item valueForProperty:MPMediaItemPropertyTitle];
            NSLog(@"Picked item: %@", itemName);
        }

        MPMusicPlayerController* musicPlayer = [MPMusicPlayerController
            iPodMusicPlayer];

        [musicPlayer setQueueWithItemCollection:mediaItemCollection];

        [musicPlayer play];

        [self dismissViewControllerAnimated:NO completion:nil];
}

- (void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker {
    [self dismissViewControllerAnimated:NO completion:nil];
}

Discussion

An MPMediaPickerController uses the exact same user interface as the one you see in the built-in Music application. This means that your player doesn’t have to waste time learning how to navigate a different interface.

When you create an MPMediaPickerController, you can choose what kinds of media you want the user to pick. In this recipe, we’ve gone with MPMediaTypeAnyAudio, which, as the name suggests, means the user can pick any audio: music, audiobooks, podcasts, and so on. Other options include:

  • MPMediaTypeMusic
  • MPMediaTypePodcast
  • MPMediaTypeAudioBook
  • MPMediaTypeAudioITunesU
  • MPMediaTypeMovie
  • MPMediaTypeTVShow
  • MPMediaTypeVideoPodcast
  • MPMediaTypeMusicVideo
  • MPMediaTypeVideoITunesU
  • MPMediaTypeHomeVideo
  • MPMediaTypeAnyVideo
  • MPMediaTypeAny

In addition to setting what kind of content you want the user to pick, you can also set whether you want the user to be able to pick multiple items or just one:

picker.allowsPickingMultipleItems = YES;

Finally, you can decide whether you want to present media that the user has purchased from iTunes, but isn’t currently downloaded onto the device. Apple refers to this feature as “iTunes in the Cloud,” and you can turn it on or off through the showsCloudItems property:

picker.showsCloudItems = NO;

When the user finishes picking media, the delegate of the MPMediaPickerController receives the mediaPicker:didPickMediaItems: message. The media items that were chosen are contained in an MPMediaItemCollection object, which is basically an array of MPMediaItems.

In addition to getting information about the media items that were selected, you can also give the MPMediaItemCollection directly to an MPMusicPlayerController, and tell it to start playing:

MPMusicPlayerController* musicPlayer = [MPMusicPlayerController iPodMusicPlayer];

[musicPlayer setQueueWithItemCollection:mediaItemCollection];

[musicPlayer play];

Once you’re done getting content out of the media picker, you need to dismiss it, by using the dismissViewControllerAnimated:completion: method. This also applies if the user taps the Cancel button in the media picker: in this case, your delegate receives the mediaPickerDidCancel: message, and your application should dismiss the view controller in the same way.

Cooperating with Other Applications’ Audio

Problem

You want to play background music only when the user isn’t already listening to something.

Solution

You can find out if another application is currently playing audio by using the AVAudioSession class:

AVAudioSession* session = [AVAudioSession sharedInstance];

if (session.otherAudioPlaying) {
    // Another application is playing audio. Don't play any sound that might
    // conflict with music, such as your own background music.
} else {
    // No other app is playing audio - crank the tunes!
}

Discussion

The AVAudioSession class lets you control how audio is currently being handled on the device, and gives you considerable flexibility in terms of how the device should handle things like the ringer switch (the switch on the side of the device) and what happens when the user locks the screen.

By default, if you begin playing back audio using AVAudioPlayer and another application (such as the built-in Music app) is playing audio, the other application will stop all sound, and the audio played by your game will be the only thing audible.

However, you might want the player to be able to listen to her own music while playing your game—the background music might not be a very important part of your game, for example.

To change the default behavior of muting other applications, you need to set the audio session’s category. For example, to indicate to the system that your application should not cause other apps to mute their audio, you need to set the audio session’s category to AVAudioSessionCategoryAmbient:

NSError* error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient
 error:&error];

if (error != nil) {
    NSLog(@"Problem setting audio session: %@", error);
}

There are several categories of audio session available. The most important to games are the following:

AVAudioSessionCategoryAmbient
Audio isn’t the most important part of your game, and other apps should be able to play audio alongside yours. When the ringer switch is set to mute, your audio is silenced, and when the screen locks, your audio stops.
AVAudioSessionCategorySoloAmbient
Audio is reasonably important to your game. If other apps are playing audio, they’ll stop. However, the audio session will continue to respect the ringer switch and the screen locking.
AVAudioSessionCategoryPlayback
Audio is very important to your game. Other apps are silenced, and your app ignores the ringer switch and the screen locking.

Note

When using AVAudioSessionCategoryPlayback, your app will still be stopped when the screen locks. To make it keep running, you need to mark your app as one that plays audio in the background. To do this, follow these steps:

  1. Open your project’s information page, by clicking on the project at the top of the Project Navigator.
  2. Go to the Capabilities tab.
  3. Turn on “Background Modes,” and then turn on “Audio and AirPlay.”

Your app will now play audio in the background, as long as the audio session’s category is set to AVAudioSessionCategoryPlayback.

Determining How to Best Use Sound in Your Game Design

Problem

You want to make optimal use of sound and music in your game design.

Solution

It’s really hard to make an iOS game that relies on sound. For one, you can’t count on the user wearing headphones, and sounds in games (and everything else, really) don’t sound their best coming from the tiny speakers found in iOS devices.

Many games “get around” this by prompting users to put on their headphones as the game launches, or suggesting that they are “best experienced via headphones” in the sound and music options menu, if it has one. We think this is a suboptimal solution.

The best iOS games understand and acknowledge the environment in which the games are likely to be played: typically a busy, distraction-filled environment, where your beautiful audio might not be appreciated due to background noise or the fact that the user has the volume turned all the way down.

The solution is to make sure your game works with, or without, sound. Don’t count on the fact the user can hear anything at all, in fact.

Discussion

Unless you’re building a game that is based around music or sound, you should make it completely playable without sound. Your users will thank you for it, even if they never actually thank you for it!



[1] We apologize for the pun and have fired Jon, who wrote it.

Get iOS Game Development Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.