Chapter 12. Streaming: Thumping, Pulse-Quickening Game Excitement

This is the longest chapter of the audio section, and may actually be the longest chapter of the entire book. But don't feel too discouraged, because the length is not related to the difficulty. We are going to cover multiple APIs, comparing their functionality to give you a better understanding of how they work. The goal is to help you select the API that accomplishes the most in the least amount of effort for your own game.

First, we will look at how you might use large audio files in your games. Then we will cover the Media Player framework, which allows you to access the iPod music library from within your application. We will walk through an example that lets the user construct a playlist from the iPod music library to be used as background music for Space Rocks!.

Next, we will move on to the more generic audio streaming APIs: AVFoundation, OpenAL, and Audio Queue Services. Using each of these APIs, we will repeat implementing background music for Space Rocks!. And in the OpenAL example, we will do sort of a grand finale and embellish the game by adding streaming speech and fully integrating it with the OpenAL capabilities we implemented in the previous two chapters.

Then we will turn our attention to audio capture. Once again, we will compare and contrast three APIs: Audio Queue Services, AVFoundation, and OpenAL.

Finally, we bring things full circle and close with some OpenGL and OpenAL optimization tips.

Music and Beyond

In the previous chapters, we focused on playing short sound effects. But what about longer things like music? Most games have background music, right?

For performance, we have been preloading all the audio files into RAM. But this isn't feasible for large audio files. A 44 kHz, 60-second stereo sample takes about 10MB of memory to hold (as linear PCM). For those wondering how to do the math:

Music and Beyond

Now, 10MB is a lot of RAM to use, particularly when we are listening to only a small piece of the 1-minute audio sample at any given moment. And on a RAM-limited device such as an iPhone, a more memory-efficient approach is critical if you hope to do anything else in your game, such as graphics.

In this chapter, we will look at APIs to help us deal with large amounts of audio data. Since the device we are talking about originates from a portable music player, it is only reasonable that iPhone OS provides special APIs to allow you to access the iPod music library. We will also look at the more general audio APIs that allow us to play our own bundled music. These general audio APIs are often referred to as buffer-queuing APIs or streaming APIs, because you continuously feed small chunks of data to the audio system. But I don't want you to get myopia for just music—these audio APIs are general purpose, and can handle other types of audio.

You might be thinking that you don't need anything beyond music in your game. However, there are other types of long-duration sounds that you may require. Speech is the most notable element. Speech can find many ways into games. Adventure games with large casts of characters with dialogue, such as the cult classic Star Control 2: The Ur-Quan Masters and the recently rereleased The Secret of Monkey Island (see Figures 12-1 and 12-2) are prime examples of games that make heavy use of both simultaneous music and speech throughout the entire game.

The Secret of Monkey Island is a prime example of a game that has both music and speech. It's now available for iPhone OS!

Figure 12.1. The Secret of Monkey Island is a prime example of a game that has both music and speech. It's now available for iPhone OS!

In-game screenshot of The Secret of Monkey Island. Voice actors provide speech for the dialogue to enhance the quality of the game.

Figure 12.2. In-game screenshot of The Secret of Monkey Island. Voice actors provide speech for the dialogue to enhance the quality of the game.

It is also common for games to have an opening sequence that provides a narrator. And even action-oriented games need speech occasionally. Half-Life opened with an automated train tour guide talking to you as you descended into the compound, and you meet somewhat chatty scientists along the way. And for many people, the most memorable things from Duke Nukem 3D are the corny lines Duke says throughout the game.

Quick, arcade-style games may use streaming sound—perhaps for an opening or to tell you "game over"—because the sound is too infrequently used to be worth keeping resident. I recall seeing a port of Street Fighter 1 on the semi-obscure video game console TurboGrafx-16 with the CD-ROM drive add-on. To get the "Round 1 fight" and "<Character> wins" speeches, the console accessed the CD-ROM. The load times were hideously slow on this machine, so the game would pause multiple seconds to pull this dialogue. But the console had very limited memory, so the game developers decided not to preload these files. Whether they actually streamed them, I cannot say definitively, but they could have.

Games built around immersive environments that might not have a dedicated soundtrack still might need streaming. Imagine entering a room like a bar that has a TV, a radio, and a jukebox within earshot. You will be hearing all these simultaneously, and they are probably not short looping samples if they are going to be interesting.

You may remember my anecdote about SDL_mixer from Chapter 10, and how it ultimately led me to try OpenAL. My problem with SDL_mixer was it got tunnel vision on music. SDL_mixer made a strong distinction between sound effects (the short-duration sounds completely preloaded for performance) and music. SDL_mixer had a distinct API for dealing with music that was separate from sound effects. This in itself was not necessarily bad, but SDL_mixer made the assumption that you would only ever need one "music" channel. The problem I ran into was that I needed both speech and music. But speech was too long to be a "sound effect." Furthermore, at the time, SDL_mixer didn't support highly compressed audio such as MP3 for the nonmusic channel, which was one of the odd things about having a distinct API for music and sound effects. So SDL_mixer was pretty much a dead-end for my usage case without major hacking. This event scarred me for life, so I encourage you not to box yourself in by thinking too small.

And on that note, there is one final thing we will briefly address in this chapter that is related to streaming: audio input, also known as audio capture or audio recording. Audio capture also deals with continuous small chunks of data, but the difference is that it is coming from the audio system, rather than having you submit it to the audio system.

iPod Music Library (Media Player Framework)

iPhone OS 3.0 was the first version to let you access to the iPod music library programmatically via the Media Player framework. You can access songs, audiobooks, and podcasts from the user's iPod music library and play them in your application. This is one step beyond just setting the audio session to allow the user to mix in music from the iPod application (see Figure 12-3). This framework gives you ultimate authority over which songs are played. You can also present a media item picker so the user may change songs without quitting your application (and returning to the iPod application).

The built-in iPod application in Cover Flow mode. Prior to iPhone OS 3.0, allowing your application to mix audio with the playing iPod application was the only way to let users play their own music in your application, albeit manually. Now with the Media Player framework, this can be accomplished more directly.

Figure 12.3. The built-in iPod application in Cover Flow mode. Prior to iPhone OS 3.0, allowing your application to mix audio with the playing iPod application was the only way to let users play their own music in your application, albeit manually. Now with the Media Player framework, this can be accomplished more directly.

Note

The Media Player framework does not allow you to get direct access to the raw files or to the PCM audio data being played. There is also limited access to metadata. That means certain applications are not currently possible. Something as simple as a music visualizer really isn't possible because you can't analyze the PCM audio as it plays. And any game that hopes to analyze the music data, such as a beat/rhythm type of game, will not be able to access this data.

We will go through a short example. This example will probably be quite different from what you'll find in other books and documentation. Basic playback is quite simple, so there generally is an emphasis on building rich UIs to accompany your application. These include notifications to know when a song has changed (so you can update status text), how to search through collections to find songs that meet special criteria, and how to retrieve album artwork and display it in your app. But this is a game book, so we are going to take another approach.

Here, we are going to access the iPod library, present a media item picker, and play. The unique aspect is that we will use our existing Space Rocks! code and mix that in with our existing OpenAL audio that we used for sound effects.

First, you should make sure you have at least one song installed in your device's iPod library. (Production code should consider what to do in the case of an empty iPod library.) Also note that the iPhone simulator is currently not supported, so you must run this on an actual device.

Second, I want to remind you that we have been setting our audio session to kAudioSessionCategory_AmbientSound in the Space Rocks! code thus far. This allows mixing your application's audio with other applications. More specifically, this allows you to hear both your OpenAL sound effects and the iPod. (If you haven't already tried it, you might take a moment to go to the iPod application and start playing a song. Then start up Space Rocks! and hear the mixed sound for yourself.) When using the Media Player framework, you must continue to use the AmbientSound mode if you want to hear both your OpenAL sound effects and the Media Player framework audio.

Finally, we need to make a decision about how we want our media player to behave, as Apple gives us two options. Apple provides an application music player and an iPod music player. The iPod music player ties in directly to the built-in iPod music player application and shares the same state (e.g., shuffle and repeat modes). When you quit your application using this player, music still playing will continue to play. In contrast, the application music player gets its own state, and music will terminate when you quit your application. For this example, we will use the iPod music player, mostly because I find its seamless behavior with the built-in iPod player to be distinctive from the other streaming APIs we will be looking at later. Those who wish to use the application music player shouldn't fret about missing out. The programming interface is the same.

Playing iPod Music in Space Rocks!

We will continue building on Space Rocks! from the previous chapter, specifically, the project SpaceRocksOpenAL3D_6_SourceRelative. The completed project for this example is SpaceRocksMediaPlayerFramework.

To get started, we will create a new class named IPodMusicController to encapsulate the Media Player code. We also need to add the Media Player framework to the project.

Our IPodMusicController will be a singleton. It will encapsulate several convenience methods and will conform to the Apple's MPMediaPickerControllerDelegate so we can respond to the MPMediaPicker's delegate callback methods.

#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>

@interface IPodMusicController : NSObject <MPMediaPickerControllerDelegate>
{
}

+ (IPodMusicController*) sharedMusicController;
- (void) startApplication;
- (void) presentMediaPicker:(UIViewController*)current_view_controller;

@end

The startApplication method will define what we want to do when our application starts. So what do we want to do? Let's keep it fairly simple. We will get an iPod music player and start playing music if it isn't already playing.

Apple's MPMusicPlayerController class represents the iPod music player. It also uses a singleton pattern, which you've seen multiple times in previous chapters. For brevity, the implementation of the singleton accessor method is omitted here. See the finished example for the method named sharedMusicController.

Let's focus on the startApplication method.

- (void) startApplication
{
   MPMusicPlayerController* music_player = [MPMusicPlayerController iPodMusicPlayer];
   // Set or otherwise take iPod's current modes
   // [music_player setShuffleMode:MPMusicShuffleModeOff];
   // [music_player setRepeatMode:MPMusicRepeatModeNone];

   if(MPMusicPlaybackStateStopped == music_player.playbackState)
   {
      // Get all songs in the library and make them the list to play
      [music_player setQueueWithQuery:[MPMediaQuery songsQuery]];
      [music_player play];
   }
   else if(MPMusicPlaybackStatePaused == music_player.playbackState)
   {
      // Assuming that a song is already been selected to play
      [music_player play];
   }
   else if(MPMusicPlaybackStatePlaying == music_player.playbackState)
{
      // Do nothing, let it continue playing
   }
   else
   {
      NSLog(@"Unhandled MPMusicPlayerController state: %d", music_player.playbackState);
  }
}

As you can see in the startApplication method, to get the iPod music player, we simply do this:

MPMusicPlayerController* music_player = [MPMusicPlayerController iPodMusicPlayer];

If we wanted to get the application music player instead, we would invoke this method instead:

MPMusicPlayerController* music_player = [MPMusicPlayerController applicationMusicPlayer];

Optionally, we can set up some properties, such as the shuffle and repeat modes, like this:

[music_player setShuffleMode:MPMusicShuffleModeOff];
[music_player setRepeatMode:MPMusicRepeatModeNone];

We won't set them for this example, and instead rely on the iPod's current modes.

Next, we need to find some songs to play and start playing them. The MPMediaQuery class allows you to form queries for specific files. It also provides some convenient methods, which we will take advantage of to query for all songs contained in the library. The MPMusicPlayerController has a method called setQueueWithQuery, which will add the results of the query to the iPod's play queue. So by the end of it, we will construct a queue containing all of the songs in the library.

[music_player setQueueWithQuery:[MPMediaQuery songsQuery]];

Then to play, we just invoke play:

[music_player play];

But you can see from this method implementation that we get a little fancy. We have a large if-else block to detect whether the iPod is currently playing audio, using the MPMusicPlayerController's playbackState property. We can instruct the iPod to create a new queue only if it is not already playing audio. There are six different states: playing, paused, stopped, interrupted (e.g., phone call interruption), seeking forward, and seeking backward. For this example, we will concern ourselves with only the first three states.

Tip

You may encounter the paused state more frequently than you might initially expect. This is because the iPod application doesn't have a stop button; it has only a pause button. Users who were playing music on their iPod and "stopped" it in the middle of a song are likely to be paused. You will encounter the stopped state if the user has just rebooted the device or let the iPod finish playing a playlist to completion. I separate the case for demonstration purposes, but for real applications, you may consider lumping stopped and paused into the same case, as the users may not remember they were in the middle of a song.

Now we need to invoke this method from Space Rocks! We will return to BBSceneController.m and add the following line to its init method:

[[IPodMusicController sharedMusicController] startApplication];

