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
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
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
Solution
Create a manager object that manages a collection of AVAudioPlayer
s. 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
AVAudioPlayer
s 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:
-
It keeps a list of
AVAudioPlayer
objects in anNSMutableArray
. - 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.
-
If there’s no
AVAudioPlayer
that it can use—either because all of the suitableAVAudioPlayer
s are playing, or because there’s noAVAudioPlayer
with the right URL—then it creates one, prepares it with the URL provided, and adds it to the list ofAVAudioPlayer
s. This means that when this newAVAudioPlayer
is done playing, it will be able to be reused.
Cross-Fading Between Tracks
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
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 AVSpeechUtterance
s 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
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 MPMusicPlayerController
s 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
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
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
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 MPMediaItem
s.
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
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:
- Open your project’s information page, by clicking on the project at the top of the Project Navigator.
- Go to the Capabilities tab.
- 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
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.
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.