iPhone
iPhone SDK Example: Moving And Rotating Image Part 3 – Playing Audio
May 14, 2009
0

This article continues from Part 1 and Part 2.  We will be using the project created previously. This time, the focus is basic sound playback.  We will use one of Apple examples which makes it easy to load and play sound files.  You can find the original files at Apple Developer Network by searching for BubbleLevel at http://developer.apple.com/ (sorry, no direct link since you need to login to get to the file).

Step 1

We’re going to add the audio files.  iPhone likes CAF file format. You don’t have to use CAF, but if you do, you can read about how to create or convert to CAF here. For this demonstration, I will use one CAF and one MP3.

Import the two audio files into the Resource group, using the same step as when you imported images (control-click the Resources icon then select Add Existing Files).

addaudiointoproject

Step 2

Add SoundEffect.m and SoundEffect.h from the BubbleLevel example (if you don’t have it, it is also included in the zip at the end of this article). As before, control-click on the Classes group then select Add Existing Files).

/*

File: SoundEffect.h
Abstract: SoundEffect is a class that loads and plays sound files.

Version: 1.6

Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Inc.
("Apple") in consideration of your agreement to the following terms, and your
use, installation, modification or redistribution of this Apple software
constitutes acceptance of these terms.  If you do not agree with these terms,
please do not use, install, modify or redistribute this Apple software.

In consideration of your agreement to abide by the following terms, and subject
to these terms, Apple grants you a personal, non-exclusive license, under
Apple's copyrights in this original Apple software (the "Apple Software"), to
use, reproduce, modify and redistribute the Apple Software, with or without
modifications, in source and/or binary forms; provided that if you redistribute
the Apple Software in its entirety and without modifications, you must retain
this notice and the following text and disclaimers in all such redistributions
of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may be used
to endorse or promote products derived from the Apple Software without specific
prior written permission from Apple.  Except as expressly stated in this notice,
no other rights or licenses, express or implied, are granted by Apple herein,
including but not limited to any patent rights that may be infringed by your
derivative works or by other works in which the Apple Software may be
incorporated.

The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
COMBINATION WITH YOUR PRODUCTS.

IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR
DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF
CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF
APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Copyright (C) 2008 Apple Inc. All Rights Reserved.

*/

#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioServices.h>

@interface SoundEffect : NSObject {
    SystemSoundID _soundID;
}

+ (id)soundEffectWithContentsOfFile:(NSString *)aPath;
- (id)initWithContentsOfFile:(NSString *)path;
- (void)play;
/*

File: SoundEffect.m
Abstract: SoundEffect is a class that loads and plays sound files.

Version: 1.6

Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Inc.
("Apple") in consideration of your agreement to the following terms, and your
use, installation, modification or redistribution of this Apple software
constitutes acceptance of these terms.  If you do not agree with these terms,
please do not use, install, modify or redistribute this Apple software.

In consideration of your agreement to abide by the following terms, and subject
to these terms, Apple grants you a personal, non-exclusive license, under
Apple's copyrights in this original Apple software (the "Apple Software"), to
use, reproduce, modify and redistribute the Apple Software, with or without
modifications, in source and/or binary forms; provided that if you redistribute
the Apple Software in its entirety and without modifications, you must retain
this notice and the following text and disclaimers in all such redistributions
of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may be used
to endorse or promote products derived from the Apple Software without specific
prior written permission from Apple.  Except as expressly stated in this notice,
no other rights or licenses, express or implied, are granted by Apple herein,
including but not limited to any patent rights that may be infringed by your
derivative works or by other works in which the Apple Software may be
incorporated.

The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
COMBINATION WITH YOUR PRODUCTS.

IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR
DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF
CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF
APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Copyright (C) 2008 Apple Inc. All Rights Reserved.

*/

#import "SoundEffect.h"
@implementation SoundEffect
// Creates a sound effect object from the specified sound file
+ (id)soundEffectWithContentsOfFile:(NSString *)aPath {
    if (aPath) {
        return [[[SoundEffect alloc] initWithContentsOfFile:aPath] autorelease];
    }
    return nil;
}