(Don't forget to #import "IPodMusicController.h" at the top of the file.)

Now, when the game loads, you should hear music playing from your iPod. And you should notice that the OpenAL audio still works. Congratulations!

Note

MPMusicPlayerController has a volume property. However, this volume is the master volume control, so if you change the volume, it affects both the iPod music and the OpenAL audio in the same way. This may make volume balancing between the iPod and OpenAL very difficult—if not impossible—as you will have only fine-grained control over OpenAL gain levels, and not the iPod in isolation.

Adding a Media Item Picker

Now we will go one extra step and allow the user to build a list of songs from a picker (see Figures 12-4 and 12-5). Unfortunately, our Space Rocks! code really didn't intend us to do this kind of thing. So what you are about to see is kind of a hack. But I don't want to overwhelm you with a lot of support code to make this clean, as our focus is on the picker and music player.

Scrolling though the list of albums presented in the media item picker in the albums display mode showing my geeky but on-topic/game-related iPod music library.

Figure 12.4. Scrolling though the list of albums presented in the media item picker in the albums display mode showing my geeky but on-topic/game-related iPod music library.

Scrolling through the same media item picker in the songs display mode. Note the buttons on the right side of the table view list entries that allow you to add a song to the playlist you are constructing.

Figure 12.5. Scrolling through the same media item picker in the songs display mode. Note the buttons on the right side of the table view list entries that allow you to add a song to the playlist you are constructing.

As you might have noticed earlier, I listed a prototype in IPodMusicController for this method:

- (void) presentMediaPicker:(UIViewController*)current_view_controller;

We will write this method to encapsulate creating the picker and displaying it.

Apple provides a ready-to-use view controller subclass called MPMediaPickerController, which will allow you to pick items from your iPod library. Apple calls this the media item picker. It looks very much like the picker in the iPod application. One very notable difference, however, is that there is no Cover Flow mode with the media item picker.

Despite the shortcoming, we will use this class for our picker. Add the implementation for presentMediaPicker in IPodMusicController.m.

- (void) presentMediaPicker:(UIViewController*)current_view_controller
{
   MPMediaPickerController* media_picker = [[[MPMediaPickerController alloc]
    initWithMediaTypes:MPMediaTypeAnyAudio] autorelease];
   [media_picker setDelegate:self];
   [media_picker setAllowsPickingMultipleItems:YES];
   // For a message at the top of the view
   media_picker.prompt = NSLocalizedString(@"Add songs to play", "Prompt in media item
    picker");

        [current_view_controller presentModalViewController:media_picker animated:YES];
}

In the first line, we create a new MPMediaPickerController. It takes a mask parameter that allows you to restrict which media types you want to display in your picker. Valid values are MPMediaTypeMusic, MPMediaTypePodcast, MPMediaTypeAudioBook, and the convenience mask MPMediaTypeAnyAudio. We will use the latter, since the user may have been listening to a podcast or audiobook before starting Space Rocks!, and I don't see any reason to restrict it.

In the next line, we set the picker's delegate to self. Remember that earlier we made the IPodMusicController class conform to the MPMediaPickerControllerDelegate protocol. We will implement the delegate methods in this class shortly. Setting the delegate to self will ensure our delegate methods are invoked.

Next, we set an option on the picker to allow picking multiple items. This will allow the users to build a list of songs they want played, rather than just selecting a single song. This isn't that critical for our short game, but it is an option that you might want to use in your own games.

Then we set another option to show a text label at the very top of the picker. We will display the string "Add songs to play". We set the prompt property with this string. You may have noticed the use of NSLocalizedString. In principle, when generating text programmatically, you should always think about localization. Since this is a Cocoa-level API, we can use Cocoa's localization support functions. Since we are not actually localizing this application, this is a little overkill, but it's here as a reminder to you to think about localization.

Now we are ready to display the media item picker. Notice that we passed in a parameter called current_view_controller. This represents our current active view controller. We want to push our media item picker view controller onto our active view controller, so we send the presentModalViewController message to our active view controller, with the media picker view controller as the parameter.

Next, let's implement the two delegate methods. The first delegate method is invoked after the user selects songs and taps Done. The second delegate method is invoked if the user cancels picking, (e.g., taps Done without selecting any songs).

#pragma mark MPMediaPickerControllerDelegate methods

- (void) mediaPicker:(MPMediaPickerController*)media_picker
didPickMediaItems:(MPMediaItemCollection*)item_collection
{
   MPMusicPlayerController* music_player = [MPMusicPlayerController iPodMusicPlayer];
   [music_player setQueueWithItemCollection:item_collection];
   [music_player play];

   [media_picker.parentViewController dismissModalViewControllerAnimated:YES];
}

- (void) mediaPickerDidCancel:(MPMediaPickerController *)media_picker
{
   [media_picker.parentViewController dismissModalViewControllerAnimated:YES];
}

The code in the first method should seem familiar, as it is almost the same as our startApplication code. We get the iPodMusicController, set the queue, and call play. Since this delegate method provides us the item collection, we use the setQueueWithItemCollection method instead of the query version. Lastly, in both functions, we remove the media picker item from the view controller so we can get back to the game.

That's the media item picker in a nutshell. Now we need to design a way for the user to bring up the picker. As I said, this is a hack. Rather than spending time trying to work in some new button, we will exploit the accelerometer and use a shake motion to be the trigger for bringing up a picker. (This also has the benefit of being kind of cool.) We will embark on another little side quest to accomplish this. (You might think that a better thing to do is shuffle the songs on shake, which you might like to try on your own.)

Shake It! (Easy Accelerometer Shake Detection)

In Chapter 3, you learned how to use the accelerometer. But we are going to spice things up and do something a little different. In iPhone OS 3.0, Apple introduced new APIs to make detecting shakes much easier:

- (void) motionBegan:(UIEventSubtype)the_motion withEvent:(UIEvent*)the_event;
- (void) motionEnded:(UIEventSubtype)the_motion withEvent:(UIEvent*)the_event;
- (void) motionCancelled:(UIEventSubtype)the_motion withEvent:(UIEvent*)the_event;

These were added into the UIResponder class, so you no longer need to access the accelerometer directly and analyze the raw accelerometer data yourself for common motions like shaking. Here, we will just use motionBegan:withEvent:.

Since this is part of the UIResponder class, we need to find the best place to add this code. This happens to be BBInputViewController, where we also handle all our touch events.

We will add the method and implement it as follows:

- (void) motionBegan:(UIEventSubtype)the_motion withEvent:(UIEvent *)the_event
{
   if(UIEventSubtypeMotionShake == the_motion)
   {
      [[IPodMusicController sharedMusicController] presentMediaPicker:self];
   }
}

This is very straightforward. We just check to make sure the motion is a shake event. If it is, we invoke our presentMediaPicker: method, which we just implemented. We pass self as the parameter because our BBInputViewController instance is the current active view controller to which we want to attach the picker.

For this code to actually work though, we need to do some setup. We need to implement three more methods for this class to make our BBInputViewController the first responder. Otherwise, the motionBegan:withEvent: method we just implemented will never be invoked. To do this, we drop in the following code:

- (BOOL) canBecomeFirstResponder
{
   return YES;
}

- (void) viewDidAppear:(BOOL)is_animated
{
   [self becomeFirstResponder];
   [super viewDidAppear:is_animated];
}

- (void) viewWillDisappear:(BOOL)is_animated
{
   [self resignFirstResponder];
   [super viewWillDisappear:is_animated];
}

Now you are ready to try it. Start up Space Rocks! and give the device a shake to bring up the picker. Select some songs and tap Done. You should hear the music change to your new selection. Shake it again and pick some other songs. Fun, eh? (Yes, pausing the game when presenting the media item picker would be a great idea, but since we currently lack a game pause feature, I felt that would be one side quest too far.)

This concludes our Media Player framework example. We will now move to general streaming APIs.

Audio Streaming

Audio streaming is the term I use to describe dealing with large audio data. Ultimately, the idea is to break down the audio into small, manageable buffers. Then in the playback case, you submit each small buffer to the audio system to play when it is ready to receive more data. This avoids needing to load the entire thing into memory at the same time and exhausting your limited amount of RAM.

The type of streaming I'm describing here is not the same as network audio streaming, which is often associated with things like Internet radio. With network audio streaming, the emphasis is on the network part. The idea is that you are reading in packets of data over the network and playing it. In principle, the audio streaming I am describing is pretty much the same idea, but lower level and only about the audio system. It doesn't really care where the data came from—it could be from the network, from a file, captured from a microphone, or dynamically generated from an algorithm.

While the concept is simple, depending on the API you use, preparing the buffers for use and knowing when to submit more data can be tedious. We will discuss three native APIs you can use for streaming: AVFoundation, OpenAL, and Audio Queue Services. Each has its own advantages and disadvantages.

AVFoundation is the easiest to use but the most limited in capabilities. OpenAL is lower-level, but more flexible than AVFoundation and can work seamlessly with all the cool OpenAL features we've covered in the previous two chapters. Audio Queue Services is at about the same level of difficulty as OpenAL, but may offer features and conveniences that OpenAL does not provide.

For applications that are not already using OpenAL already for nonstreaming audio, but need audio streaming capabilities that AVFoundation does not provide, Audio Queue Services is a compelling choice. But if your application is already using OpenAL for nonstreaming audio, the impedance mismatch between Audio Queue Services and OpenAL may cause you to miss easy opportunities to exploit cool things OpenAL can already do for you, such as spatializing your streaming audio (as demonstrated in this chapter).

AVFoundation-Based Background Music for Space Rocks!

Here's some good news: You already know how to play long files (stream) with AVAudioPlayer. We walked through an example in Chapter 9. AVAudioPlayer takes care of all the messy bookkeeping and threading, so you don't need to worry about it if you use this API.

There is more good news: iPhone OS allows you to use different audio APIs without jumping through hoops. We can take our Space Rocks! OpenAL-based application and add background music using AVAudioPlayer.

One thing to keep in mind is that you want to set the audio session only once (don't set it up in your AVFoundation code, and again in your OpenAL code). But interruptions must be handled for each API.

We will continue building on SpaceRocksOpenAL3D_6_SourceRelative from the previous chapter. (Note that this version does not include the changes we made for background music using the Media Player framework.) The completed project for this example is SpaceRocksAVFoundation. We will go through this very quickly, since you've already seen most of it in Chapter 9.

The Playback Sound Controller

As a baseline template, let's copy over our AVPlaybackSoundController class from Chapter 9 into the Space Rocks! project. We will then gut the code to remove the things we don't need. We will also add a few new methods to make it easier to integrate with Space Rocks! In truth, it would probably be just as easy to use Apple's AVAudioPlayer directly in the BBSceneController, but I wanted a separate place to put the interruption delegate callbacks to keep things clean.

Starting with the AVPlaybackSoundController header, let's delete all the old methods. Then delete the separate speech and music player. In its place, we'll create a generic "stream" player. The idea is that if the game needs multiple streams, we can instantiate multiple instances of this class. I suppose this makes this class less of a "controller," but oh well.

The class also conformed to the AVAudioSessionDelegate protocol. We will let the OpenAL controller class continue to set up and manage the audio session, so we can delete this, too. We need a way to specify an arbitrary sound file, so we'll make a new designated initializer called initWithSoundFile:, which takes an NSString* parameter. Finally, we'll add some new methods and properties to control playing, pausing, stopping, volume, and looping. The modified AVPlaybackSoundController.h file should look like this:

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

@interface AVPlaybackSoundController : NSObject <AVAudioPlayerDelegate>
{
   AVAudioPlayer* avStreamPlayer;
}

@property(nonatomic, retain) AVAudioPlayer* avStreamPlayer;
@property(nonatomic, assign) NSInteger numberOfLoops;
@property(nonatomic, assign) float volume;

- (id) initWithSoundFile:(NSString*) sound_file_basename;

- (void) play;
- (void) pause;
- (void) stop;

@end

In our implementation, we delete almost everything except the AVAudioPlayerDelegate methods. We will purge the audioPlayerDidFinishPlaying:successfully:-specific implementation though. We then just implement the new methods. Most of them are direct pass-throughs to AVAudioPlayer. The one exception is our new initializer. This code will create our new AVAudioPlayer instance. We also copy and paste the file-detection code we use from the OpenAL section to locate a file without requiring an extension. The new AVPlaybackSoundController.m file looks like this:

#import "AVPlaybackSoundController.h"

@implementation AVPlaybackSoundController

@synthesize avStreamPlayer;


- (id) initWithSoundFile:(NSString*)sound_file_basename

{
   NSURL* file_url = nil;
   NSError* file_error = nil;

   // Create a temporary array containing the file extensions we want to handle.
   // Note: This list is not exhaustive of all the types Core Audio can handle.
   NSArray* file_extension_array = [[NSArray alloc]
     initWithObjects:@"caf", @"wav", @"aac", @"mp3", @"aiff", @"mp4", @"m4a", nil];
   for(NSString* file_extension in file_extension_array)
   {
       // We need to first check to make sure the file exists;
       // otherwise NSURL's initFileWithPath:ofType will crash if the file doesn't exist
       NSString* full_file_name = [NSString stringWithFormat:@"%@/%@.%@",
         [[NSBundle mainBundle] resourcePath], sound_file_basename, file_extension];
       if(YES == [[NSFileManager defaultManager] fileExistsAtPath:full_file_name])
       {
          file_url = [[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle]
             pathForResource:sound_file_basename ofType:file_extension]] autorelease];
          break;
       }
    }
    [file_extension_array release];

    if(nil == file_url)
    {
       NSLog(@"Failed to locate audio file with basename: %@", sound_file_basename);
       return nil;
    }

    self = [super init];
    if(nil != self)
    {
       avStreamPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:file_url
          error:&file_error];
       if(file_error)
       {
          NSLog(@"Error loading stream file: %@", [file_error localizedDescription]);
       }
       avStreamPlayer.delegate = self;
       // Optional: Presumably, the player will start buffering now instead of on play.
       [avStreamPlayer prepareToPlay];
    }
    return self;
}

- (void) play
{
   [self.avStreamPlayer play];
}

- (void) pause
{
   [self.avStreamPlayer pause];
}

- (void) stop
{
   [self.avStreamPlayer stop];
}

- (void) setNumberOfLoops:(NSInteger)number_of_loops
{
   self.avStreamPlayer.numberOfLoops = number_of_loops;
}

- (NSInteger) numberOfLoops
{
   return self.avStreamPlayer.numberOfLoops;
}

- (void) setVolume:(float)volume_level
{
   self.avStreamPlayer.volume = volume_level;
}

- (float) volume
{
   return self.avStreamPlayer.volume;
}

- (void) dealloc
{
   [avStreamPlayer release];
   [super dealloc];
}

#pragma mark AVAudioPlayer delegate methods

- (void) audioPlayerDidFinishPlaying:(AVAudioPlayer*)which_player
    successfully:(BOOL)the_flag
{
}

- (void) audioPlayerDecodeErrorDidOccur:(AVAudioPlayer*)the_player
    error:(NSError*)the_error
{
   NSLog(@"AVAudioPlayer audioPlayerDecodeErrorDidOccur: %@", [the_error
         localizedDescription]);
}

- (void) audioPlayerBeginInterruption:(AVAudioPlayer*)which_player
{
}

- (void) audioPlayerEndInterruption:(AVAudioPlayer*)which_player
{
   [which_player play];
 }
@end

We probably should do something interesting with audioPlayerDidFinishPlaying:successfully: to integrate it with the rest of the game engine, similar to what we did with the OpenAL resource manager. But since this example is concerned only with playing background music, which we will infinitely loop, we won't worry about that here.

Integration into Space Rocks!

Now for the integration. Remember to add AVFoundation.framework to the project so it can be linked. In BBConfiguration.h, we'll add our background music file. Prior to this point, I've been making my own sound effects, or in a few instances, finding public domain stuff on the Internet. But creating a good soundtrack exceeds my talents, and it is very hard to find one in the public domain or free for commercial use. Fortunately for us, Ben Smith got permission from the musician he used for Snowferno to reuse one of his soundtracks for Space Rocks! for our book. Please do not use this song for your own projects, as this permission does not extend beyond this book's example.

Attribution Credits:
Music by Michael Shaieb
© Copyright 2009 FatLab Music
From "Snowferno" for iPhone/iPod touch

Also, for demonstration purposes, I have compressed the soundtrack using AAC into an .m4a container file. This is to demonstrate that we can use restricted compression formats for our audio files in our game. Chapter 11 covered audio file formats. Remember that the hardware decoder can handle only one file in compression format at a time. While compressing it, I converted it down to 22 kHz to match all our other sample rates for performance. You'll find the file D-ay-Z-ray_mix_090502.m4a in the completed project. Make sure to add it to your project if you are following along.

Add this line to BBConfiguration.h:

#define BACKGROUND_MUSIC @"D-ay-Z-ray_mix_090502"

In BBSceneController.h, we're going to add a new instance variable:

@class AVPlaybackSoundController;

@interface BBSceneController : NSObject <EWSoundCallbackDelegate> {
  ...
  AVPlaybackSoundController* backgroundMusicPlayer;
}

In BBSceneController.m, we are going to create a new instance of the backgroundMusicPlayer in the init method. We want to use our background music file and set the player to infinitely loop. To avoid overwhelming all the other audio, we will reduce the volume of the music. Once this is all set up, we tell the player to play.

- (id) init
{
   self = [super init];
   if(nil != self)
   {
      SetPreferredSampleRate(22050.0);
      [[OpenALSoundController sharedSoundController] setSoundCallbackDelegate:self];
      [self invokeLoadResources];
      [[OpenALSoundController sharedSoundController]
          setDistanceModel:AL_INVERSE_DISTANCE_CLAMPED];
      [[OpenALSoundController sharedSoundController] setDopplerFactor:1.0];
      [[OpenALSoundController sharedSoundController] setSpeedOfSound:343.3];
      backgroundMusicPlayer = [[AVPlaybackSoundController alloc]
          initWithSoundFile:BACKGROUND_MUSIC];
      backgroundMusicPlayer.numberOfLoops = −1; // loop music
      backgroundMusicPlayer.volume = 0.5;
   }
   return self;
}

Conversely, in our dealloc method, we should remember to delete the player for good measure.

[backgroundMusicPlayer stop];
[backgroundMusicPlayer release];

And, of course, we need to actually start playing the music. We could do this in init, but the startScene method seems to be more appropriate. So add the following line there:

[backgroundMusicPlayer play];

Finally, in the audio session initialization code in our OpenALSoundController class, we should switch the mode from Ambient to Solo Ambient, because we are using an .m4a file for our background music, and we would like to minimize the burden on the CPU by using the hardware decoder. If we don't do this, the music will play using the software decoder, which will work but could significantly degrade the performance of Space Rocks!, since the game already pushes the CPU pretty hard, even without audio. And it doesn't make a lot of sense to keep using Ambient mode, which would allow mixing iPod music, now that we have our own soundtrack. Alternatively, we could avoid using a restricted compression format for our audio and not worry about this. But since we have an idle hardware decoding unit and can benefit from higher compression, we will exploit these capabilities. The remaining streaming examples will also switch to Solo Ambient for these same reasons.

Find our call to InitAudioSession in OpenALSoundController.m's init method, and change the first parameter to kAudioSessionCategory_SoloAmbientSound.

InitAudioSession(kAudioSessionCategory_SoloAmbientSound, MyInterruptionCallback, self, PREFERRED_SAMPLE_OUTPUT_RATE);

Congratulations! Not only do you have background music, but you have also learned all the basic essential elements in creating a complete game solution with respect to audio. Many developers will often be satisfied here. They can play OpenAL sound effects and stream audio easily with AVFoundation.

But I encourage you to stick around for the next section on the OpenAL streaming solution. While it's more difficult, you'll continue to get features such as spatialized sound. You'll also be able to reuse more infrastructure, so things will integrate more smoothly, instead of having two disparate audio systems that don't really talk to each other. For example, we currently do nothing with AVAudioPlayer's audioPlayerDidFinishPlaying:successfully: method because we would have to think about how it would integrate with the rest of the system. We already defined all those behaviors for our OpenAL system, so there is less ambiguity on how we should handle it with OpenAL streaming.

OpenAL Buffer Queuing Introduction

Before we jump into the technical specifics of how OpenAL handles streaming, it may be a useful thought exercise to imagine how you might accomplish streaming with what you already know. Let's start with the three fundamental objects in OpenAL: buffers, sources, and listeners. Listeners are irrelevant to this discussion, so we can ignore them. It's going to come down to buffers and sources.

Conceptually, streaming is just playing small buffers sequentially. You use small buffers so you don't consume too much RAM trying to load a whole giant file.

