In this article, I’m looking at two distinct things:
- Basic use of AVAudioPlayer, a CocoaTouch class that gives reasonably good sound support for many applications. If you’re deep into waves, search OpenAL instead.
- How to use the F* meta-pattern to integrate audio as a ‘pluggable’ feature, with minimum interaction with existing code.
As a side effect (this is integral to the spirit of the F* pattern), you might be able to integrate sample code from this article in your application very easily. If you’re not into the theory, please skip to the code and have fun.
A few days ago, I have decided to try applying the F* meta-pattern with my game integration. A first step towards this was i/o integration, for which I provided:
- A dispatcher for in-game milestone events
- A protocol used to identify consumers of milestone events.
- Classes packaged separately that take care of i/o.
Practice makes better. I’m starting to understand how I can apply F* to developing my game, and audio integration is the next step.
Here, I want to associate a sound with specified actor activities. For now I’ll keep it simple, just start a sound when the actor begins an activity.
In my little game framework, the actor’s activity is updated at every frame. Activity changes only occur when the new activity is different from the previous activity. So what I’ll do is define an observer pattern allowing me to detect activity changes without creating type dependencies with the Actor class. I’ll then register an instance of a ‘sound feature class’ with the event dispatcher.
Observer Pattern
I have moved my dispatchers and listeners to a separate package. I then create a new group including the following:
- ActivityChangeDispatcher provides static methods used to notify listeners to activity change events.
- ActivityChangeListener specifies… activity change listener functions.
Here is the code for ActivityChangeListener (act is just a typedef in Definitions.h):
#import "Definitions.h"
@class Actor;
@protocol ActivityChangeListener
-(void)actor:(Actor*)a activityChangedFrom:(act)a0 to:(act)a1;
@end
Then the header file for ActivityChangeDispatcher
#import "Definitions.h"
#import "ActivityChangeListener.h"
#import "EventDispatcherMacros.h"
@class Actor;
@interface ActivityChangeDispatcher : NSObject
DEC_LISTENERS(ActivityChangeListener)
+(void)actor:(Actor*)a activityChangedFrom:(act)a0 to:(act)a1;
@end
Finally the implementation of ActivityChangeDispatcher.
#import "ActivityChangeDispatcher.h"
@implementation ActivityChangeDispatcher
DEF_LISTENERS(ActivityChangeListener)
+(void)actor:(Actor*)a activityChangedFrom:(act)a0 to:(act)a1{
for (id l in listeners) {
[l actor:a activityChangedFrom:a0 to:a1];
}
}
@end
DEC_LISTENER() and DEF_LISTENER() are declared as follows in EventDispatcherMacros.h:
#define DEF_LISTENERS(interface) \
static NSMutableArray* listeners; \
+(void)addListener:(id)l{ \
if (listeners==nil)listeners=[[NSMutableArray alloc]init]; \
[listeners addObject:l]; \
} \
+(void)removeListener:(id)l{ \
if (listeners==nil)return; \
[listeners removeObject:l]; \
}
#define DEC_LISTENERS(interface) \
+(void)addListener:(id)l; \
+(void)removeListener:(id)l;
I was kind of hoping I might be able to define a catch-all macro avoiding writing code for new observer pattern classes altogether. This isn’t quite like it although some of the boiler plate has been removed. But then I can probably do better. Next time i’ll try to make this more concise as I do not want to define more than one event per dispatcher.
Once this is done, I need to activate the dispatch by pasting this code wherever activity changes:
[ActivityChangeDispatcher actor:a activityChangedFrom:x to:y];
The Sound Feature
My sound feature will be PlaySoundOnActivityChange.
Here’s the header
#import "ActivityChangeListener.h"
#import "Definitions.h"
@interface PlaySoundOnActivityChange : NSObject {
NSMutableDictionary* map;
NSMutableDictionary* players;
}
-(void)map:(tag)label to:(tag)soundFile;
@end
Now here’s the implementation:
#import "PlaySoundOnActivityChange.h"
#import <AVFoundation/AVFoundation.h>
#import "ActivityChangeDispatcher.h"
@interface PlaySoundOnActivityChange (private)
-(AVAudioPlayer*)prepareSound:(tag)name;
@end
@implementation PlaySoundOnActivityChange
-(id)init{
if (self=[super init]) {
map=[[NSMutableDictionary alloc]init];
players=[[NSMutableDictionary alloc]init];
[ActivityChangeDispatcher addListener:self];
}return self;
}
-(void)actor:(Actor *)a activityChangedFrom:(act)a0 to:(act)a1{
NSString* code=[NSString stringWithFormat:@"%i",a1];
tag name=[map objectForKey:code];
if (name==nil)return;
AVAudioPlayer* player=[players objectForKey:name];
if (player==nil)player=[self prepareSound:name];
if (player==nil)return;
[player play];
}
-(void)map:(tag)label to:(tag)soundFile{
[map setObject:soundFile forKey:label];
}
// private ---------------------------------------------------------
-(AVAudioPlayer*)prepareSound:(tag)name{
NSBundle *mainBundle = [NSBundle mainBundle];
NSError *error;
NSURL *url = [NSURL fileURLWithPath:
[mainBundle pathForResource:name ofType:@"caf"]];
//
AVAudioPlayer* player = [[AVAudioPlayer alloc]
initWithContentsOfURL:url error:&error];
if (!player) {
NSLog(@"no player: %@", [error localizedDescription]);
return nil;
}
[player prepareToPlay];
[players setObject:player forKey:name];
return player;
}
@end
I haven’t tested this yet (OK, it compiles… – PS: after testing, it actually works). Also, I’m a little bit worried about what will happen if I have a 100 or a 1000 sounds registered in my map – I don’t really know what resources players are holding to, especially once [prepareToPlay] has been invoked. Pending, note the way a feature is designed and implemented using F*, and also a couple of errors:
- I have provided an event dispatcher allowing me to trigger sounds at the right moment. The generated events provide me with all the information I need to select sound.
- Actor does not import PlaySoundOnActivityChange – I can remove the sound feature without breaking my code (so far).
- PlaySoundOnActivityChange and ActivityChangeListener/Dispatcher import the actor class. This could, and should, be easily avoided (so that I can use the same sound feature for non actor sounds, or in an altogether different context.
I need to import sounds into my project and map them. Probably this would be done better with a property list defined as a resource but for now, I just want to map a couple of values and check the result. So I define a feature setup class with a couple of mappings. Invoke the setup class directly from my GameSetup class. Here’s the code for InitSound:
#import "InitSound.h"
#import "PlaySoundOnActivityChange.h"
#import "Definitions.h"
#import "VRConstants.h"
@implementation InitSound
+(void)apply{
NSString* label1=[NSString stringWithFormat:@"%i",WALK];
NSString* label2=[NSString stringWithFormat:@"%i",STAB];
PlaySoundOnActivityChange* feature=[[PlaySoundOnActivityChange alloc]init];
[feature map:label1 to:@"tick"]; // just reused the sounds
[feature map:label2 to:@"tock"]; // from Metronome example for a try.
}
@end
Done with sound.