// Initializes a sound effect object with the contents of the specified sound file
- (id)initWithContentsOfFile:(NSString *)path {
    self = [super init];

	// Gets the file located at the specified path.
    if (self != nil) {
        NSURL *aFileURL = [NSURL fileURLWithPath:path isDirectory:NO];

		// If the file exists, calls Core Audio to create a system sound ID.
        if (aFileURL != nil)  {
            SystemSoundID aSoundID;
            OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)aFileURL, &aSoundID);

            if (error == kAudioServicesNoError) { // success
                _soundID = aSoundID;
            } else {
                NSLog(@"Error %d loading sound at path: %@", error, path);
                [self release], self = nil;
            }
        } else {
            NSLog(@"NSURL is nil for path: %@", path);
            [self release], self = nil;
        }
    }
    return self;
}

// Releases resouces when no longer needed.
-(void)dealloc {
    AudioServicesDisposeSystemSoundID(_soundID);
    [super dealloc];
}

// Plays the sound associated with a sound effect object.
-(void)play {
	// Calls Core Audio to play the sound for the specified sound ID.
	AudioServicesPlaySystemSound(_soundID);
}
@end

Your Classes group should now look like this:

addedsoundeffectclass

 

Try compiling now.  You will get an error like this:

“_AudioServicesPlaySystemSound”, referenced from:
-[SoundEffect play] in SoundEffect.o
“_AudioServicesDisposeSystemSoundID”, referenced from:
-[SoundEffect dealloc] in SoundEffect.o
“_AudioServicesCreateSystemSoundID”, referenced from:
-[SoundEffect initWithContentsOfFile:] in SoundEffect.o
ld: symbol(s) not found
collect2: ld returned 1 exit status

Step 3

The error means that the linker could not find some of the functions that our application uses.  Since we haven’t added anything new other than the SoundEffect class, we know it’s something related to sound.  Sometimes it is not obvious which Framework is needed, here’s a tip, short of Googling it.
a) Look at the BubbleLevel example and see which Frameworks are needed. Or b) Do some investigative work, such as opening up the Xcode Developer Documentation window and search for the missing functions (do not include the underline in the function name as the underline is added by the compiler).  By searching for AudioServicesCreateSystemSoundID, you can see from the documentation that it requires AudioToolbox framework.xcodedocwindowAlternatively, you can open the problematic file (SoundEffect.o is the compiled obj file of SoundEffect class), so you can also open SoundEffect.h and see this import statement: import <audioToolbox/AudioServices.h>.

Now, let’s include that Framework.  Expand the Target group, select Link Binary With Libraries, select Add ->Existing Frameworks.

balladdexistinglibrary

 

A dialog will popup.  Click the plusbutton icon on the bottom, and add select AudioToolbox.framework.

audiotoolboxadd

PS: For more tips, check out this post.

Rebuild the project, the error should disappear now.

Step 3

It’s coding time.

If you look at SoundEffect.h, you can see the methods that are available.  We will use initWithContentsOfFile since -well- that’s the only one we can use.  Seriously, it is very simple, basically you create the SoundEffect object and then call play.  For instance, below is the code to play “audio.mp3”.

SoundEffect* soundEffect=[[SoundEffect alloc];
      initWithContentsOfFile:[mainBundle pathForResource:@"audio" ofType:@"mp3"]]];  
soundEffect.play();

What is mainBundle?  From Apple documentation: “In general, the main bundle corresponds to an application file package or application wrapper: a directory that bears the name of the application and is marked by a “.app” extension.” You can retrieve the pointer to it by simply calling [NSBundle mainBundle] as we will see later.

Edit Ball.h and add a variable to store the SoundEffect object.

#import <Foundation/Foundation.h>
@class SoundEffect;

@interface Ball : UIImageView {
	int mXSpeed;
	int mYSpeed;
	float mAngle;
	SoundEffect* mSoundEffect;
}
- (void)move;
- (void)setSpeedX:(int)xSpeed Y:(int)ySpeed;
- (void)setSoundEffect:(SoundEffect*)soundEffect;
@end

Add the #import and the setSoundEffect function to Ball.m

#import "SoundEffect.h"

...

- (void)setSoundEffect:(SoundEffect*)soundEffect {
	mSoundEffect=soundEffect;
}

Edit the move function in Ball.m to play the SoundEffect when the ball hits the screen-edges.

- (void)move {
	self.center=CGPointMake(self.center.x + mXSpeed, self.center.y + mYSpeed);
	if (!CGRectContainsRect(self.superview.frame, self.frame))
	{
		Boolean bHit=false;
		if (self.frame.origin.x<self.superview.frame.origin.x)
		{
			bHit=true;
			mXSpeed=abs(mXSpeed);
		}

		if (self.frame.origin.x>self.superview.frame.size.width-self.frame.size.width)
		{
			bHit=true;
			mXSpeed=-abs(mXSpeed);

		}

		if (self.frame.origin.y<0)//self.superview.frame.origin.y)
		{
			bHit=true;
			mYSpeed=abs(mYSpeed);
		}
		if (self.frame.origin.y>self.superview.frame.size.height-self.frame.size.height)
		{
			bHit=true;
			mYSpeed=-abs(mYSpeed);
		}
		if (bHit)
		{
			[mSoundEffect play];
		}
	}
	self.transform=CGAffineTransformMakeRotation (mAngle);
	mAngle+=0.1;
}

Edit BallViewController.m to load and assign the sound file to the Ball objects.

- (void)viewDidLoad {
	[super viewDidLoad];

	NSBundle *pMainBundle = [NSBundle mainBundle];

	mBallArray = [[NSMutableArray alloc] init];
	UIImage* image1=[UIImage imageNamed:@"ball1.png"];
	UIImage* image2=[UIImage imageNamed:@"ball2.png"];
	SoundEffect* sound1=[[SoundEffect alloc]
		initWithContentsOfFile:[pMainBundle pathForResource:@"ding1" ofType:@"caf"]];
	SoundEffect* sound2=[[SoundEffect alloc]
		initWithContentsOfFile:[pMainBundle pathForResource:@"ding2" ofType:@"mp3"]];

	int i;
	for (i = 0; i < 10; i++)
	{
		UIImage* image;
		SoundEffect* sound;
		if ((i%2)==0)
		{
			image=image1;
			sound=sound1;
		}
		else
		{
			image=image2;
			sound=sound2;
		}
		Ball* ball=[[Ball alloc] initWithImage:image];
		[mBallArray addObject:ball];
		[self.view addSubview:ball];
		int speed=random() % 10;

		[ball setSpeedX:speed Y:speed];
		[ball setSoundEffect:sound];
		ball.center=self.view.center;
	}

	[NSTimer scheduledTimerWithTimeInterval:1.0/30.0
		target:self selector:@selector(moveBall) userInfo:nil repeats:YES];
}

I allocated two SoundEffects, ding1.caf and ding2.mp3, then I alternated between the two, assigning the first one on even-indexed Balls and the second one on odd-indexed Balls.

This line gets the main bundle, which is needed for initWithContentsOfFile:
NSBundle *pMainBundle = [NSBundle mainBundle];

There are some issues to be aware of, which makes the AudioTolbox somewhat limited, although for most games, it should be good enough.
1) AudioServicesPlaySystemSound can only play audio that is no more than 30 seconds.
2) If you try to play the same audio file while it is still playing, it may mess up future payback (ie: subsequent call to play the same audio may produce silence.

To handle the second issue, we need to modify the code slightly so that we won’t be playing the sound while it is still playing.
Edit SoundEffect.h and add a variable to indicate that the sound is playing. We will set this flag to true when the audio is playing:

@interface SoundEffect : NSObject {
 	SystemSoundID _soundID;
	Boolean isPlaying;
}

We also need to modify the play function in SoundEffect.m to check is the audio is playing and to set the flag when we start playing. We also initiate AudioServicesAddSystemSoundCompletion to “callback” the AudioPlaybackComplete function when the audio finishes playing, while passing self as the extra data to be passed to AudioPlaybackComplete, so that we know which instance of the SoundEffect object has finished playing.

-(void)play {
	if (!isPlaying)
	{
		// Calls Core Audio to play the sound for the specified sound ID.
		AudioServicesPlaySystemSound(_soundID);
		AudioServicesAddSystemSoundCompletion(_soundID, NULL, NULL, AudioPlaybackComplete, self);
		isPlaying=true;
	}
}

Unfortunately, you’ll have to do some C and create a static function (class method) and do some pointer stuff (the -> thing) here, but it’s fairly short:

static void AudioPlaybackComplete(SystemSoundID  ssID, void *clientData)
{
	SoundEffect* pSoundEffect=clientData;
	if (pSoundEffect->_soundID==ssID)
	{
		pSoundEffect->isPlaying=false;
	}

}

The code sets the isPlaying variable back to false so that the SoundEffect object knows it’s safe to play the sound again. Because the function is static, the same function will be called by all instances of the SoundEffect class. So we need to pass the SoundEffect object that is calling back the function, which is done as the second parameter *clientData.

I will post the project in the next few days.