To implement streaming with what you already know, you might try something like this (pseudo code):

while( StillHaveDataInAudioFile(music_file) )
{
   pcm_data = GetSmallBufferOfPCMDataFromFile(music_file);
   alBufferData(al_buffer, pcm_data, ...); // copy the data into a OpenAL buffer
   alSourcei(al_source, AL_BUFFER, al_buffer); // attach OpenAL Buffer to OpenAL source
   alSourcePlay(al_source);
   MagicFunctionThatWaitsForPlayingToStop();
}

In this pseudo code, we read a small chunk of data from a file, copy the data into OpenAL, and then play the data. Once the playback ends, we repeat the process until we have no more audio data.

In a theoretical world, this code could work. But in the real world, there is a fatal flaw of latency risk. Once the playback ends, you need to load the next chunk of data and then start playing it before the user perceives a pause in the playback. If you have a very fast, low-latency computer, you might get away with this, but probably not.

You could try to improve on this function by using a double-buffer technique (often employed in graphics). You would create two OpenAL buffers. While the source is playing, you could start reading and copying the next chunk of data. Then when the playback ends, you can immediately feed the source the next buffer. You still have a latency risk, in that you might not be able to call play fast enough, but your overall latency has been greatly reduced. To mitigate the case of being overloaded and falling behind, you could simply extend the number of buffers you attempt to preload. Instead of having two buffers, try ten or a hundred.

Fortunately, OpenAL solves this last remaining problem by providing several additional API functions that allow you to queue buffers to a playing (or stopped) source. That way, you don't need to worry about swapping the buffer at the perfect time and calling play to restart. OpenAL will do that on your behalf behind the scenes. This is a reasonably elegant design, in that you don't need to introduce any new object types. You remain with just buffers, sources, and listeners. Figure 12-6 illustrates how buffer queuing works in OpenAL.

The OpenAL buffer queuing life cycle. OpenAL allows you to queue OpenAL buffers on an OpenAL source. The source will play a buffer, and when finished, it will mark the buffer as processed. The source will then automatically start playing the next buffer in the queue. To keep the cycle going, you should reclaim the processed buffers, fill them with new audio data, and queue them again to the source. Note that you are permitted to have multiple OpenAL sources doing buffer queuing.

Figure 12.6. The OpenAL buffer queuing life cycle. OpenAL allows you to queue OpenAL buffers on an OpenAL source. The source will play a buffer, and when finished, it will mark the buffer as processed. The source will then automatically start playing the next buffer in the queue. To keep the cycle going, you should reclaim the processed buffers, fill them with new audio data, and queue them again to the source. Note that you are permitted to have multiple OpenAL sources doing buffer queuing.

That said, there is some grunt work and bookkeeping you must do. You will need to manage multiple buffers, and find opportune times to fill the buffers with new data and add them to the queue on your designated source(s). You are also required to unqueue[30] buffers from your source(s) when OpenAL is done with them (called processed buffers).

Because there is enough going on, I felt it would be better to start with a simple isolated example before trying to integrate streaming into Space Rocks! We will start with a new project. The completed project for this example is called BasicOpenALStreaming. The project has a UI that is similar to the interface we constructed in Chapter 9 for AVPlayback, but even simpler (see Figure 12-7).

The example project: BasicOpenALStreaming, which has a minimal UI

Figure 12.7. The example project: BasicOpenALStreaming, which has a minimal UI

In this example, we will just play music and forego the speech player. The play button will play and pause the music. For interest, we will add a volume slider. The volume slider is connected to the OpenAL listener gain, so we are using it like a master volume control.

All the important code is contained in the new class OpenALStreamingController. Much of this class should look very familiar to you, as it is mostly a repeat of the first OpenAL examples in Chapter 10 (i.e., initialize an audio session, create a source, and so on).

Initialization

Following the pattern we used for OpenALSoundController in Chapter 10, let's examine the changes we need to make for OpenALStreamingController.m's initOpenAL method.

- (void) initOpenAL
{
   openALDevice = alcOpenDevice(NULL);
   if(openALDevice != NULL)
   {
// Use the Apple extension to set the mixer rate
      alcMacOSXMixerOutputRate(44100.0);

      // Create a new OpenAL context
      // The new context will render to the OpenAL device just created
      openALContext = alcCreateContext(openALDevice, 0);
      if(openALContext != NULL)
      {
         // Make the new context the current OpenAL context
         alcMakeContextCurrent(openALContext);
      }
      else
      {
          NSLog(@"Error, could not create audio context.");
          return;
      }
   }
   else
   {
       NSLog(@"Error, could not get audio device.");
       return;
   }

   alGenSources(1, &streamingSource);
   alGenBuffers(MAX_OPENAL_QUEUE_BUFFERS, availableALBufferArray);
   availableALBufferArrayCurrentIndex = 0;

   // File is from Internet Archive Open Source Audio, US Army Band, public domain
   // http://www.archive.org/details/TheBattleHymnOfTheRepublic_993
   NSURL* file_url = [NSURL fileURLWithPath:[[NSBundle mainBundle]
        pathForResource:@"battle_hymn_of_the_republic" ofType:@"mp3"]];
   if(file_url)
   {
      streamingAudioRef = MyGetExtAudioFileRef((CFURLRef)file_url,
        &streamingAudioDescription);
   }
   else
   {
       NSLog(@"Could not find file!
");
       streamingAudioRef = NULL;
   }

   intermediateDataBuffer = malloc(INTERMEDIATE_BUFFER_SIZE);
}

The only new/customized code is at the bottom of the method after the OpenAL context is initialized.

First, we create an OpenAL source, which we will use to play our music using alGenSources. Next, we create five OpenAL buffers using the array version of the function. The number of buffers was picked somewhat arbitrarily. We'll talk about picking the number a little later, but for now, just know that our program will have a maximum of five buffers queued at any one time.

Then we open the music file we are going to play. I am reusing the same file from the AVPlayback example in Chapter 9.

The first semi-new thing is that we call MyGetExtAudioFileRef. Recall that in our side quest in Chapter 10 when we loaded files into OpenAL using Core Audio, we deliberately broke up the function into three pieces, but we used only the third piece, MyGetOpenALAudioDataAll, which is built using the other two pieces. MyGetOpenALAudioDataAll loads the entire file into a buffer. But since we don't want to load the entire file for this streaming example, we need to use the two other pieces. The first piece, MyGetExtAudioFileRef, will just open the file using Core Audio (Extended File Services) and return a file handle (ExtAudioFileRef). We save this file reference in an instance variable, because we need to read the data from it later as we stream. We also have streamingAudioDescription as an instance variable, because we need to keep the audio description metadata around for the other function. (We'll get to the second piece a little later.)

Next, we allocate memory for a PCM buffer. This is memory we will have Extended File Services decode file data into so we can use it. I have hard-coded the buffer to be 32KB, which means this demo is going to stream audio data in 32,768-byte chunks at a time.

That concludes our initialization. Let's now go to the heart of the program, which is in the animationCallback: method in OpenALStreamingController. This is where all the important stuff happens.

Unqueuing

We need to do two things with OpenAL streaming: queue buffers to play and unqueue buffers that have finished playing (processed buffers). We will start with unqueuing (and reclaiming) the processed buffers. I like to do this first, because I can then turn around and immediately use that buffer again for the next queue.

First, OpenAL will let us query how many buffers have been processed using alGetSourcei with AL_BUFFERS_PROCESSED. In OpenALStreamingController.m's animationCallback: method, we need to have the following:

ALint buffers_processed = 0;
alGetSourcei(streamingSource, AL_BUFFERS_PROCESSED, &buffers_processed);

Next, we can write a simple while loop to unqueue and reclaim each processed buffer, one at a time.[31] To actually unqueue a buffer, OpenAL provides the function alSourceUnqueueBuffers. You provide it the source you want to unqueue from, the number of buffers you want to unqueue, and a pointer to where the unqueued buffer IDs will be returned.

while(buffers_processed > 0)
{
   ALuint unqueued_buffer;
   alSourceUnqueueBuffers(streamingSource, 1, &unqueued_buffer);
availableALBufferArrayCurrentIndex--;
   availableALBufferArray[availableALBufferArrayCurrentIndex] = unqueued_buffer;

   buffers_processed--;
}

We keep an array of available buffers so we can use them in the next step. We use the array like a stack, hence the incrementing and decrementing of the availableALBufferArrayCurrentIndex. Don't get too hung up on this part. Just consider it an opaque data structure. Because we're doing a minimalist example, I wanted to avoid using Cocoa data structures or introducing my own. The next example will not be minimalist.

Queuing

Now we are ready to queue a buffer. When we queue a buffer, we need to load a chunk of the data from the file, get the data into OpenAL, and queue it to the OpenAL source. We are going to queue only one buffer in a single function pass. The idea is that this function will be called repeatedly (30 times a second or so). The assumption is the function call rate will be faster than OpenAL can use up the buffer, so there will always be multiple buffers queued in a given moment, up to some maximum number of queue buffers that we define. For this example, MAX_OPENAL_QUEUE_BUFFERS is hard-coded as follows:

#define MAX_OPENAL_QUEUE_BUFFERS 5

We continue adding code to animationCallback: in OpenALStreamingController.m. The first thing we do is check to make sure we have available buffers to queue. If not, then we don't need to do anything.

if(availableALBufferArrayCurrentIndex < MAX_OPENAL_QUEUE_BUFFERS)
{

Once we establish we need to queue a new buffer, we do it.

ALuint current_buffer = availableALBufferArray[availableALBufferArrayCurrentIndex];

  ALsizei buffer_size;
  ALenum data_format;
  ALsizei sample_rate;

  MyGetDataFromExtAudioRef(streamingAudioRef, &streamingAudioDescription,
     INTERMEDIATE_BUFFER_SIZE, &intermediateDataBuffer, &buffer_size,
    &data_format, &sample_rate);
  if(0 == buffer_size) // will loop music on EOF (which is 0 bytes)
  {
     MyRewindExtAudioData(streamingAudioRef);
     MyGetDataFromExtAudioRef(streamingAudioRef, &streamingAudioDescription,
       INTERMEDIATE_BUFFER_SIZE, &intermediateDataBuffer, &buffer_size,
       &data_format, &sample_rate);
  }
  alBufferData(current_buffer, data_format, intermediateDataBuffer, buffer_size,
    sample_rate);
  alSourceQueueBuffers(streamingSource, 1, &current_buffer);
availableALBufferArrayCurrentIndex++;

We use our MyGetDataFromExtAudioRef function (which is our second piece of the Core Audio file loader from Chapter 10) to fetch a chunk of data from our file. We provide the file handle and the audio description, which you can consider as opaque data types for Core Audio. We provide the buffer size and buffer to which we want the data copied. The function will return by reference the amount of data we actually get back, the OpenAL data format, and the sample rate. We can then feed these three items directly into alBufferData. We use alBufferData for simplicity in this example. For performance, we should consider using alBufferDataStatic, since this function is going to be called a lot, and we are going to be streaming in the middle of game play where performance is more critical. We will change this in the next example.

Finally, we call OpenAL's function alSourceQueueBuffers to queue the buffer. We specify the source, the number of buffers, and an array containing the buffer IDs.

There is a corner case handled in the preceding code. If we get 0 bytes back from MyGetDataFromExtAudioRef, it means we hit the end of the file (EOF). For this example, we want to loop the music, so we call our custom helper function MyRewindExtAudioData, which rewinds the file pointer to the beginning. We then grab the data again. If you were thinking you could use OpenAL's built-in loop functionality, this won't work. OpenAL doesn't have access to the full data anymore, since we broke everything into small pieces and unqueued the old data (the beginning of the file). We must implement looping ourselves.

Buffer Underrun

We've now queued the buffer, but there is one more step we may need to take. It could happen that we were not quick enough in queuing more data, and OpenAL ran out of data to play. If OpenAL runs out of data to play, it must stop playing. This makes sense, because OpenAL can't know whether we were too slow at queuing more data or we actually finished playing (and don't plan to loop). I call the case where we were too slow a buffer underrun.

To remedy the potential buffer underrun case, our task is to find out if the OpenAL source is still playing. If it is not playing, we need to determine if it is because of a buffer underrun or because we don't want to play (e.g., finished playing, wanted to pause, etc.).

We will use alGetSourcei to find the source state to figure out if we are not playing. Then we can use alGetSourcei to get the number of buffers queued to help determine if we are in a buffer underrun situation. (We will have one queued buffer now since we just added one in the last step.) Then we make sure the user didn't pause the player, which would be a legitimate reason for not playing even though we have buffers queued. If we determine we should, in fact, be playing, we simply call alSourcePlay.

ALenum current_playing_state;
alGetSourcei(streamingSource, AL_SOURCE_STATE, &current_playing_state);
// Handle buffer underrun case
if(AL_PLAYING != current_playing_state)
{
ALint buffers_queued = 0;

   alGetSourcei(streamingSource, AL_BUFFERS_QUEUED, &buffers_queued);

   if(buffers_queued > 0 && NO == self.isStreamingPaused)
   {
      // Need to restart play
      alSourcePlay(streamingSource);
   }
}

Pausing

We use alSourcePause for the first time in this example. It probably behaves exactly as you think it does, so there is not much to say about it.

Fast Forwarding and Rewinding

You may be wondering why we removed the fast-forward and rewind buttons from the AVPlayback example. The reason is that seeking is more difficult with OpenAL streaming than it is with AVAudioPlayer.

OpenAL does have support for seeking. There are three different attributes you can use with alSource* and alGetSource*: AL_SEC_OFFSET, AL_SAMPLE_OFFSET, and AL_BYTE_OFFSET. These deal with the positions in seconds, samples, or bytes. With fully loaded buffers, seeking is pretty easy. But with streaming, you will need to do some additional bookkeeping.

The problem is that with streaming, OpenAL can seek only within the range of what is currently queued. If you seek to something that is outside that range, it won't work. In addition, you need to make sure your file handle is kept in sync with your seek request; otherwise, you will be streaming new data in at the wrong position. Thus, you really need to seek at the file level instead of the OpenAL level.

Generally, emptying the current queue and rebuffering it at the new position is the easiest solution. (If you don't empty the queue, you may have a lag between when the users requested a seek and when they actually hear it.) But, of course, as you've just emptied your queue and need to start over, you may have introduced some new latency issues. For this kind of application, probably no one will care though. For simplicity, this was omitted from the example. For the curious, notice that our MyRewindExtAudioData function wraps ExtAudioFileSeek. This is what you'll probably want to use to seek using Extended File Services.

Startup Number of Buffers and Filling the Queue

Recall that in this example, we fill only one buffer per update loop pass. This means the game loop must be run through at least five times before we can fill our queue to our designated maximum of MAX_OPENAL_QUEUE_BUFFERS. This brings up the question, "What is the best strategy for filling the queue?"

There are two extremes to consider. The first extreme is what we did in the example. We fill a single buffer and move on. The advantages of this strategy are that it is easy to implement and it has a cheap startup cost. It allows us to spread the buffer fill-up cost over a longer period of time. The disadvantage is that when we first start up, we don't have a safety net. We are particularly vulnerable to an untimely system usage spike that prevents us from being able to queue the next buffer in time.

The other extreme is that we fill up our queue to our designated maximum when we start playing. So, in this example, rather than filling just one buffer, we would fill up five buffers. While this gives us extra protection from starvation, we have a trade-off of a longer startup cost. This may manifest itself as a performance hiccup in the game if the startup cost is too great. For example, the player may fire a weapon that we tied to streaming. If we spend too much time up front to load more buffers, the user may perceive a delay between the time he touched the fire button and when the weapon actually seemed to fire in the game.

You will need to experiment to find the best pattern for you and your game. You might consider hybrids of the two extremes, where on startup, you queue two or three buffers instead of the maximum. You might also consider varying the buffer sizes and the number of buffers you queue based on metrics such as how empty the queue is or how taxed the CPU is. And, of course, multithreading/concurrency can be used, too.

How to Choose the Number and Size of Buffers

Finally, you may be wondering why we picked five buffers and made our buffers 32KB. This is somewhat of a guessing game. You generally need to find values that give good performance for your case. You are trying to balance different things.

The buffer size will affect how long it takes to load new data from the file. Larger buffers take more time. If you take too much time, you will starve the queue, because you didn't get data into the queue fast enough. In my personal experience, I have found 1KB to 64KB to be the range for buffer sizes. More typically, I tend to deal with 8KB to 32KB. Apple seems to like 16KB to 64KB. Some of Apple's examples include a CalculateBytesForTime function, which will dynamically decide the size based on some criteria. (It's worth a look.)

I also like powers of two because of a hard-to-reproduce bug I had way back with the Loki OpenAL implementation. When not using power-of-two buffer sizes, I had certain playback problems. That implementation is dead now, but the habit stuck with me. Some digital processing libraries, like for a Fast Fourier Transform (FFT), often prefer arrays in power-of-two sizes for performance reasons. So it doesn't hurt.[32]

As for the number of buffers, you basically want enough to prevent buffer underruns. The more buffers you have queued, the less likely it will be that you deplete the queue before you can add more. However, if you have too many buffers, you are wasting memory. The point of streaming is to save memory. And, obviously, there is a relationship with the buffer size. Larger buffer sizes will last longer, so you need fewer buffers.

Also, you might think about the startup number of buffers. In our example, we queue only one buffer to start with. One consequence of that is we must make our buffer larger to avoid early starvation. If we had queued more buffers, the buffer size could be smaller.

OpenAL-Based Background Music for Space Rocks!

This is the moment you've been waiting for. Now that you understand OpenAL buffer queuing, let's integrate it into Space Rocks!, thereby completing our game engine.

We will again use SpaceRocksOpenAL3D_6_SourceRelative from the previous chapter as our starting point for this example. The completed project for this example is SpaceRocksOpenALStreaming1. (This version does not include the changes we made for background music using Media Player framework or AVFoundation. This will be an exclusively OpenAL project.)

The core changes will occur in two locations. We will need to add streaming support to our update loop in OpenALSoundController, and we need a new class to encapsulate our stream buffer data.

A New Buffer Data Class for Streaming

Let's start with the new class. We will name it EWStreamBufferData and create .h and .m files for it. The purpose of this class is analogous to the EWSoundBufferData class we made earlier, except it will be for streamed data.

@interface EWStreamBufferData : NSObject
{
   ALenum openalFormat;
   ALsizei dataSize;
   ALsizei sampleRate;

In Chapter 10, we implemented a central/shared database of audio files with soundFileDictionary and EWSoundBufferData. We will not be repeating that pattern with EWStreamBufferData. This is because it makes no sense to share streamed data. With fully loaded sounds, we can share the explosion sound between the UFO and the spaceship. But with streamed data, we can't do this because we have only a small chunk of the data in memory at any given time. So, in the case of the explosion sound, the spaceship might be starting to explode while the UFO is ending its explosion. Each needs a different buffer segment in the underlying sound file. For streamed data, if both the UFO and spaceship use the same file, they still need to have separate instances of EWStreamBufferData. Because of the differences between the two classes, I have opted to not make EWStreamBufferData a subclass of EWSoundBufferData. You could do this if you want, but you would need to modify the existing code to then be aware of the differences.

This class will contain the multiple OpenAL buffers needed for buffer queuing. In the previous example, we used five buffers. This time, we will use 32. (I'll explain the increase in buffers later.) We will use the alBufferDataStatic extension for improved performance, so we also need 32 buffers for raw PCM data.

#define EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS 32
   ALuint openalDataBufferArray[EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS];
   void* pcmDataBufferArray[EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS];

We will also need to keep track of which buffers are queued and which are available. In the previous example, I apologized for being too minimalist for using a single C array. This time, we will use two NSMutableArrays for more clarity.

NSMutableArray* availableDataBuffersQueue;
  NSMutableArray* queuedDataBuffersQueue;

This class will also contain the ExtAudioFileRef (file handle) to the audio file (and AudioStreamBasicDescription) so we can load data into the buffers from the file as we need it.

ExtAudioFileRef streamingAudioRef;
  AudioStreamBasicDescription streamingAudioDescription;

We also have a few properties. We have a streamingPaused property in case we need to pause the audio, which will allow us to disambiguate from a buffer underrun, as in the previous example. (We will not actually pause the audio in our game.) We add audioLooping so it can be an option instead of hard-coded. And we add an atEOF property so we can record if we hit the end of file.

BOOL audioLooping;
   BOOL streamingPaused;
   BOOL atEOF;
}
@property(nonatomic, assign, getter=isAudioLooping) BOOL audioLooping;
@property(nonatomic, assign, getter=isStreamingPaused) BOOL streamingPaused;
@property(nonatomic, assign, getter=isAtEOF) BOOL atEOF;

We will also add three methods:

+ (EWStreamBufferData*) streamBufferDataFromFileBaseName:(NSString*)sound_file_basename;
- (EWStreamBufferData*) initFromFileBaseName:(NSString*)sound_file_basename;
- (BOOL) updateQueue:(ALuint)streaming_source;
@end

The initFromFileBaseName: instance method is our designated initializer, which sets the file to be used for streaming. For convenience, we have a class method named streamBufferDataFromFileBaseName:, which ultimately does the same thing as initFromFileBaseName:, except that it returns an autoreleased object (as you would expect, following standard Cocoa naming conventions).

You might have noticed that this design contrasts slightly with the method soundBufferDataFromFileBaseName:, which we placed in OpenALSoundController instead of EWSoundBufferData. The main reason is that for EWSoundBufferData objects, we had a central database to allow resource sharing. Since OpenALSoundController is a singleton, it was convenient to put the central database there. EWStreamBufferData differs in that we won't have a central database. Since this class is relatively small, I thought we might take the opportunity to relieve the burden on OpenALSoundController. Ultimately, the two classes are going to work together, so it doesn't matter too much. But for symmetry, we will add a convenience method to OpenALSoundController named streamBufferDataFromFileBaseName:, which just calls the one in this class.

The updateQueue: method is where we are going to do most of the unqueuing and queuing work. We could do this in the OpenALSoundController, but as you saw, the code is a bit lengthy. So again, I thought I we might take the opportunity to relieve the burden on OpenALSoundController.

Finally, because we are using alBufferDataStatic, we want an easy way to keep our PCM buffer associated with our OpenAL buffer, particularly with our NSMutableArray queues. We introduce a helper class that just encapsulates both buffers so we know they belong together.

@interface EWStreamBufferDataContainer : NSObject
{
   ALuint openalDataBuffer;
   void* pcmDataBuffer;
}
@property(nonatomic, assign) ALuint openalDataBuffer;
@property(nonatomic, assign) void* pcmDataBuffer;
@end

In our initialization code for EWStreamBufferData, we need to allocate a bunch of memory: 32 OpenAL buffers (via alGenBuffers), 32 PCM buffers (via malloc), and 2 NSMutableArrays, which are going to mirror the buffer queue state by recording which buffers are queued and which are available. Let's first look at the code related to initialization:

@implementation EWStreamBufferData
@synthesize audioLooping;
@synthesize streamingPaused;
@synthesize atEOF;

- (void) createOpenALBuffers
{

   for(NSUInteger i=0; i<EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS; i++)
   {
       pcmDataBufferArray[i] = malloc(EW_STREAM_BUFFER_DATA_INTERMEDIATE_BUFFER_SIZE);
   }

   alGenBuffers(EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS, openalDataBufferArray);

   availableDataBuffersQueue = [[NSMutableArray alloc]
         initWithCapacity:EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS];
queuedDataBuffersQueue = [[NSMutableArray alloc]
      initWithCapacity:EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS];

   for(NSUInteger i=0; i<EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS; i++)
   {
       EWStreamBufferDataContainer* stream_buffer_data_container =
         [[EWStreamBufferDataContainer alloc] init];
       stream_buffer_data_container.openalDataBuffer = openalDataBufferArray[i];
       stream_buffer_data_container.pcmDataBuffer = pcmDataBufferArray[i];
       [availableDataBuffersQueue addObject:stream_buffer_data_container];
       [stream_buffer_data_container release];
    }
}

- (id) init
{
   self = [super init];
   if(nil != self)
    {
      [self createOpenALBuffers];
    }
    return self;
}

- (EWStreamBufferData*) initFromFileBaseName:(NSString*)sound_file_basename
{
   self = [super init];
   if(nil != self)
   {
      [self createOpenALBuffers];

      NSURL* file_url = nil;

// Create a temporary array containing all the file extensions we want to handle.
// Note: This list is not exhaustive of all the types Core Audio can handle.
NSArray* file_extension_array = [[NSArray alloc]
  initWithObjects:@"caf", @"wav", @"aac", @"mp3", @"aiff", @"mp4", @"m4a", nil];
for(NSString* file_extension in file_extension_array)
{
    // We need to first check to make sure the file exists;
    // otherwise NSURL's initFileWithPath:ofType will crash if the file doesn't exist
    NSString* full_file_name = [NSString stringWithFormat:@"%@/%@.%@",
      [[NSBundle mainBundle] resourcePath], sound_file_basename, file_extension];
    if(YES == [[NSFileManager defaultManager] fileExistsAtPath:full_file_name])
    {
       file_url = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle]
          pathForResource:sound_file_basename ofType:file_extension]];
       break;
    }
 }
 [file_extension_array release];

 if(nil == file_url)
 {
    NSLog(@"Failed to locate audio file with basename: %@", sound_file_basename);
    [self release];
    return nil;
}

 streamingAudioRef = MyGetExtAudioFileRef((CFURLRef)file_url,
    &streamingAudioDescription);
 [file_url release];
 if(NULL == streamingAudioRef)
 {
    NSLog(@"Failed to load audio data from file: %@", sound_file_basename);
    [self release];
    return nil;
 }
}
        return self;
}


+ (EWStreamBufferData*) streamBufferDataFromFileBaseName:(NSString*)sound_file_basename
{
        return [[[EWStreamBufferData alloc] initFromFileBaseName:sound_file_basename] autorelease];
}

Let's examine the initFromFileBaseName: method. The first part of the method is just a copy-and-paste of our file-finding code that tries to guess file extensions. The following is the only important line in this function:

streamingAudioRef = MyGetExtAudioFileRef((CFURLRef)file_url, &streamingAudioDescription);

This opens our file and returns the file handle and AudioStreamBasicDescription.

The streamBufferDataFromFileBaseName: convenience class method just invokes the initFromFileBaseName: instance method.

Finally, in the createOpenALBuffers method, the final for loop fills our availableDataBuffersQueue with our EWStramBufferDataContainer wrapper object. That way, it is easy to get at both the PCM buffer and OpenAL buffer from the array object.

You might be wondering why we have two arrays when the previous example had only one. With two arrays, we can easily know if a buffer is queued or available. We are doing a little extra work here by mirroring (or shadowing) the OpenAL state, but it's not much more work.

For correctness, we also should write our dealloc code:

- (void) dealloc
{
   [self destroyOpenALBuffers];

   if(streamingAudioRef)
   {
      ExtAudioFileDispose(streamingAudioRef);
   }

   [super dealloc];
}
- (void) destroyOpenALBuffers
{
   [availableDataBuffersQueue release];
   [queuedDataBuffersQueue release];

   alDeleteBuffers(EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS,
       openalDataBufferArray);

   for(NSUInteger i=0; i<EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS; i++)
   {
       free(pcmDataBufferArray[i]);
       pcmDataBufferArray[i] = NULL;
   }
}

There shouldn't be any surprises here, except that this class also keeps a file handle, so we need to remember to close the file handle if it is open.

The real meat of this class is the updateQueue: method. As I said, we are going to put all the stuff in the updateQueue update loop from the BasicOpenALStreaming example. This loop should look very familiar to you, as it is the same algorithm. For brevity, I won't reproduce the entire body of the code here, but it's included with the completed project example. However, we will zoom in on subsections of the method next.

Unqueue the Processed Buffers

As in the BasicOpenALStreaming example, let's start with unqueuing the processed buffers in the updateQueue method.

ALint buffers_processed = 0;
alGetSourcei(streaming_source, AL_BUFFERS_PROCESSED, &buffers_processed);
while(buffers_processed > 0)
{
   ALuint unqueued_buffer;
   alSourceUnqueueBuffers(streaming_source, 1, &unqueued_buffer);

   [availableDataBuffersQueue insertObject:[queuedDataBuffersQueue lastObject]
       atIndex:0];
   [queuedDataBuffersQueue removeLastObject];

   buffers_processed--;
}

Astute observers might notice that we do nothing with the unqueued_buffer we retrieve from OpenAL. This is because we are mirroring (shadowing) the OpenAL buffer queue with our two arrays, so we already know which buffer was unqueued.[33]

Queue a New Buffer If Necessary

Continuing in updateQueue, let's queue a new buffer if we haven't gone over the number of queued buffers specified by our self-imposed maximum, which is 32 buffers. Since we have a data structure that holds all our available buffers, we can just check if the data structure has any buffers.

if([availableDataBuffersQueue count] > 0 && NO == self.isAtEOF)
{
    // Have more buffers to queue
    EWStreamBufferDataContainer* current_stream_buffer_data_container =
      [availableDataBuffersQueue lastObject];
    ALuint current_buffer = current_stream_buffer_data_container.openalDataBuffer;
    void* current_pcm_buffer = current_stream_buffer_data_container.pcmDataBuffer;

    ALenum al_format;
    ALsizei buffer_size;
    ALsizei sample_rate;

    MyGetDataFromExtAudioRef(streamingAudioRef, &streamingAudioDescription,
      EW_STREAM_BUFFER_DATA_INTERMEDIATE_BUFFER_SIZE,
      &current_pcm_buffer, &buffer_size, &al_format, &sample_rate);
    if(0 == buffer_size) // will loop music on EOF (which is 0 bytes)
    {
       if(YES == self.isAudioLooping)
       {
          MyRewindExtAudioData(streamingAudioRef);
          MyGetDataFromExtAudioRef(streamingAudioRef, &streamingAudioDescription,
            EW_STREAM_BUFFER_DATA_INTERMEDIATE_BUFFER_SIZE,
            &current_pcm_buffer, &buffer_size, &al_format, &sample_rate);
       }
       else
       {
          self.atEOF = YES;
       }
   }

   if(buffer_size > 0)
   {
      alBufferDataStatic(current_buffer, al_format, current_pcm_buffer, buffer_size,
         sample_rate);
      alSourceQueueBuffers(streaming_source, 1, &current_buffer);

      [queuedDataBuffersQueue insertObject:current_stream_buffer_data_container
         atIndex:0];
         [availableDataBuffersQueue removeLastObject];

There are just a few minor new things here. First, audio looping is now an option, so we have additional checks for atEOF. If we are at an EOF, we don't want to do anything. (We will have additional logic at the end of this function to determine what to do next if we do encounter EOF.) If we encounter EOF while fetching data, we record that in our instance variable, but only if we are not supposed to loop audio. If we are supposed to loop, we pretend we never hit EOF and just rewind the file.

The other minor thing is we now use alBufferDataStatic.

Handle a Buffer Underrun

Still in updateQueue, let's handle the buffer underrun case.

ALenum current_playing_state;
    alGetSourcei(streaming_source, AL_SOURCE_STATE, &current_playing_state);
    // Handle buffer underrun case
    if(AL_PLAYING != current_playing_state)
    {
       ALint buffers_queued = 0;
       alGetSourcei(streaming_source, AL_BUFFERS_QUEUED, &buffers_queued);
       if(buffers_queued > 0 && NO == self.isStreamingPaused)
       {
          // Need to restart play
          alSourcePlay(streaming_source);
       }
     }
   }
}

This code is essentially identical to the previous example.

Handle EOF and Finished Playing

The part to handle EOF and finished playing is new but simple. To end the updateQueue method, for convenience, this function will return YES if we discover that the source has finished playing its sound. (The OpenALSoundController will use this information later.) This requires two conditions:

  • We must have encountered an EOF.

  • The OpenAL source must have stopped playing.

Once we detect those two conditions, we just record the state so we can return it. We also add a repeat check for processed buffers, since typically, once it is detected that the sound is finished playing, this method will no longer be invoked for that buffer instance. This is a paranoid check. In theory, there is a potential race condition between OpenAL processing buffers and us trying to remove them. It is possible that once we pass the processed buffers check at the top of this method, OpenAL ends up processing another buffer while we are in the middle of the function. We would like to leave everything in a pristine state when it is determined that that we are finished playing, so we run the processed buffers check one last time and remove the buffers as necessary.

if(YES == self.isAtEOF)
  {
     ALenum current_playing_state;

     alGetSourcei(streaming_source, AL_SOURCE_STATE, &current_playing_state);
     if(AL_STOPPED == current_playing_state)
     {
        finished_playing = YES;

        alGetSourcei(streaming_source, AL_BUFFERS_PROCESSED, &buffers_processed);
while(buffers_processed > 0)
        {
           ALuint unqueued_buffer;
           alSourceUnqueueBuffers(streaming_source, 1, &unqueued_buffer);
           [availableDataBuffersQueue insertObject:[queuedDataBuffersQueue lastObject]
             atIndex:0];
           [queuedDataBuffersQueue removeLastObject];

           buffers_processed--;
         }
     }
  }

  return finished_playing;
}

Whew! You made it through this section. I hope that wasn't too bad, as you've already seen most of it before. Now we need to make some changes to the OpenALSoundController.

OpenALSoundController Changes

Since the OpenALSoundController is our centerpiece for all our OpenAL code, we need to tie in the stuff we just wrote with this class. There are only a few new methods we are going to introduce in OpenALSoundController:

- (EWStreamBufferData*) streamBufferDataFromFileBaseName:(NSString*)sound_file_basename;
- (void) playStream:(ALuint)source_id streamBufferData:(EWStreamBufferData*)stream_buffer_data;
- (void) setSourceGain:(ALfloat)gain_level sourceID:(ALuint)source_id;

The method streamBufferDataFromFileBaseName: is just a pass-through to EWStreamBufferData's streamBufferDataFromFileBaseName:, which we add mostly for aesthetic reasons. This gives us symmetry with OpenALSoundController's soundBufferDataFromFileBaseName: method.

- (EWStreamBufferData*) streamBufferDataFromFileBaseName:(NSString*)sound_file_basename
{
   return [EWStreamBufferData streamBufferDataFromFileBaseName:sound_file_basename];
}

The setSourceGain: method is kind of a concession/hack. Prior to this, only our BBSceneObjects made sound. They all access their OpenAL source properties (such as gain) through EWSoundSourceObjects. But because we are focusing on background music, it doesn't make a lot of sense to have a BBSceneObject to play background music. So, this is a concession to let us set the volume level on our background music without needing the entire SceneObject infrastructure.

- (void) setSourceGain:(ALfloat)gain_level sourceID:(ALuint)source_id
{
   alSourcef(source_id, AL_GAIN, gain_level);
}

The playStream: method is the most important new method we add. This method will allow us to designate we are playing a stream instead of a preloaded sound. We could get really clever and try to unify everything into one play method, but this will keep things more straightforward for educational purposes.

We also need to go back and modify some existing methods to be aware of streams. Additionally, we will introduce some additional bookkeeping to manage our streams. Most important, we will introduce a new instance variable:

NSMutableDictionary* streamingSourcesDictionary;

In the initOpenAL method, allocate a new dictionary for this instance variable with the other collections. (And don't forget to release it in the tearDownOpenAL method.)

streamingSourcesDictionary = [[NSMutableDictionary alloc] initWithCapacity:MAX_NUMBER_OF_ALSOURCES];

We will use this to track which sources are currently playing a stream, similar to how we tracked currently playing (preloaded) sounds. You may notice this is a dictionary instead of a set, which is different from what we did with preloaded sources. In this case, we also want access to the buffer that the source is currently playing. So when we go through our OpenALSoundController's update loop, we can call the updateQueue: method we wrote in the previous section. In the preloaded case, we didn't need to do anything with the buffer, so we didn't need access to the data. Thus, we could use an NSSet containing only sources. But our streamingSourcesDictionary will be keyed by source IDs, and the values will be EWStreamBufferData objects.

Now that you know where we are going with streamingSourcesDictionary, let's look at our new methods. The only one with new material here worth talking about is playStream:.

// Must call reserveSource to get the source_id
- (void) playStream:(ALuint)source_id
    streamBufferData:(EWStreamBufferData*)stream_buffer_data
{
   // Trusting the source_id passed in is valid
   [streamingSourcesDictionary setObject:stream_buffer_data
          forKey:[NSNumber numberWithUnsignedInt:source_id]];
   // updateQueue will automatically start playing
   [stream_buffer_data updateQueue:source_id];
}

This is a simple method because all the logic is elsewhere. It is just bookkeeping. First, we add our source and EWStreamBufferData to our streamingSourcesDictionary. Then we just call the updateQueue: method we implemented in the previous section to start playing. Recall that one pass of updateQueue: will queue a new buffer and then do buffer underrun detection. Since there will be a new buffer in the queue but the source is stopped (because we never started playing it), the code will consider this to be a buffer underrun situation and automatically start playing for us. How nice and elegant, right?

Now it's important that we enhance our update method to handle streams. It needs to do all the same stuff that the loop does for preloaded sources—detect finished playback, invoke callbacks, and recycle the sources. But it also needs to update the buffer queues for each playing stream. To update the buffer queues, we need to invoke updateQueue: on each EWStreamBufferData object. This will be very easy to implement. In fact, it is almost a copy-and-paste of the existing code. At the bottom of the update method, add the following code:

NSMutableDictionary* streaming_items_to_be_purged_dictionary =
    [[NSMutableDictionary alloc] initWithCapacity:[streamingSourcesDictionary count]];
  for(NSNumber* current_number in streamingSourcesDictionary)
  {
      ALuint source_id = [current_number unsignedIntValue];
      EWStreamBufferData* stream_buffer_data =
        [streamingSourcesDictionary objectForKey:current_number];
      BOOL finished_playing = [stream_buffer_data updateQueue:source_id];
      if(YES == finished_playing)
      {
         [streaming_items_to_be_purged_dictionary setObject:stream_buffer_data
         forKey:current_number];
      }
  }
  for(NSNumber* current_number in streaming_items_to_be_purged_dictionary)
  {
      [streamingSourcesDictionary removeObjectForKey:current_number];
      [self recycleSource:[current_number unsignedIntValue]];
      if([self.soundCallbackDelegate
         respondsToSelector:@selector(soundDidFinishPlaying:)])
      {
          [self.soundCallbackDelegate soundDidFinishPlaying:current_number];
      }
  }
  [streaming_items_to_be_purged_dictionary release];

As before with preloaded sources, we iterate through all the items in the collection and look for sources that have stopped playing. Notice that we use the return value of updateQueue: to determine if we stopped playing. We use this instead of querying the source directly because we don't want to accidentally interpret a buffer underrun condition as a finished playing situation. We already wrote all that logic in updateQueue:, and now we get to benefit from it.

Also notice that we kill two birds with one stone. In calling updateQueue: to find out if a source has finished playing, we are also updating the OpenAL buffer queue for the stream if it is not finished playing. (Don't you love how this is all coming together?)

And finally, when we do encounter a sound finished situation, we do what we did before: recycle the source and invoke our soundDidFinishedPlaying: delegate callback. Again, this all wonderfully coming together in an elegant manner. Even though we are talking about buffer streaming, it doesn't need to affect the behavior of the OpenAL sources. So when a source finishes playing, regardless of whether it is preloaded or loaded, you get (the same) callback notification. This means all our existing BBSceneObject code that listens for these callbacks doesn't need to change at all.

Now you can see why trying to deal with callbacks in our AVFoundation background music example would require some serious design decisions and possible significant code changes: AVFoundation callbacks don't match up with how our BBSceneObjects are using them.

Now that we're on a roll with respect to integration, there is one more easy thing we can try. Even though we have separate playSound: and playStream: methods, we can reuse the stopSound: method for both cases.

- (void) stopSound:(ALuint)source_id
{
   // Trusting the source_id passed in is valid
   alSourceStop(source_id);
   alSourcei(source_id, AL_BUFFER, AL_NONE); // detach the buffer from the source
   // Remove from the playingSourcesCollection or streamingSourcesDictionary,
   //no callback will be fired. Just try removing from both collections.
   //As far as I know, there is no problem trying to remove if it doesn't exist.
   [playingSourcesCollection removeObject:[NSNumber numberWithUnsignedInt:source_id]];
   [streamingSourcesDictionary removeObjectForKey:[NSNumber
      numberWithUnsignedInt:source_id]];
   [self recycleSource:source_id];
}

We are employing a trick here. Instead of first checking to see which collection the source is in, we just try to remove it from both. According to Apple's documentation on NSMutableDictionary, removing a key that does not exist does nothing. So it sounds like a safe operation. The documentation doesn't say anything about NSMutableSet, but I am going to assume they are consistent.

Also, notice this line:

alSourcei(source_id, AL_BUFFER, AL_NONE);

For preloaded sources, I said it was optional to detach the buffer, since we were just attaching new buffers when we needed the source again. But with buffer queuing, if we stop prematurely, we may still have queued buffers, and there is no API to remove those. Officially, from the spec, this line of code is how you clear all the buffers from a source.

Finally, as we leave OpenALSoundController, remember to change the audio session mode to kAudioSessionCategory_SoloAmbientSound in the init method in our call to InitAudioSession().

BBSceneController Integration

We're finally here. Now we get to try playing our music. Again, we will be using the music from FatLab Music, just as in the AVFoundation/Space Rocks! example.

Remember to add the #define to BBConfiguration.h:

// Music by Michael Shaieb
// © Copyright 2009 FatLab Music
// From "Snowferno" for iPhone/iPod Touch
#define BACKGROUND_MUSIC @"D-ay-Z-ray_mix_090502"

In BBSceneController, we are going to add two new instance variables:

EWStreamBufferData* backgroundMusicStreamBufferData;
ALuint backgroundMusicSourceID;

In the init method, let's load the sound file and tell it to loop:

backgroundMusicStreamBufferData = [[EWStreamBufferData alloc] initFromFileBaseName:BACKGROUND_MUSIC];
backgroundMusicStreamBufferData.audioLooping = YES;

In the startScene method, let's reserve a source and start playing. (We also set the music's gain level here.)

BOOL source_available = [[OpenALSoundController sharedSoundController] reserveSource:&backgroundMusicSourceID];
if(YES == source_available)
{
   [[OpenALSoundController sharedSoundController] setSourceGain:0.50
       sourceID:backgroundMusicSourceID];
   [[OpenALSoundController sharedSoundController] playStream:backgroundMusicSourceID
       streamBufferData:backgroundMusicStreamBufferData];
}
else
{
   NSLog(@"Unexpected Error: No AL source available for background music");
}

To clean up, in the dealloc method, we will stop playing and release the buffer.

[[OpenALSoundController sharedSoundController] stopSound:backgroundMusicSourceID];
[backgroundMusicStreamBufferData release];

That's it. You're ready to build and play. You can now pat yourself on the back. You have a fully capable OpenAL engine that can play sound effects and streams.

Analysis: Number of Buffers and Buffer Sizes

I promised to explain why we used 32 buffers and 16KB buffer sizes. This was a number I experimentally found to work well enough with Space Rocks! I don't claim this is optimal, but it is sufficient to get over one of our dangerous bottleneck points.

In our game engine design, we use NSTimer to trigger our game update loop, which the audio engine is tied to. The problem is that NSTimer operates on the main thread, so it can be blocked if we are busy doing other things. When you die and touch the game restart button, the engine reloads a whole bunch of things. This creates a bottleneck point.

On a first-generation iPhone, the reload time was sufficiently long that the music suffered a buffer underrun condition. While the game is loading, we are not able to queue more buffers, and with too few buffers, the audio system hit an underrun. This may be exacerbated by the fact that we don't guarantee the queue is always filled with the maximum number of buffers and it is a best-effort approach. On a first-generation iPhone, all the visual particle effects can tax the device pretty hard. Usually, the particle effects increase when the player dies, which may subsequently lead to starving the buffer queue because the CPU can't keep up with our desired update rate. By the time the game gets to the reload routine, there may be very few buffers remaining in the queue to endure the length of the reload routine time. Experimentally, I found 32 buffers at 16KB to be sufficient to be able to reload the game without encountering an underrun condition.[34]

Now 32 buffers at 16KB buffer sizes add up to arguably a lot of memory to use when the point of streaming was to minimize our memory footprint in a given moment.[35] When you begin memory tuning for your game, this should be one of the first areas to revisit. As a reference point to consider and shoot for, Daniel Peacock says he personally likes to use four buffers of 25 milliseconds. Buffer size is scaled based on the sample rate to fill 25 milliseconds.

There are other remedies you can try instead of increasing the number of buffers or sizes of the buffers. One approach is to identify the long blocking parts of your application and sprinkle some calls to queue more buffers throughout the routines. For example, in our reloading routine, we could try something aggressive and queue a buffer after every new BBRock we allocate.

Another solution is to use threads to update our OpenAL audio engine so it is not blocked. You could explicitly write your own multithreaded code. Alternatively. if you have access to another timer system like CADisplayLink on the iPhone or Core Video on the Mac (a high-priority timer running on an alternative thread that is guaranteed to fire in unison with your display's refresh rate), you could tie the OpenAL buffer updates to that. This is obviously a general statement to all OpenAL developers, not just iPhone developers, as this blocking problem may be an issue on all single-threaded engine designs. You will need to find a good system that works for you and your target platform(s).

And at the risk of stating the obvious, a very easy solution is to reduce the sample rate of your audio. A 44 kHz sample takes twice as much memory as a 22 kHz sample of the same duration. Another way to think of this is that you can fit twice as much play time in a single buffer using a 22 kHz sample as opposed to a 44 kHz sample.

Star Power Ready!

While we have just accomplished streaming (stereo) music, we've only scratched the surface of what the streaming part of our audio engine can do. So, we're not just going to leave it there. We will do one more short embellishment to give you an idea of what kind of audio power you now have.

OpenAL Speech for Space Rocks!

As I've said, streaming isn't just for music. And there is no reason to limit yourself to just one streaming source at a time.

We did a whole lot of work to set up OpenAL streaming. Now, let's reap the benefits. We will add a speech file to the game and spatialize that speech. You are going to see all the stuff we have done with OpenAL in the previous chapters come together in one little speech sound effect. Think of this as the grand finale!

This speech is going to be really cheesy. We'll have the little UFO taunt you with a "Game Over" message when you die (see Figure 12-8). Since the UFO moves (and we will be using a mono sound file), we just need to play the speech using the existing EWSoundSourceObject infrastructure we developed in the prior two chapters. In fact, we can just replace the UFO engine hum with the taunt and not even touch the spatialization code.

We add a new UFO to taunt using spatialized streaming speech.

Figure 12.8. We add a new UFO to taunt using spatialized streaming speech.

The speech is a 6-second sample. Although it's shorter than what streaming is typically used for, it works for demonstration purposes, and is large enough that multiple buffers are needed to play the file. And for an infrequently used sound, streaming is not an unreasonable option.

The new audio file is called gameover.caf. It sings, "Game over. You're dead. You lose. Do not pass go."[36] I compressed it using IMA4. Because the background music is already using the hardware decoder, we want to make sure to avoid overburdening the system with playing a second restricted format. To match all our other sample rates, this file is encoded at 22,050 Hz. And, of course, it is mono so it can be spatialized.

We will continue building on SpaceRocksOpenALStreaming1. The completed project for this example is SpaceRocksOpenALStreaming2.

Add the #define to BBConfiguration.h:

#define GAME_OVER_SPEECH @"gameover"

EWSoundSourceObject: Finishing the implementation

Next, we should add some missing infrastructure to EWSoundSourceObject. We currently have a playSound: method in the class, but not a playStream: method. Let's add it.

- (BOOL) playStream:(EWStreamBufferData*)stream_buffer_data
{
   OpenALSoundController* sound_controller = [OpenALSoundController
      sharedSoundController];
   if(sound_controller.inInterruption)
   {
      NSInvocation* an_invocation =
         CreateAutoreleasedInvocation(self,@selector(playStream:),
         stream_buffer_data, nil);
      [sound_controller queueEvent:an_invocation];
      // Yes or No?
      return YES;
    }
    else
    {
       ALuint source_id;
       BOOL is_source_available = [sound_controller reserveSource:&source_id];
       if(NO == is_source_available)
       {
          return NO;
       }

       self.sourceID = source_id;
       self.hasSourceID = YES;
       [self applyState];
       [sound_controller playStream:source_id streamBufferData:stream_buffer_data];
    }
    return YES;
}

This code is essentially a copy-and-paste from playSound:, but is a little simpler, because we pass the buck and let the OpenALSoundController figure out how to attach and queue the buffers.

Now we're finished filling in the missing infrastructure. Wow, that was too easy.

BBSceneController: Adding a New UFO

Next, let's modify BBSceneController to create a taunting UFO on player death. We will change the gameOver method to add two new lines:

-(void)gameOver
{
   UFOCountDown = RANDOM_INT(500,800);
   [self addTauntUFO];
   [inputController gameOver];
}

First, we reset the UFOCountDown timer, because we want to minimize the chance of having two UFOs at the same time. Since the UFO has a noisy engine, we would like to increase the chances of hearing the speech clearly. (You might wait for the first UFO to pass before dying.) For simplicity, if there is a UFO already in the scene, we leave it alone. There is a small probability that the existing UFO could destroy our new taunting UFO with its missiles, but we're not going to worry about that. Consider it an Easter Egg.

Next, we call a new method called addTauntUFO to create the UFO.

-(void)addTauntUFO
{
   BBUFO * ufo = [[BBUFO alloc] init];
   // The UFO starts in the upper left and moves to the right
   ufo.translation = BBPointMake(-270.0, 60.0, 0.0);
   ufo.speed = BBPointMake(50.0, 0.0, 0.0);
   ufo.scale = BBPointMake(30, 30, 30.0);
   ufo.rotation = BBPointMake(-20.0, 0.0, 0.0);
   ufo.rotationalSpeed = BBPointMake(0.0, 0.0, 50.0);
   ufo.shouldTaunt = YES;
   [self addObjectToScene:ufo];
   [ufo release];
}

This is very similar to addUFO. We just change the speed and rotation for distinctiveness, and adjust the y position so it is lower on the screen. This is to avoid overlapping with the regular UFO if it is on the screen. We also set a new property called shouldTaunt to YES, placing it before addObjectToScene:. We want this property set before the UFO's awake method is called so the correct audio sample will be used.

And that's all we need to change in BBSceneController. This is way too easy. Now we only need to modify BBUFO to taunt.

UFO: Taunting and Callbacks

We will add the new Boolean property, shouldTaunt, to BBUFO. While we're here, let's add an ALuint for the tauntID. We will use this to save the OpenAL source ID that we are playing the taunt on for later use with a callback.

@interface BBUFO : BBMobileObject {
   BBParticleSystem * particleEmitter;
   NSInteger missileCountDown;
   BOOL destroyed;
   BOOL shouldTaunt;
   ALuint tauntID;
}
@property(nonatomic, assign) BOOL shouldTaunt;

After synthesizing the shouldTaunt property in the implementation, we modify the awake method to conditionally set the proper sound effect. If we are taunting, we load the gameover file as a stream. If we are not taunting, we load the engine sound as usual. (We could add a second source to the UFO to play both, but the engine is noisy, so this is fine.)

I want to reiterate that we could do this without touching a single 3D audio property, but the existing directional cone is going to make hearing the speech kind of difficult. So as a special case, we will disable the directional cones for taunting. (Perhaps it would have been better to subclass the UFO, but this is meant to be just a quick hack for demonstration purposes.)

if(YES == self.shouldTaunt)
        {
           self.soundSourceObject.coneInnerAngle = 360.0;
           self.soundSourceObject.coneOuterAngle = 360.0;
           self.soundSourceObject.coneOuterGain = 0.0;
           self.soundSourceObject.rolloffFactor = 0.5;
           self.soundSourceObject.referenceDistance = 300.0;

           self.soundSourceObject.audioLooping = AL_FALSE;
           self.soundSourceObject.gainLevel = 1.0;

           EWStreamBufferData* game_over_speech =
             [[OpenALSoundController sharedSoundController]
             streamBufferDataFromFileBaseName:GAME_OVER_SPEECH];
           [self.soundSourceObject playStream:game_over_speech];
           tauntID = self.soundSourceObject.sourceID;
        }
        else
        {
           self.soundSourceObject.coneInnerAngle = 90.0;
           self.soundSourceObject.coneOuterAngle = 270.0;
           self.soundSourceObject.coneOuterGain = 0.50;
           self.soundSourceObject.rolloffFactor = 0.5;
           self.soundSourceObject.referenceDistance = 300.0;

           self.soundSourceObject.audioLooping = AL_TRUE;
           self.soundSourceObject.gainLevel = 0.3; // let's lower sound it's too loud
          [self.soundSourceObject playSound:
[[OpenALSoundController sharedSoundController]
             soundBufferDataFromFileBaseName:UFO_ENGINE]];
   }

With streaming, we need to remember to always turn off looping on the source, because we do looping in the buffer. In this case, we aren't looping at all, so both need to be off. We load the file and play it. And we save the source ID in tauntID. We release the newly created buffer because we don't need it anymore. (The system will retain it as long as it is playing.)

In the dealloc method, let's make sure we stop the taunt. We could let it play to completion, but it makes more sense to kill the taunt if the player starts a new game.

if(AL_TRUE == self.soundSourceObject.audioLooping)
  {
     [self.soundSourceObject stopSound];
  }
  // Additional check needed for taunt mode. Let's kill the speech if we are resetting
  else if(YES == self.shouldTaunt)
  {
     [self.soundSourceObject stopSound];
  }

And that's it. We have a UFO that will taunt us! And this streaming speech will have all the same 3D effects that we applied to the engine hum. You can even hear the Doppler effect mess up the singing.

But let's now put the cherry on top. We will use the callback feature that we spent so much effort building in Chapter 10. When the UFO is finished speaking, we will do something very explicit so we know everything is working. We will have the UFO fire off a four-missile salvo to show off (see Figure 12-9). This will prove the callback system works!

Note

In the last moments of this book production, Ben changed the artwork for the UFO missile at my request to make it more distinct for example purposes. So the image in Figure 12-9 is a little different from what you will see when running the program.

Using the callback system we implemented in Chapter 10, we instruct the UFO to fire a four-missile salvo after the taunt.

Figure 12.9. Using the callback system we implemented in Chapter 10, we instruct the UFO to fire a four-missile salvo after the taunt.

Let's implement the sound callback.

- (void) soundDidFinishPlaying:(NSNumber*)source_number
{
   [super soundDidFinishPlaying:source_number];

   if(YES == self.shouldTaunt && [source_number unsignedIntValue] == tauntID)
   {
      tauntID = 0;
      [self fireMissile];
      [self fireMissile];
      [self fireMissile];
      [self fireMissile];
   }
}

If we are taunting and get back the tauntID we saved, that means the source finished playing the taunt.

Build and run the game. With luck, everything will just work. Congratulations! You have completed the entire OpenAL engine. Yes, that was a lot of work, but you have everything! You have resource management, callbacks, 3D effects, and streaming—all working in tandem. In my opinion, this is much better than what we had in the AVFoundation music integration example. And the Audio Queue Services solution discussed in the next section will have the same impedance mismatch as AVFoundation.

The alBufferDataStatic Crashing Bug

We have been using Apple's alBufferDataStatic extension, introduced in Chapter 10, to make audio streamingmore efficient, but it has a nonobvious behavior that cancause your application to crash if you're not careful to work around it.

To refresh your memory, the alBufferDataStatic extension works by allowing you to provide a pointer to the raw PCM data for your audio for an OpenAL buffer. This is in contrast to the standard alBufferData command, which will copy the PCM data to the OpenAL buffer. The alBufferDataStatic extension gives an edge in performance because you avoid the memory copy. But as a trade-off, you must manage the memory for the PCM data yourself.

Managing the memory yourself entails two simple concepts:

  • Remember to release the memory when you are completely finished with it so your program doesn't have any memory leaks.

  • Do not destroy (or modify) the data while some other part of the system may be accessing it.

Let's begin with a basic example. We start playing a sound in OpenAL using a buffer created from the alBufferDataStatic extension. If we try to free the raw PCM buffer while OpenAL is still playing this sound, we will likely crash our program. So obviously, we shouldn't free our PCM buffers while OpenAL is using them. This seems easy enough.

Let's extend the example. As we just did in the previous example with the taunting UFO, we start playing a buffer and poll OpenAL until it stops playing (or explicitly stop the sound ourselves). When OpenAL tells us the AL_SOURCE_STATE is AL_STOPPED, we detach the OpenAL buffer from the source, delete the OpenAL buffer using alDeleteBuffers(), and then free the raw PCM buffer. On the surface, this seems reasonable and should work. But there is an implementation detail that we must contend with. Apple's OpenAL implementation may still be using the raw PCM buffer under the hood, even though it seemingly appears the system is done with the buffer. And if Apple is still using the buffer, our program will likely crash. Unfortunately, the UFO taunting example has created the perfect storm for this race condition.

Why did this race condition appear only now, in the UFO taunting example? Well, this is the first time we delete buffers immediately after they finish playing. Please be aware that this is not a bug specific to streaming. This could happen with preloaded sounds, too. With our preloaded sounds from Chapter 10 and our streaming background music, we didn't delete the buffers until shutdown, and the race condition didn't occur in those situations.

Note

In my opinion, the alBufferDataStatic extension crashing problem is an Apple bug. If the OpenAL source has stopped playing (either by natural completion or by calling alSourceStop() explicitly), and the AL_SOURCE_STATE says it is AL_STOPPED, you wouldn't expect Apple to be still using the buffer. However, Apple contests my labeling of this as a bug and considers it the proper, expected behavior.

Apple's response to dealing with this issue is to check for an OpenAL error after you call alDeleteBuffers(), but before you free the PCM data. Apple claims to have overloaded the error mechanism for this case. Officially, the only thing the spec says is that alDeleteBuffers returns the error AL_INVALID_NAME. So Apple says it added a special case to let you know that the deletion of the buffer has failed, which you should use as an indicator of whether it is actually safe to free the PCM data. Unfortunately, this doesn't seem to work for me.

So you can experience this yourself, let's add it to Space Rocks! (These code changes are included as part of SpaceRocksOpenALStreaming2.) In EWStreamBufferData.m, modify the destroyBuffers method to look like the following:

- (void) destroyOpenALBuffers
{
   ALenum al_error = alGetError(); // clear errors
   [availableDataBuffersQueue release];
   [queuedDataBuffersQueue release];

   alDeleteBuffers(EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS,
      openalDataBufferArray);
   al_error = alGetError();
   if(AL_NO_ERROR != al_error)
   {
      NSLog(@"EWStreamBufferData alDeleteBuffers error: %s", alGetString(al_error));
   }
#ifdef USE_BUFFER_DATA_STATIC_EXTENSION_FOR_STREAM
   for(NSUInteger i=0; i<EW_STREAM_BUFFER_DATA_MAX_OPENAL_QUEUE_BUFFERS; i++)
   {
       if(NULL != pcmDataBufferArray[i])
       {
          free(pcmDataBufferArray[i]);
          pcmDataBufferArray[i] = NULL;
       }
   }
#else
  if(NULL != pcmDataBuffer)
  {
     free(pcmDataBuffer);
     pcmDataBuffer = NULL;
        }
#endif
}

There are two things to notice:

  • We added an alGetError() check after alDeleteBuffers(). If we encounter an error, we call NSLog() to report it. The code then continues as if nothing happened. In reality, we should do something special to avoid calling free() on the PCM buffer, but since I have never once seen this print an error when the program crashes, there doesn't seem to be much point in complicating the example.

  • There is a new preprocessor macro named USE_BUFFER_DATA_STATIC_EXTENSION_FOR_STREAM to allow us to switch between using the alBufferDataStatic extension and disabling it. This macro is defined in EWStreamBufferData.h. Because I didn't want you to experience a crash the first time you ran this program, I have commented out the line in the code that accompanies this book.

To try to reproduce this problem, you should reactivate the line and recompile. Then run the game and destroy your ship to summon the taunting UFO. When the UFO stops speaking, the crashing bug has a chance of occurring. You may also tap the Try Again button while the UFO is in the middle of the taunt. This will immediately stop the playback and run the same cleanup code.[37] For me, doing this creates a fairly reliable reproducible demonstration of the bug (i.e., over 50% crash rate).

Work-Arounds

In the absence of alGetError() telling us anything useful, we must devise our own work-arounds. Since this is a race condition problem, a solution is to increase the amount of time between when the sound supposedly stops playing and when the buffer is deleted. A naive approach would be to add some commands that waste some time before we delete the buffer. For example, a simple call to sleep() or usleep() at the beginning of the destroyOpenALBuffers method might be sufficient for most cases. You will need to experimentally find the shortest amount of time that consistently avoids the crash. If you have more useful commands you can run instead of sleeping, that would be better. The time probably doesn't need to be long. I found that just adding a few NSLog() statements to debug this block of code increases the execution time enough to make a significant difference between crashing and not crashing. But the downside to this solution is that there are no guarantees that the delay time you pick will always be long enough. And if you pick too long of a time, the player may notice the delay while playing the game.

A less naive approach to increase the time between stop and deletion is to add another layer of indirection and create an event queue holding the buffers that need to be deleted at some future point. You could then revisit this queue at some arbitrarily long time later. When you get around to revisiting those buffers, you need to call alDeleteBuffers() again and check for an error again. If there is an error, you need to keep the buffers in the queue and try again later. Otherwise, you can finally delete the PCM buffer. There are two downsides to this solution. First, there is a lot more complexity. Second, if you are tight on memory, the resources waiting in the queue will not be useful to you until you finally free a queue item. However, this is probably the best work-around.

Finally, at the risk of stating the obvious, you could avoid using the extension, either entirely or just in cases where it is likely to bite you. For example. this race condition bug was not really an issue with just the sound effects and background music, so maybe you would single out short-lived sounds like the UFO taunting.

Note

As I pointed out earlier, Creative Labs also supports the alBufferDataStatic extension for its Xbox and Xbox 360 implementations. Though I have not personally tested these implementations, I have been informed that their XAudio- and XAudio 2-based implementations do not suffer from the race condition as just described for Apple's implementation. So our original design implementation should just work as expected, and all the work-arounds we talked about in this section are irrelevant.

Audio Queue Services Based Background Music for Space Rocks!

Even though we have just completed the entire audio engine with OpenAL, we're going to take a step back and take a look at a third potential API you can use: Audio Queue Services. With the introduction of AVFoundation and the fact that you can accomplish streaming through OpenAL, the case for using Audio Queue Services has grown weaker as of late. But there are features and conveniences that Audio Queue Services provides that the other two do not. Also, Audio Queue Services is currently the only way to access the full power of audio capturing, which is covered later in this chapter. Here, I will give you a brief introduction and example of using Audio Queue Services for background music.

According to Apple, Audio Queue Services is a "high-level" API for playing and recording audio. But according to most Cocoa developers, what is considered high level by the Core Audio team is typically infuriating and frustrating. (AVFoundation is the first API to ever buck that trend.) Audio Queue Services provides a C API, and using it is roughly about the same level of difficulty as what you just saw with OpenAL buffer queuing. In fact, it is pretty much the same design in that you are periodically unqueuing and queuing new buffers.

Perhaps the most significant difference between Audio Queue Services and OpenAL buffer queuing is that Audio Queue Services is callback-driven, whereas you need to poll in OpenAL. This means you get a nice, clean event to provide more data, instead of constantly asking the system if a queue has been processed. This also has an advantage in that there are fewer blocking issues. As you saw in our OpenAL example, if we were blocked from running our update loop (for example because we were preoccupied restarting the level and not running the update loop), then we risked starving the audio (buffer underrun). Audio Queue Services runs the callback on a separate thread, so blocking is not really an issue.[39]

As with all the Core Audio APIs, there is a lot of setting and getting of properties through generic functions and opaque types, which adds to the tedium of writing the code, but nothing you can't handle. So, if you are not already using OpenAL in your application and AVFoundation is not suitable, you might consider using Audio Queue Services. The callback-driven design might make things simpler to write. Also, Audio Queue Services has some fine-grained timing and synchronization features you can investigate if you're interested.

Apple has a pretty good example of how to use Audio Queue Services for playback in the "Playing Audio" section of the Audio Queue Services Programming Guide, found here:

http://developer.apple.com/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW2

To give you a working project that you can play with, I have taken this example and integrated it in our Space Rocks! code. (This version does not include our changes for AVFoundation or OpenAL streaming.) I have left Apple's line-identifier comments intact, plus added explicit "Listing" markers so you can easily cross-reference with the Apple documentation.

Once again, this project is based on SpaceRocksOpenAL3D_6_SourceRelative from Chapter 11. The completed project for this example is SpaceRocksAudioQueueServices. See AudioQueueServicesController.h and AudioQueueServicesController.m for the code specific to Audio Queue Services.

We do need to make a few changes in this project:[40]

  • A bit of refactoring was necessary to encapsulate it into a nice class for easy reuse in the game.

  • The original example runs its own infinite run loop. This wasn't going to cut it, since we have a game loop to run, so this is changed to work with our application.

  • We loop the audio, rather than letting it end, since we are going to use it for background music. (It might have been better to make looping an option, but I didn't want to overcomplicate the changes, and we would need to start thinking about callback notifications for sound finishing.)

  • We add interruption support.

Let's examine two of the changes in a bit more detail: looping and interruptions.

Rewind and Looping

In the HandleOutputBuffer callback, the critical change is to reset the mCurrentPacket to 0. Being even more explicit, the fifth parameter to AudioFileReadPackets tells the function which packet to start reading from. In the rewind case, it is 0.

// New rewind code
pAqData->mCurrentPacket = 0; // reset counter
UInt32 numPackets = pAqData->mNumPacketsToRead;
   result = AudioFileReadPackets(
   pAqData->mAudioFile,
   false,
   &numBytesReadFromFile,
   pAqData->mPacketDescs,
   0,  // start at the beginning (packet #0)
   &numPackets,
   inBuffer->mAudioData);

Audio Session Interruptions

Apple talks about interruptions in Technical Q&A QA1558, found here:

http://developer.apple.com/iphone/library/qa/qa2008/qa1558.html

The crux is that starting in iPhone OS 3.0, audio queues are paused automatically in an interruption, and you just need to resume (if appropriate) on the endInterruption event. But there is a major caveat. If you are using the hardware decoder, all bets are off, because the hardware decoder may not be able to restore its state for ambiguous reasons. Since we are using the hardware decoder for our music, we can't rely on this feature.[41] Thus, we must do it the pre-3.0 way by completely tearing down the audio queue on interruption and reloading it on endInterruption.

To accomplish the complete teardown and then restore the state, we must do the following:

  • Stop the audio queue.

  • Save the current mCurrentPacket value.

  • Close the audio queue and file, and then clean up memory.

Then on endInterruption, we create a new audio queue using the same file.[42] We remember to set the mCurrentPacket to our saved value, and then start playing the queue. See the methods beginInterruption and endInterruption in AudioQueueServicesController.m.

A special change we need to make for Space Rocks! to handle interruptions is where we place the callback function. Originally, we had the MyInterruptionCallback function inside OpenALSoundController. The problem is that we need to handle interruptions for both OpenAL and audio queues, but they are in separate files that don't know about each other. So we move the callback and audio session initialization code to the BBSceneController.m file, since our scene controller needs to talk to both of these files to play sound and music. Note that we didn't have this problem with our AVFoundation/Space Rocks! integration because AVAudioPlayer has its own delegate callbacks that are invoked on interruption.

For more information about handling interruptions, see Apple's documentation entitled "Handling Audio Interruptions" which is part of the Audio Sessions Programming Guide, found here:

http://developer.apple.com/iphone/library/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/HandlingAudioInterruptions/HandlingAudioInterruptions.html#//apple_ref/doc/uid/TP40007875-CH11-SW11

Perfect Full Combo!

Marvelous! You have seen how to implement streaming playback in AVFoundation, OpenAL, and Audio Queue Services. It's now up to you to decide which API to use, based on features and simplicity that make the most sense for your own projects.

Incidentally, this marks the end of using Space Rocks! as an example for the book. But the fun isn't quite over yet. We are now going to move from playing back (streaming) sound to capturing sound.

Audio Capture

We are going to talk a little about reading (capturing) data from an audio input device such as a microphone. This could involve recording audio to save or archive of audio to some medium, such as a tape or file. But it also applies in cases when you don't want to save the data. You may just want to use it immediately as it comes in and throw it away. Maybe you are just forwarding the information over the network. Maybe you are using it to seed a random-number generator. Maybe you just want to detect the current frequency for a guitar tuner type app. Maybe you just want to measure the current amplitude for a decibel meter app. None of these examples require you to archive the data.

Of course, not all iPhone OS devices have microphone support. The first-generation iPod touch completely lacks a microphone, and the second-generation iPod touch has only an external add-on microphone. However, there are some really clever newer iPhone/iPod touch games that use microphone support. One such example is Ocarina by Smule, a clever app that transforms your iPhone into a musical instrument. This app has four virtual buttons you touch on the screen to simulate finger holes, and you blow into the microphone to play a musical note (see Figures 12-10 and 12-11).

The virtual music instrument app, Ocarina by Smule, makes interesting use of audio capture. Touch the virtual finger holes and blow into the microphone to generate a musical note.

Figure 12.10. The virtual music instrument app, Ocarina by Smule, makes interesting use of audio capture. Touch the virtual finger holes and blow into the microphone to generate a musical note.

The instruction page for Ocarina

Figure 12.11. The instruction page for Ocarina

Here, I will give a brief overview of the capture APIs on iPhone OS, and then present a couple examples.

Audio Capture APIs

The same three frameworks that give us streaming playback also provide (streaming) capture:

  • AVFoundation: AVFoundation introduced AVAudioRecorder as part of the API in 3.0. It provides a simple API in Objective-C for recording, as AVAudioPlayer does for playing. Unfortunately, this class has somewhat limited utility for games as it stands. Currently, it is geared toward recording to an audio file. So rather than pulling buffers into RAM, which you can immediately use, the buffers are written to a file. If you want to get at the data, you will need to open that file. We will run through a quick AVFoundation capturing example in the next section.

  • OpenAL: OpenAL includes a capture API as part of the 1.1 spec. But here's the bad news: As of iPhone OS 3.1.2, Apple still has not finished implementing it. This means you cannot currently use OpenAL to capture your audio. Technically speaking, Apple's implementation does conform to the OpenAL 1.1 spec in that it provides the appropriate API functions in the headers and library, so you can compile and link your application successfully. But alas, when you try to actually open a capture device while running your program, the operation fails. Still, I feel you should have access to an example on how to use OpenAL capture, so I have implemented one (actually two). We will run through this example at the end of this chapter.

    Note

    I don't know if or when Apple will finish implementing OpenAL capture. If you want it, I strongly recommend you file a bug report on it. (Apple counts how many people want a feature and uses that to help determine priorities, so duplicate bug reports are good.) I am personally optimistic Apple will finish implementing OpenAL capture support, but this not based on any special information. So, if you have an important project with a hard deadline, I wouldn't advise waiting for this feature.

  • Audio Queue Services: If you need general-purpose capture support that can deal with buffers instead of just files, then Audio Queue Services is currently the only game in town[43] (until Apple finishes implementing OpenAL capture). Using Audio Queue Services for recording is very similar to playing, which was demonstrated in an example earlier in this chapter. Apple's Audio Queue Services Programming Guide contains a step-by-step guide for recording. So using your experience from the earlier section, you should be able to learn recording fairly easily. Also, you can refer to Apple's official iPhone SDK example called SpeakHere, which uses audio queues for recording.

Note

Apple's SpeakHere example uses Objective-C++, which is basically C++ and Objective-C mixed together in peaceful coexistence. (File extensions are .mm.) If you don't know C++, don't worry too much. You should be able to make it through the example as the C++isms aren't too extreme. You might be wondering why Apple would do this for an instructional example, which really didn't need C++, meant for iPhone developers who are being encouraged to develop in C and Objective-C. I don't know the answer. But Cocoa developers will add this as another bullet point to their long list of grievances with the Core Audio group through the years. The Core Audio group definitely sticks out like a sore thumb at Apple, as it is one of the few groups to post public example code in C++. Incidentally, if you ever dig through the open source, Apple's OpenAL implementation is written in C++, even though it is publicly a C API. Presumably, the Core Audio implementation is also written in C++, though this is all private, behind-the-scenes stuff that doesn't affect us since we don't access the source code.

AVFoundation: File Recording with AVAudioRecorder

Our project is named AVRecorder. Structurally, this project looks very much like the AVPlayback project example in Chapter 9. This example will have an AVAudioRecorder instance to record audio to a file, and an AVAudioPlayer instance to play back that file. We will have a simple UI that contains two buttons: a record button and a play button (see Figure 12-12). To avoid having to draw art assets, we will use the camera button to represent the record button (I'm not going to win any design awards, for sure). For convenience, all the AVFoundation-related code will be restricted to one file, AVRecorderSoundController. The UI logic is restricted to the other files. Since you already know how to use AVAudioPlayer, we will focus on AVAudioRecorder.

To start with, we need to know if an input device is available. Audio Session Services provides a way to query this, and AVAudioSession wraps it in an Objective-C API. In our demo, we put up a simple UIAlertView to notify the user if we could not find an input device.

if(NO == [[AVAudioSession sharedInstance] inputIsAvailable])
   {
      NSLog(@"%@", NSLocalizedString(@"No input device found", @"No input device
        found"));

      UIAlertView* alert_view = [[UIAlertView alloc]
         initWithTitle:NSLocalizedString(@"No input device found",
           @"No input device found")
         message:NSLocalizedString(@"We could not detect an input device.
           If you have an external microphone, plug it in.", @"Plug in your microphone")
         delegate:nil
         cancelButtonTitle:NSLocalizedString(@"OK", @"OK")
       otherButtonTitles:nil
];
      [alert_view show];
      [alert_view release];
   }
Our recording example project, AVRecorder

Figure 12.12. Our recording example project, AVRecorder

In addition, we can listen for device changes using Audio Session Services. AVAudioSession provides a nice delegate callback to let us know the input device status has changed (i.e., someone plugged in or unplugged an external microphone).

- (void) inputIsAvailableChanged:(BOOL)is_input_available
{
   NSLog(@"AVAudioSession inputIsAvailableChanged:%d", is_input_available);
}

Speaking of delegate callbacks, AVAudioRecorder defines several callback methods, such as audioRecorderDidFinishRecording: and the usual stuff for interruptions. We make our class conform to this protocol and set the delegate to self in our example.

@interface AVRecorderSoundController : NSObject <AVAudioSessionDelegate, AVAudioPlayerDelegate, AVAudioRecorderDelegate>

Next, there is a slight change in how we approach initializing an audio session in this example. In all our other examples, we set the audio session at the beginning of the program and never touched it again. In this example, we switch the audio session between recording and playback mode, depending on which we want to do at the moment. We also deactivate the audio session when our application is idle for good measure.

So when we are about to record, we set the following:

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];

And when we are about to play, we set this:

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];

When idle, we disable the audio session:

[[AVAudioSession sharedInstance] setActive:NO error:nil];

For this example, we create a temporary file to store our recording. We make use of the Cocoa function NSTemporaryDirectory() to help us find the temporary directory to write to:

NSString* temp_dir = NSTemporaryDirectory();
NSString* recording_file_path = [temp_dir stringByAppendingString: @"audio_recording.caf"];
NSURL* recording_file_url = [[NSURL alloc] initFileURLWithPath:recording_file_path];

To actually create the AVAudioRecorder, we just do this:

avRecorder = [[AVAudioRecorder alloc] initWithURL:recording_file_url
      settings:nil
      error:nil
    ];

Optionally, you can provide an NSDictionary containing settings for the recorder. There are a bunch of different options. Some of the more interesting ones are AVFormatIDKey, which lets you specify the compression format; AVEncoderAudioQualityKey, which specifies the quality level in terms of high, medium, low, and so on; and AVSampleRateKey, which dictates the sample rate.

Here's an example dictionary:

NSDictionary* record_settings = [[NSDictionary alloc]
  initWithObjectsAndKeys:
      [NSNumber numberWithDouble:8000.0], AVSampleRateKey,
      [NSNumber numberWithInt:kAudioFormatAppleLossless],
         AVFormatIDKey, // needs CoreAudioTypes.h
      [NSNumber numberWithInt:1], AVNumberOfChannelsKey, // mono
      [NSNumber numberWithInt:AVAudioQualityMax], AVEncoderAudioQualityKey,
      nil
];

There is a caveat about the sample rate. Your physical microphone may be much less capable than the sample rate you request. For example, the first-generation iPhone's built-in microphone can go up to only 8000 Hz. If you request 44,100 Hz, AVAudioRecorder might up-sample to meet your request, but you won't get any better quality, just a larger file.

If you want more information or control over your devices, Audio Session Services provides two properties that might be useful, which AVAudioSession also wraps:

  • CurrentHardwareSampleRate: This is a read-only value, and will tell you the current rate of your device. One twist is that it is context-sensitive. If you are in recording mode, it will tell you about the input device. If you are in playback mode, it will tell you about the output device.

  • PreferredHardwareSampleRate: You can set this if you want to try changing the rate. If you query this value, there is a possibility it will return 0.0, so be prepared for that.

According to Technical Q&A QA1631 (http://developer.apple.com/iphone/library/qa/qa2008/qa1631.html), you should deal with PreferredHardwareSampleRate when the audio session is not active. Conversely, you should query CurrentHardwareSampleRate when the audio session is active.

To record, you do this:

[self.avRecorder record];

Optionally, you can call the method prepareToRecord before recording. This will create the file for writing beforehand and try to do things to minimize latency when you do finally invoke record.

To stop recording, do this:

[self.avRecorder stop];

Interruptions will automatically pause your recording. On endInterruption, you can resume by calling record again.

That is AVAudioRecorder in a nutshell. It is a pretty easy-to-use API and closely mirrors AVAudioPlayer, so you shouldn't have any trouble learning it.

OpenAL: Capture Oscilloscope

So we're finally here. This is the last OpenAL (and last audio) example for the book. The example is an oscilloscope. It captures the data from the microphone and dumps it to the screen using OpenGL. In theory, if/when Apple finishes capture support, this example should just work.[44] See the project OpenALCaptureiPhone.

Meanwhile, since the example isn't a lot of fun when it doesn't work, I have ported it to Mac OS X (see Figure 12-13), in the project named OpenALCaptureMac. The OpenAL code is the same, with the slight exception of the setup, which needs to set up the audio session on iPhone OS. The OpenGL setup code is a little different, since I use NSOpenGLView instead of EAGLView.[45] And for interest, I demonstrate how to switch over from using NSTimer to Core Video's display link. On Mac OS X, the Core Video display link is a better way to produce timer callbacks, as the display link is guaranteed to fire in unison with your display's refresh rate. NSTimer is problematic because it may drift.

Note

Those on iPhone OS should take notice that I just bashed how all the code in this book works, as we use NSTimer. In my first draft of this chapter, I resulted to dropping a lot of hints (without violating the NDA) encouraging you to look at the Mac OS X display link code, believing it was inevitable that a comparable technology would come to iPhone OS. Since then, Apple has released iPhone OS 3.1, which does contain an analogous technology called CADisplayLink. So I can now drop the pretense and tell you to go use it. I have ported the OpenALCaptureiPhone project to use it. (You may have noticed I also snuck in the CADisplayLink code to the BasicOpenALStreaming example. (Unfortunately, Apple's 3.1 release came too late for us to integrate this into the Space Rocks! game.) Apple's new default OpenGL templates also demonstrate it.

OpenAL capture oscilloscope example for Mac OS X (because iPhone OS does not yet support OpenAL capture)

Figure 12.13. OpenAL capture oscilloscope example for Mac OS X (because iPhone OS does not yet support OpenAL capture)

The Capture APIs

The OpenAL Capture API is pretty small. It consists of five functions:

ALCdevice* alcCaptureOpenDevice(const ALCchar* device_name, ALCuint sample_rate,
ALCenum al_format, ALCsizei buffer_size);
ALCboolean alcCaptureCloseDevice(ALCdevice* device_name);
void alcCaptureStart(ALCdevice* device_name);
void alcCaptureStop(ALCdevice* device_name);
void alcCaptureSamples(ALCdevice* device_name, ALCvoid* data_buffer, ALCsizei
  number_of_samples);

Open, close, start, and stop should be pretty obvious. The remaining function, alcCaptureSamples, is how you get PCM data back from the input device.

ALC Device Enumeration

To start with, you might consider checking to see if a capture device actually exists. You can use Audio Session Services to tell you this on iPhone, as in the preceding example. With pure OpenAL, you could just try opening the device, which might be the best way of doing it. You can also try the ALC enumeration system, which lets you get a list of devices and find out the name of the default device. There are separate flags for getting input devices and output devices.

Note

The ALC enumeration system is still sometimes referred to as the Enumeration extension, but the OpenAL 1.1 spec formally adopted it and requires this to be supported. Incidentally, OpenAL Capture is in the same boat and is sometimes still called the Capture extension. But OpenAL Capture was officially adopted in the 1.1 spec, so it is no longer just an extension.

To be overzealous, you can check for the extension anyway (using the OpenAL extension mechanism). Then you can get the device name and list of devices. However, on iPhone OS as of this writing, the Enumeration extension seems to be broken. The extension returns true, but you get no device names back. (It fails on output devices, too.) So, you are better off just trying to open the device. But for interest, because dealing with a double NULL-terminated C array for string lists is probably something a lot of people aren't used to seeing, here is some sample code:

if(alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT") == AL_TRUE)

{
    // Enumeration extension found
    printf("ALC_ENUMERATION_EXT available");

    const ALCchar* list_of_devices;
    const ALCchar* default_device_name;

    // Pass in NULL device handle to get list of devices
    default_device_name = alcGetString(NULL, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER);

    // Devices contains the device names, separated by NULL
    // and terminated by two consecutive NULLs
    list_of_devices = alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER);

    printf("Default capture device is %s
", default_device_name);
    const ALCchar* device_string_walk = list_of_devices;
    int device_num = 0;
    do
    {

       printf(" * Capture Device %d: [%s].
", device_num, device_string_walk);
       device_string_walk += strlen(device_string_walk)+1;
       device_num++;

    } while(device_string_walk[0] != ''),
}

Capturing Audio

To actually open the device, we do this:[46]

alCaptureDevice = alcCaptureOpenDevice(NULL, 22050, AL_FORMAT_MONO16, 32768);

The first parameter is a C string containing the device name, which can be obtained from alcGetString(), as shown in the previous code snippet. Or we can just pass NULL to open the default capture device as we do here.

The second parameter is the sample rate. We use 22 kHz here. (Optionally, you could query the currentHardwareSampleRate, as discussed earlier.) If the rate is higher than what your hardware can support, OpenAL generally up-samples.

The third parameter is the data format. We are telling it we want to get back the data as 16-bit mono. Other options include 8-bit and stereo. You've seen these same data formats before when dealing with alBufferData.

Note

A data format extension supported by many OpenAL implementations is called AL_EXT_float32, which means implementations can support 32-bit floating-point samples. If your implementation supports it, you can get the format types for 32-bit floating-point mono and 32-bit floating-point stereo. Technically, the spec allows the capture device opening to fail with "supported" extension formats. The reason is that the extension may be supported only for output. But to my knowledge, most implementations that support the float extension support it with capture as well. Unfortunately, iPhone OS currently does not support this extension.

The last parameter is how large of a buffer you want OpenAL to reserve for holding audio data. The larger the buffer, the larger the window of samples you get to see. Note that with higher sample rates and larger data formats, you need a larger buffer to hold the same number of samples as you would with lower rates and smaller formats. For simplicity, I somewhat arbitrarily pick 32KB here.

So, if we don't get back NULL from this call, we have an open device. Unlike with OpenAL output, we don't set up a context. OpenAL capture does not have a context. In fact, the three OpenAL object types—sources, buffers, and listeners—do not make an appearance in the OpenAL Capture API. You deal with the device and PCM buffers directly. (If you want to take the PCM buffer and play it, then you can give it to an OpenAL buffer and play it on a source.)

Once we are ready to start capturing samples, we call the following method:

alcCaptureStart(alCaptureDevice);

Once again, we need to remember that OpenAL has a polling-oriented design. That means we need to constantly check if OpenAL has gotten more input data. In our update loop, we query OpenAL to find out how many samples it has collected (see OpenALCaptureController.m's dataArray:maxArrayLength:getBytesPerSample). If we want the samples, we can retrieve them.

To find out how many samples OpenAL has collected, we do this:

ALCint number_of_samples = 0;
alcGetIntegerv(alCaptureDevice, ALC_CAPTURE_SAMPLES, 1, &number_of_samples);

Now we have some options. We can retrieve the PCM samples now, or we can wait until we accumulate more. In our demo program, we wait until we reach a certain threshold before we retrieve the data. This way, the rest of the code doesn't need to worry about always dealing with different amounts of data. One trade-off with this approach is latency. We have higher latency costs because we wait until we have a full buffer.

Finally, to retrieve the data, we do this:

alcCaptureSamples(alCaptureDevice, data_array, number_of_samples);

The variable data_array is the buffer where we want the OpenAL capture data to be copied, Once we retrieve the data, it will be removed from OpenAL's internal buffer. Also, keep in mind that the number of samples is not the same as the number of bytes. So we need to make sure the data_array we pass in is large enough to hold the number of samples we retrieve.

To compute the number of bytes needed for a given number of samples, the formula is as follows:

Capturing Audio

where:

Capturing Audio

or combined:

Capturing Audio

That's pretty much it. If you want to pause capturing, you can call this method:

alcCaptureStop(alCaptureDevice);

And to close the device, do this:

alcCloseDevice(alCaptureDevice);

So that is OpenAL capture in a nutshell. If you run the program on your Mac and your default input device is set to the internal microphone (go to System Preferences), you should just be able to make some noise and see the scope change.

This demo application does a few extracurricular activities that should be explained:

  • The method dataArray:maxArrayLength:getBytesPerSample was done as a protocol. I wanted to keep the OpenGL and OpenAL code mostly separated for cleanliness. But since OpenGL needs the data from OpenAL to draw, I did it using a delegate pattern to keep things generic. In theory, something else other than OpenAL could conform to the protocol and generate the data for OpenGL without requiring drastic changes to the code.

  • In the OpenGL side of the code, there is conversion for 16-bit integer to 32-bit float before the data is rendered in OpenGL. There are a couple of reasons for this, mostly centered on performance. First, once upon a time, Apple implied that you should be using floating-point values for vertices in OpenGL, as fixed-point values would have no performance benefits and would lose accuracy. However, Apple has since refined its documentation a little to suggest using the smallest data types you can get away with, because you may get some performance savings for having less overall data to send across the bus. But Apple says if you do any math computations, you should be using floating-point values. So, if you need to do any computations on the data you captured, such as FFT calculations, you are better off with floating-point values. To demonstrate, I included the conversion in the demo, even though we don't do any calculations here.

  • As I already mentioned, the demo uses Core Video and the display link in the Mac OS X version. Look for the #define USE_COREVIDEO_TIMER in the code.

The OpenGL code itself demonstrates an OpenGL optimization technique, which is discussed in the next section.

Back to OpenGL

In a sense, we have come full circle with the OpenGL chapters. OpenAL audio capture is used to generate data for the visuals, which are implemented in OpenGL. But I use a slightly more advanced form of getting data to the GPU, called vertex buffer objects.

Vertex Buffer Objects

Currently, the way we draw using vertex arrays in OpenGL ES is generally inefficient. The problem is that we start with large amounts of data in the CPU and main system memory describing vertices, colors, normals, and texture coordinates, and we need to copy them to the GPU to draw. We do this every time we draw a frame. This can be a huge amount of data. And typically, memory bus speeds are slower than processor speeds, which present bottlenecks. In addition, a lot of our data is static (unchanging). The geometry of our spaceship never changes. Wouldn't it be better if we didn't need to keep sending this data across the bus? The answer is yes. OpenGL has a solution: the vertex buffer object (VBO), which among other things, gives you a way to specify if the data needs to change.

Note

A long time ago, OpenGL provided something called a display list, which offered a way of caching static geometry on the video card so you didn't need to keep sending it. One downside to the display list was that it could not be altered, so if you needed to change something, you had to destroy it and create a new one.

For this example, it is debatable whether we will see any performance gains since we are frequently changing the data. However, this demo does reuse the data until enough new capture data is accumulated, so there is a potential case for a performance boost. Also, with respect to performance on PowerVR chipsets, using VBOs may or may not lead to performance gains, depending on which model of chip you are using. This is also sensitive to driver implementations as well. But according to both Apple and PowerVR, you are encouraged to use VBOs for performance. PowerVR's recommendations go as far as to claim that while VBOs may not help your performance depending on the chip, it will also not hurt your performance. You can see these recommendations here:

http://www.imgtec.com/factsheets/SDK/PowerVR%20MBX.3D%20Application%20Development%20Recommendations.1.0.67a.External.pdf

In addition, the word on the Internet is that the iPhone 3GS sees significant performance gains using VBOs, so the time to start looking at VBOs is now.

But the real reason I used VBOs was for educational purposes. Many OpenGL beginning tutorials depend on the glBegin/glVertex/glEnd paradigm, which is not in OpenGL ES and is being removed from OpenGL (proper). Since most people use OpenGL for performance, I felt it was worth the extra steps to demonstrate VBOs.

This demo shows how to change a VBO when you have streaming data. See the renderScene method in the OpenGL code for the interesting parts. Also, the color of the oscilloscope line will change to red when the VBO has just been changed. This will give you a sense of two things: how much we are reusing the data and how long it takes us to fill our OpenAL buffer (see Figure 12-14).

The OpenAL capture oscilloscope turns red when the VBO is updated.

Figure 12.14. The OpenAL capture oscilloscope turns red when the VBO is updated.

Tip

For more details on using VBOs, see the tutorial on my web site (http://playcontrol.net/ewing/jibberjabber/opengl_vertex_buffer_object.html). That demo uses static geometry. (And, by the way, static geometry is what all the models in Space Rocks! are using, so we would theoretically get a performance boost by using VBOs in our Space Rocks! code.)

Some Notes on OpenGL and OpenAL Optimization

Since I am on the topic of optimization, I'll close by mentioning a few thoughts I have on this topic, focusing on the similarities and differences between OpenGL and OpenAL.

First, the easiest optimization is to know what your underlying implementation uses as its native data format. This is true for OpenGL and OpenAL. OpenAL on the iPhone wants 16-bit little-endian signed integer data. Similarly in OpenGL, you want to pick a texture format the hardware is optimized to deal with. In OpenGL, you also want to think about packing your vertex arrays so the underlying types (vertices, colors, textures, and normals) are interleaved and word-aligned.

In OpenGL, one of the biggest guidelines is to avoid unnecessary state changes. Turing on and off texturing, loading new textures, changing colors, and so on all take their toll on OpenGL performance. In principle, this is true for OpenAL as well. However, particularly on the iPhone, where OpenAL is a software implementation, this tends to be less significant. You are mostly paying for function call overhead in the software case, which is far less disastrous as stalling and flushing the OpenGL pipeline.

Also in OpenGL is the notion of never using glGet*. Because OpenGL usually works on a separate processor (GPU) than the CPU, there is an amount of concurrency that can be achieved. But the use of glGet* creates sync points where the GPU and CPU must come together to get you the information you request. This will kill concurrency and result in poor performance. In OpenAL, it is mostly impossible to avoid using alGet*, as a lot of the API design requires you to ask for things. As you saw, we were constantly querying for the source state for resource management, callbacks, and buffer underruns. We had to query for the number of processed buffers. And we needed to query for the number of samples captured. But again, with software implementations, this is less of a problem. And the truth is that the video cards are far more sophisticated and tend to be more sensitive to these types of problems, as they are doing much more processing than the sound cards. (Face it, no one is talking about general-purpose computation on sound cards, a la OpenCL.)

In OpenGL, you typically shadow (mirror) state with your own variables to avoid using glGet. And often with OpenGL, people make even more elaborate libraries on top of their shadowing code to automatically coalesce and group state changes to minimize changes in the OpenGL state machine to maximize performance. OpenSceneGraph is an example of a third-party open source library that will sort out your OpenGL drawing order so it minimizes state changes (among other things). If you are going to build something like this for your own projects, you might consider including OpenAL state as well. Of course, you won't be able to reasonably mirror all things (e.g., the aforementioned source state, processed buffers, and samples captured), but you can get some things, like position and velocity. Whether you will see performance benefits or not is hard to say. Performance is tricky to talk about in generalities. But if you are already going to the effort of building it for your OpenGL stuff, it's probably not much more work to extend it to OpenAL and give it a try.

Note

Hardware-based OpenAL implementations, such as ones using Creative Labs sound cards, are more likely to be affected and benefit from minimizing queries to the device and changing of state. For code you intend to share across multiple platforms, you may want to take this into account when designing your implementation. One piece of low-hanging fruit presented in this book is our polling-oriented design for callbacks and buffer queuing. Implementing a timer to fire approximately when you think a polling check is needed is a relatively simple thing to do. However, there is a caveat to be aware of with this approach. Be aware that sounds that have been shifted due to using AL_PITCH or Doppler play back at different speeds. A sound pitch shifted into a higher frequency plays faster than when you play it normally. And conversely, a sound pitch shifted into a lower frequency plays slower.

Another optimization often employed in OpenGL is (geometry) batching. For example, you might be drawing ten different objects and calling glDrawArrays ten times. Instead, you can combine all your objects into one giant array and call draw just once. In OpenAL, there is also a notion of batching, though it is a little different, and usually found only on Creative Labs hardware implementations. The motivation is that the hardware will dutifully recompute all the panning and attenuation properties as you set them. So, if you are in a single pass of an update loop and you change all the positions for your sources, as you set the new positions, everything is being recomputed. But then at the end of your loop, you get around to changing the listener position, and all the prior computations need to be invalidated and redone. Creative Lab's implementation of alcSuspendContext will allow the system to batch or coalesce OpenAL commands without doing the computation. Then calling alcProcessContext will resume processing and compute just the current values without wasting computation on all the intermediate/invalidated values. As I pointed out earlier, Apple's alcSuspendContext is a no-op, so don't expect this Creative Labs technique to work elsewhere.

The End of the Audio Road

Conglaturation !!! You have completed a great game(book). And prooved the justice of our culture. Now go and rest our heroes![47] A little tongue-in-cheek, bad video game ending humor here. It was a long road, and you've made it to the end!

You've seen the various different sound APIs and their strengths and weaknesses. And you've seen how OpenGL and OpenAL can work together to help create a compelling game. You should now be able to select the APIs that best suit your needs and have ideas on how you can utilize these technologies to help you make the best game possible.

The following are some additional references that may be of help to you.

For OpenAL, these resources are available:

  • The OpenAL 1.1 Specification and Reference: http://connect.creativelabs.com/openal/Documentation/OpenAL%201.1%20Specification.pdf

  • The OpenAL Programmer's Guide: http://connect.creativelabs.com/openal/Documentation/OpenAL_Programmers_Guide.pdf

  • The OpenAL home page: http://connect.creativelabs.com/openal

Apple documentation includes these references:

  • Using Sound in iPhone OS: http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/AudioandVideoTechnologies/AudioandVideoTechnologies.html#//apple_ref/doc/uid/

  • Core Audio Overview: http://developer.apple.com/iphone/library/documentation/MusicAudio/Conceptual/CoreAudioOverview/Introduction/Introduction.html

  • Audio Session Programming Guide: http://developer.apple.com/iphone/library/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Introduction/Introduction.html

  • Audio Queue Services Programming Guide: http://developer.apple.com/iphone/library/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Introduction/Introduction.html

I also suggest using the Apple Bug Reporter (http://bugreport.apple.com) to report bugs, request features, and request documentation enhancements.

For information about OpenGL performance, see PowerVR 3D Application Development Recommendations (http://www.imgtec.com/factsheets/SDK/PowerVR%20MBX.3D%20Application%20Development%20Recommendations.1.0.67a.External.pdf).

You can also find tutorials and information on my web site, PlayControl Software (http://playcontrol.net).

It's game over for me. Peter Bakhirev will join you next and teach you all about how to add networking to your game. <Free Play/Press Start>

See you next mission!



[30] The correct term may be dequeue, but the official OpenAL API uses the term unqueue, and you will see that term used in discussions out in the wild. Thus, I will continue to use the term unqueue.

[31] If you want to be clever, you can do it without the loop in one shot. Hint: not required but, C99 variable-length arrays might be fun/convenient to use here.

[32] To be pedantic, buffer sizes must be an exact multiple of the sample frame size (or block alignment in Wave format terminology). As it turns out, powers of two always work for mono and stereo samples.

[33] I should mention that I have been bitten by OpenAL bugs where the buffer IDs returned were either wrong or in the wrong order. This is not supposed to happen, but if you are concerned, you might add an assert to verify the buffer IDs match.

[34] The ideal number is between 20 and 32, but I didn't feel like testing them all.

[35] And if you have multiple streams, this amount of memory is per-stream.

[36] Did you know that Mac OS X ships with a voice synthesizer and there is a command-line tool called say to access it? Try typing this at the terminal: say -v Bad "Game Over You're Dead You Lose Do Not Pass Go".

[37] If the other UFO destroys the taunting UFO, this also triggers a stop and deletion of the buffers.

[38] Bjarne Stroustrup, the creator of C++, has been advocating Resource Acquisition Is Initialization (RAII) for years.

[39] Just don't spend too much time doing computation in the callback.

[40] If you compare our example to Apple's original code, I may have also made a few tiny bug fixes in our version, though it's hard for me to say with certainty, since there was so much copy and paste to do, plus the changes that had to be integrated.

[41] I have independently verified that the audio queue may fail to resume with our program. It will not always fail to resume, but you probably don't want to rely on this unpredictable behavior.

[42] We save the file URL as an instance variable in the class so we can remember which file to reopen.

[43] Not counting using audio units directly,

[44] Well, it may have some bugs, because I can't test it.

[45] One of these days, I should port it over to CAOpenGLLayer and use a layer hosting view to really modernize it for the latest Mac OS X state of the art.

[46] The example code actually wraps this in a helper function called InitOpenALCaptureDevice, but it boils down to this.

[47] From the infamous ending of the horrible game Ghostbusters on the NES, with bad grammar, spelling mistakes, etc. reproduced here.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset