--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+#import "CocosDenshion.h"
+#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 30000
+ #import <AVFoundation/AVFoundation.h>
+#else
+ #import "CDXMacOSXSupport.h"
+#endif
+
+/** Different modes of the engine */
+typedef enum {
+ kAMM_FxOnly, //!Other apps will be able to play audio
+ kAMM_FxPlusMusic, //!Only this app will play audio
+ kAMM_FxPlusMusicIfNoOtherAudio, //!If another app is playing audio at start up then allow it to continue and don't play music
+ kAMM_MediaPlayback, //!This app takes over audio e.g music player app
+ kAMM_PlayAndRecord //!App takes over audio and has input and output
+} tAudioManagerMode;
+
+/** Possible states of the engine */
+typedef enum {
+ kAMStateUninitialised, //!Audio manager has not been initialised - do not use
+ kAMStateInitialising, //!Audio manager is in the process of initialising - do not use
+ kAMStateInitialised //!Audio manager is initialised - safe to use
+} tAudioManagerState;
+
+typedef enum {
+ kAMRBDoNothing, //Audio manager will not do anything on resign or becoming active
+ kAMRBStopPlay, //Background music is stopped on resign and resumed on become active
+ kAMRBStop //Background music is stopped on resign but not resumed - maybe because you want to do this from within your game
+} tAudioManagerResignBehavior;
+
+/** Notifications */
+extern NSString * const kCDN_AudioManagerInitialised;
+
+@interface CDAsynchInitialiser : NSOperation {}
+@end
+
+/** CDAudioManager supports two long audio source channels called left and right*/
+typedef enum {
+ kASC_Left = 0,
+ kASC_Right = 1
+} tAudioSourceChannel;
+
+typedef enum {
+ kLAS_Init,
+ kLAS_Loaded,
+ kLAS_Playing,
+ kLAS_Paused,
+ kLAS_Stopped,
+} tLongAudioSourceState;
+
+@class CDLongAudioSource;
+@protocol CDLongAudioSourceDelegate <NSObject>
+@optional
+/** The audio source completed playing */
+- (void) cdAudioSourceDidFinishPlaying:(CDLongAudioSource *) audioSource;
+/** The file used to load the audio source has changed */
+- (void) cdAudioSourceFileDidChange:(CDLongAudioSource *) audioSource;
+@end
+
+/**
+ CDLongAudioSource represents an audio source that has a long duration which makes
+ it costly to load into memory for playback as an effect using CDSoundEngine. Examples
+ include background music and narration tracks. The audio file may or may not be compressed.
+ Bear in mind that current iDevices can only use hardware to decode a single compressed
+ audio file at a time and playing multiple compressed files will result in a performance drop
+ as software decompression will take place.
+ @since v0.99
+ */
+@interface CDLongAudioSource : NSObject <AVAudioPlayerDelegate, CDAudioInterruptProtocol>{
+ AVAudioPlayer *audioSourcePlayer;
+ NSString *audioSourceFilePath;
+ NSInteger numberOfLoops;
+ float volume;
+ id<CDLongAudioSourceDelegate> delegate;
+ BOOL mute;
+ BOOL enabled_;
+ BOOL backgroundMusic;
+@public
+ BOOL systemPaused;//Used for auto resign handling
+ NSTimeInterval systemPauseLocation;//Used for auto resign handling
+@protected
+ tLongAudioSourceState state;
+}
+@property (readonly) AVAudioPlayer *audioSourcePlayer;
+@property (readonly) NSString *audioSourceFilePath;
+@property (readwrite, nonatomic) NSInteger numberOfLoops;
+@property (readwrite, nonatomic) float volume;
+@property (assign) id<CDLongAudioSourceDelegate> delegate;
+/* This long audio source functions as background music */
+@property (readwrite, nonatomic) BOOL backgroundMusic;
+
+/** Loads the file into the audio source */
+-(void) load:(NSString*) filePath;
+/** Plays the audio source */
+-(void) play;
+/** Stops playing the audio soruce */
+-(void) stop;
+/** Pauses the audio source */
+-(void) pause;
+/** Rewinds the audio source */
+-(void) rewind;
+/** Resumes playing the audio source if it was paused */
+-(void) resume;
+/** Returns whether or not the audio source is playing */
+-(BOOL) isPlaying;
+
+@end
+
+/**
+ CDAudioManager manages audio requirements for a game. It provides access to a CDSoundEngine object
+ for playing sound effects. It provides access to two CDLongAudioSource object (left and right channel)
+ for playing long duration audio such as background music and narration tracks. Additionally it manages
+ the audio session to take care of things like audio session interruption and interacting with the audio
+ of other apps that are running on the device.
+
+ Requirements:
+ - Firmware: OS 2.2 or greater
+ - Files: CDAudioManager.*, CocosDenshion.*
+ - Frameworks: OpenAL, AudioToolbox, AVFoundation
+ @since v0.8
+ */
+@interface CDAudioManager : NSObject <CDLongAudioSourceDelegate, CDAudioInterruptProtocol, AVAudioSessionDelegate> {
+ CDSoundEngine *soundEngine;
+ CDLongAudioSource *backgroundMusic;
+ NSMutableArray *audioSourceChannels;
+ NSString* _audioSessionCategory;
+ BOOL _audioWasPlayingAtStartup;
+ tAudioManagerMode _mode;
+ SEL backgroundMusicCompletionSelector;
+ id backgroundMusicCompletionListener;
+ BOOL willPlayBackgroundMusic;
+ BOOL _mute;
+ BOOL _resigned;
+ BOOL _interrupted;
+ BOOL _audioSessionActive;
+ BOOL enabled_;
+
+ //For handling resign/become active
+ BOOL _isObservingAppEvents;
+ tAudioManagerResignBehavior _resignBehavior;
+}
+
+@property (readonly) CDSoundEngine *soundEngine;
+@property (readonly) CDLongAudioSource *backgroundMusic;
+@property (readonly) BOOL willPlayBackgroundMusic;
+
+/** Returns the shared singleton */
++ (CDAudioManager *) sharedManager;
++ (tAudioManagerState) sharedManagerState;
+/** Configures the shared singleton with a mode*/
++ (void) configure: (tAudioManagerMode) mode;
+/** Initializes the engine asynchronously with a mode */
++ (void) initAsynchronously: (tAudioManagerMode) mode;
+/** Initializes the engine synchronously with a mode, channel definition and a total number of channels */
+- (id) init: (tAudioManagerMode) mode;
+-(void) audioSessionInterrupted;
+-(void) audioSessionResumed;
+-(void) setResignBehavior:(tAudioManagerResignBehavior) resignBehavior autoHandle:(BOOL) autoHandle;
+/** Returns true is audio is muted at a hardware level e.g user has ringer switch set to off */
+-(BOOL) isDeviceMuted;
+/** Returns true if another app is playing audio such as the iPod music player */
+-(BOOL) isOtherAudioPlaying;
+/** Sets the way the audio manager interacts with the operating system such as whether it shares output with other apps or obeys the mute switch */
+-(void) setMode:(tAudioManagerMode) mode;
+/** Shuts down the shared audio manager instance so that it can be reinitialised */
++(void) end;
+
+/** Call if you want to use built in resign behavior but need to do some additional audio processing on resign active. */
+- (void) applicationWillResignActive;
+/** Call if you want to use built in resign behavior but need to do some additional audio processing on become active. */
+- (void) applicationDidBecomeActive;
+
+//New AVAudioPlayer API
+/** Loads the data from the specified file path to the channel's audio source */
+-(CDLongAudioSource*) audioSourceLoad:(NSString*) filePath channel:(tAudioSourceChannel) channel;
+/** Retrieves the audio source for the specified channel */
+-(CDLongAudioSource*) audioSourceForChannel:(tAudioSourceChannel) channel;
+
+//Legacy AVAudioPlayer API
+/** Plays music in background. The music can be looped or not
+ It is recommended to use .aac files as background music since they are decoded by the device (hardware).
+ */
+-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop;
+/** Preloads a background music */
+-(void) preloadBackgroundMusic:(NSString*) filePath;
+/** Stops playing the background music */
+-(void) stopBackgroundMusic;
+/** Pauses the background music */
+-(void) pauseBackgroundMusic;
+/** Rewinds the background music */
+-(void) rewindBackgroundMusic;
+/** Resumes playing the background music */
+-(void) resumeBackgroundMusic;
+/** Returns whether or not the background music is playing */
+-(BOOL) isBackgroundMusicPlaying;
+
+-(void) setBackgroundMusicCompletionListener:(id) listener selector:(SEL) selector;
+
+@end
+
+/** Fader for long audio source objects */
+@interface CDLongAudioSourceFader : CDPropertyModifier{}
+@end
+
+static const int kCDNoBuffer = -1;
+
+/** Allows buffers to be associated with file names */
+@interface CDBufferManager:NSObject{
+ NSMutableDictionary* loadedBuffers;
+ NSMutableArray *freedBuffers;
+ CDSoundEngine *soundEngine;
+ int nextBufferId;
+}
+
+-(id) initWithEngine:(CDSoundEngine *) theSoundEngine;
+-(int) bufferForFile:(NSString*) filePath create:(BOOL) create;
+-(void) releaseBufferForFile:(NSString *) filePath;
+
+@end
+
--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+
+#import "CDAudioManager.h"
+
+NSString * const kCDN_AudioManagerInitialised = @"kCDN_AudioManagerInitialised";
+
+//NSOperation object used to asynchronously initialise
+@implementation CDAsynchInitialiser
+
+-(void) main {
+ [super main];
+ [CDAudioManager sharedManager];
+}
+
+@end
+
+@implementation CDLongAudioSource
+
+@synthesize audioSourcePlayer, audioSourceFilePath, delegate, backgroundMusic;
+
+-(id) init {
+ if ((self = [super init])) {
+ state = kLAS_Init;
+ volume = 1.0f;
+ mute = NO;
+ enabled_ = YES;
+ }
+ return self;
+}
+
+-(void) dealloc {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - deallocating %@", self);
+ [audioSourcePlayer release];
+ [audioSourceFilePath release];
+ [super dealloc];
+}
+
+-(void) load:(NSString*) filePath {
+ //We have alread loaded a file previously, check if we are being asked to load the same file
+ if (state == kLAS_Init || ![filePath isEqualToString:audioSourceFilePath]) {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - Loading new audio source %@",filePath);
+ //New file
+ if (state != kLAS_Init) {
+ [audioSourceFilePath release];//Release old file path
+ [audioSourcePlayer release];//Release old AVAudioPlayer, they can't be reused
+ }
+ audioSourceFilePath = [filePath copy];
+ NSError *error = nil;
+ NSString *path = [CDUtilities fullPathFromRelativePath:audioSourceFilePath];
+ audioSourcePlayer = [(AVAudioPlayer*)[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:&error];
+ if (error == nil) {
+ [audioSourcePlayer prepareToPlay];
+ audioSourcePlayer.delegate = self;
+ if (delegate && [delegate respondsToSelector:@selector(cdAudioSourceFileDidChange:)]) {
+ //Tell our delegate the file has changed
+ [delegate cdAudioSourceFileDidChange:self];
+ }
+ } else {
+ CDLOG(@"Denshion::CDLongAudioSource - Error initialising audio player: %@",error);
+ }
+ } else {
+ //Same file - just return it to a consistent state
+ [self pause];
+ [self rewind];
+ }
+ audioSourcePlayer.volume = volume;
+ audioSourcePlayer.numberOfLoops = numberOfLoops;
+ state = kLAS_Loaded;
+}
+
+-(void) play {
+ if (enabled_) {
+ self->systemPaused = NO;
+ [audioSourcePlayer play];
+ } else {
+ CDLOGINFO(@"Denshion::CDLongAudioSource long audio source didn't play because it is disabled");
+ }
+}
+
+-(void) stop {
+ [audioSourcePlayer stop];
+}
+
+-(void) pause {
+ [audioSourcePlayer pause];
+}
+
+-(void) rewind {
+ [audioSourcePlayer setCurrentTime:0];
+}
+
+-(void) resume {
+ [audioSourcePlayer play];
+}
+
+-(BOOL) isPlaying {
+ if (state != kLAS_Init) {
+ return [audioSourcePlayer isPlaying];
+ } else {
+ return NO;
+ }
+}
+
+-(void) setVolume:(float) newVolume
+{
+ volume = newVolume;
+ if (state != kLAS_Init && !mute) {
+ audioSourcePlayer.volume = newVolume;
+ }
+}
+
+-(float) volume
+{
+ return volume;
+}
+
+#pragma mark Audio Interrupt Protocol
+-(BOOL) mute
+{
+ return mute;
+}
+
+-(void) setMute:(BOOL) muteValue
+{
+ if (mute != muteValue) {
+ if (mute) {
+ //Turn sound back on
+ audioSourcePlayer.volume = volume;
+ } else {
+ audioSourcePlayer.volume = 0.0f;
+ }
+ mute = muteValue;
+ }
+}
+
+-(BOOL) enabled
+{
+ return enabled_;
+}
+
+-(void) setEnabled:(BOOL)enabledValue
+{
+ if (enabledValue != enabled_) {
+ enabled_ = enabledValue;
+ if (!enabled_) {
+ //"Stop" the sounds
+ [self pause];
+ [self rewind];
+ }
+ }
+}
+
+-(NSInteger) numberOfLoops {
+ return numberOfLoops;
+}
+
+-(void) setNumberOfLoops:(NSInteger) loopCount
+{
+ audioSourcePlayer.numberOfLoops = loopCount;
+ numberOfLoops = loopCount;
+}
+
+- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - audio player finished");
+#if TARGET_IPHONE_SIMULATOR
+ CDLOGINFO(@"Denshion::CDLongAudioSource - workaround for OpenAL clobbered audio issue");
+ //This is a workaround for an issue in all simulators (tested to 3.1.2). Problem is
+ //that OpenAL audio playback is clobbered when an AVAudioPlayer stops. Workaround
+ //is to keep the player playing on an endless loop with 0 volume and then when
+ //it is played again reset the volume and set loop count appropriately.
+ //NB: this workaround is not foolproof but it is good enough for most situations.
+ player.numberOfLoops = -1;
+ player.volume = 0;
+ [player play];
+#endif
+ if (delegate && [delegate respondsToSelector:@selector(cdAudioSourceDidFinishPlaying:)]) {
+ [delegate cdAudioSourceDidFinishPlaying:self];
+ }
+}
+
+-(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - audio player interrupted");
+}
+
+-(void)audioPlayerEndInterruption:(AVAudioPlayer *)player {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - audio player resumed");
+ if (self.backgroundMusic) {
+ //Check if background music can play as rules may have changed during
+ //the interruption. This is to address a specific issue in 4.x when
+ //fast task switching
+ if([CDAudioManager sharedManager].willPlayBackgroundMusic) {
+ [player play];
+ }
+ } else {
+ [player play];
+ }
+}
+
+@end
+
+
+@interface CDAudioManager (PrivateMethods)
+-(BOOL) audioSessionSetActive:(BOOL) active;
+-(BOOL) audioSessionSetCategory:(NSString*) category;
+-(void) badAlContextHandler;
+@end
+
+
+@implementation CDAudioManager
+#define BACKGROUND_MUSIC_CHANNEL kASC_Left
+
+@synthesize soundEngine, willPlayBackgroundMusic;
+static CDAudioManager *sharedManager;
+static tAudioManagerState _sharedManagerState = kAMStateUninitialised;
+static tAudioManagerMode configuredMode;
+static BOOL configured = FALSE;
+
+-(BOOL) audioSessionSetActive:(BOOL) active {
+ NSError *activationError = nil;
+ if ([[AVAudioSession sharedInstance] setActive:active error:&activationError]) {
+ _audioSessionActive = active;
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session set active %i succeeded", active);
+ return YES;
+ } else {
+ //Failed
+ CDLOG(@"Denshion::CDAudioManager - Audio session set active %i failed with error %@", active, activationError);
+ return NO;
+ }
+}
+
+-(BOOL) audioSessionSetCategory:(NSString*) category {
+ NSError *categoryError = nil;
+ if ([[AVAudioSession sharedInstance] setCategory:category error:&categoryError]) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session set category %@ succeeded", category);
+ return YES;
+ } else {
+ //Failed
+ CDLOG(@"Denshion::CDAudioManager - Audio session set category %@ failed with error %@", category, categoryError);
+ return NO;
+ }
+}
+
+// Init
++ (CDAudioManager *) sharedManager
+{
+ @synchronized(self) {
+ if (!sharedManager) {
+ if (!configured) {
+ //Set defaults here
+ configuredMode = kAMM_FxPlusMusicIfNoOtherAudio;
+ }
+ sharedManager = [[CDAudioManager alloc] init:configuredMode];
+ _sharedManagerState = kAMStateInitialised;//This is only really relevant when using asynchronous initialisation
+ [[NSNotificationCenter defaultCenter] postNotificationName:kCDN_AudioManagerInitialised object:nil];
+ }
+ }
+ return sharedManager;
+}
+
++ (tAudioManagerState) sharedManagerState {
+ return _sharedManagerState;
+}
+
+/**
+ * Call this to set up audio manager asynchronously. Initialisation is finished when sharedManagerState == kAMStateInitialised
+ */
++ (void) initAsynchronously: (tAudioManagerMode) mode {
+ @synchronized(self) {
+ if (_sharedManagerState == kAMStateUninitialised) {
+ _sharedManagerState = kAMStateInitialising;
+ [CDAudioManager configure:mode];
+ CDAsynchInitialiser *initOp = [[[CDAsynchInitialiser alloc] init] autorelease];
+ NSOperationQueue *opQ = [[[NSOperationQueue alloc] init] autorelease];
+ [opQ addOperation:initOp];
+ }
+ }
+}
+
++ (id) alloc
+{
+ @synchronized(self) {
+ NSAssert(sharedManager == nil, @"Attempted to allocate a second instance of a singleton.");
+ return [super alloc];
+ }
+ return nil;
+}
+
+/*
+ * Call this method before accessing the shared manager in order to configure the shared audio manager
+ */
++ (void) configure: (tAudioManagerMode) mode {
+ configuredMode = mode;
+ configured = TRUE;
+}
+
+-(BOOL) isOtherAudioPlaying {
+ UInt32 isPlaying = 0;
+ UInt32 varSize = sizeof(isPlaying);
+ AudioSessionGetProperty (kAudioSessionProperty_OtherAudioIsPlaying, &varSize, &isPlaying);
+ return (isPlaying != 0);
+}
+
+-(void) setMode:(tAudioManagerMode) mode {
+
+ _mode = mode;
+ switch (_mode) {
+
+ case kAMM_FxOnly:
+ //Share audio with other app
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio will be shared");
+ //_audioSessionCategory = kAudioSessionCategory_AmbientSound;
+ _audioSessionCategory = AVAudioSessionCategoryAmbient;
+ willPlayBackgroundMusic = NO;
+ break;
+
+ case kAMM_FxPlusMusic:
+ //Use audio exclusively - if other audio is playing it will be stopped
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_SoloAmbientSound;
+ _audioSessionCategory = AVAudioSessionCategorySoloAmbient;
+ willPlayBackgroundMusic = YES;
+ break;
+
+ case kAMM_MediaPlayback:
+ //Use audio exclusively, ignore mute switch and sleep
+ CDLOGINFO(@"Denshion::CDAudioManager - Media playback mode, audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_MediaPlayback;
+ _audioSessionCategory = AVAudioSessionCategoryPlayback;
+ willPlayBackgroundMusic = YES;
+ break;
+
+ case kAMM_PlayAndRecord:
+ //Use audio exclusively, ignore mute switch and sleep, has inputs and outputs
+ CDLOGINFO(@"Denshion::CDAudioManager - Play and record mode, audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_PlayAndRecord;
+ _audioSessionCategory = AVAudioSessionCategoryPlayAndRecord;
+ willPlayBackgroundMusic = YES;
+ break;
+
+ default:
+ //kAudioManagerFxPlusMusicIfNoOtherAudio
+ if ([self isOtherAudioPlaying]) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Other audio is playing audio will be shared");
+ //_audioSessionCategory = kAudioSessionCategory_AmbientSound;
+ _audioSessionCategory = AVAudioSessionCategoryAmbient;
+ willPlayBackgroundMusic = NO;
+ } else {
+ CDLOGINFO(@"Denshion::CDAudioManager - Other audio is not playing audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_SoloAmbientSound;
+ _audioSessionCategory = AVAudioSessionCategorySoloAmbient;
+ willPlayBackgroundMusic = YES;
+ }
+
+ break;
+ }
+
+ [self audioSessionSetCategory:_audioSessionCategory];
+
+}
+
+/**
+ * This method is used to work around various bugs introduced in 4.x OS versions. In some circumstances the
+ * audio session is interrupted but never resumed, this results in the loss of OpenAL audio when following
+ * standard practices. If we detect this situation then we will attempt to resume the audio session ourselves.
+ * Known triggers: lock the device then unlock it (iOS 4.2 gm), playback a song using MPMediaPlayer (iOS 4.0)
+ */
+- (void) badAlContextHandler {
+ if (_interrupted && alcGetCurrentContext() == NULL) {
+ CDLOG(@"Denshion::CDAudioManager - bad OpenAL context detected, attempting to resume audio session");
+ [self audioSessionResumed];
+ }
+}
+
+- (id) init: (tAudioManagerMode) mode {
+ if ((self = [super init])) {
+
+ //Initialise the audio session
+ AVAudioSession* session = [AVAudioSession sharedInstance];
+ session.delegate = self;
+
+ _mode = mode;
+ backgroundMusicCompletionSelector = nil;
+ _isObservingAppEvents = FALSE;
+ _mute = NO;
+ _resigned = NO;
+ _interrupted = NO;
+ enabled_ = YES;
+ _audioSessionActive = NO;
+ [self setMode:mode];
+ soundEngine = [[CDSoundEngine alloc] init];
+
+ //Set up audioSource channels
+ audioSourceChannels = [[NSMutableArray alloc] init];
+ CDLongAudioSource *leftChannel = [[CDLongAudioSource alloc] init];
+ leftChannel.backgroundMusic = YES;
+ CDLongAudioSource *rightChannel = [[CDLongAudioSource alloc] init];
+ rightChannel.backgroundMusic = NO;
+ [audioSourceChannels insertObject:leftChannel atIndex:kASC_Left];
+ [audioSourceChannels insertObject:rightChannel atIndex:kASC_Right];
+ [leftChannel release];
+ [rightChannel release];
+ //Used to support legacy APIs
+ backgroundMusic = [self audioSourceForChannel:BACKGROUND_MUSIC_CHANNEL];
+ backgroundMusic.delegate = self;
+
+ //Add handler for bad al context messages, these are posted by the sound engine.
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(badAlContextHandler) name:kCDN_BadAlContext object:nil];
+
+ }
+ return self;
+}
+
+-(void) dealloc {
+ CDLOGINFO(@"Denshion::CDAudioManager - deallocating");
+ [self stopBackgroundMusic];
+ [soundEngine release];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self audioSessionSetActive:NO];
+ [audioSourceChannels release];
+ [super dealloc];
+}
+
+/** Retrieves the audio source for the specified channel */
+-(CDLongAudioSource*) audioSourceForChannel:(tAudioSourceChannel) channel
+{
+ return (CDLongAudioSource*)[audioSourceChannels objectAtIndex:channel];
+}
+
+/** Loads the data from the specified file path to the channel's audio source */
+-(CDLongAudioSource*) audioSourceLoad:(NSString*) filePath channel:(tAudioSourceChannel) channel
+{
+ CDLongAudioSource *audioSource = [self audioSourceForChannel:channel];
+ if (audioSource) {
+ [audioSource load:filePath];
+ }
+ return audioSource;
+}
+
+-(BOOL) isBackgroundMusicPlaying {
+ return [self.backgroundMusic isPlaying];
+}
+
+//NB: originally I tried using a route change listener and intended to store the current route,
+//however, on a 3gs running 3.1.2 no route change is generated when the user switches the
+//ringer mute switch to off (i.e. enables sound) therefore polling is the only reliable way to
+//determine ringer switch state
+-(BOOL) isDeviceMuted {
+
+#if TARGET_IPHONE_SIMULATOR
+ //Calling audio route stuff on the simulator causes problems
+ return NO;
+#else
+ CFStringRef newAudioRoute;
+ UInt32 propertySize = sizeof (CFStringRef);
+
+ AudioSessionGetProperty (
+ kAudioSessionProperty_AudioRoute,
+ &propertySize,
+ &newAudioRoute
+ );
+
+ if (newAudioRoute == NULL) {
+ //Don't expect this to happen but playing safe otherwise a null in the CFStringCompare will cause a crash
+ return YES;
+ } else {
+ CFComparisonResult newDeviceIsMuted = CFStringCompare (
+ newAudioRoute,
+ (CFStringRef) @"",
+ 0
+ );
+
+ return (newDeviceIsMuted == kCFCompareEqualTo);
+ }
+#endif
+}
+
+#pragma mark Audio Interrupt Protocol
+
+-(BOOL) mute {
+ return _mute;
+}
+
+-(void) setMute:(BOOL) muteValue {
+ if (muteValue != _mute) {
+ _mute = muteValue;
+ [soundEngine setMute:muteValue];
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ audioSource.mute = muteValue;
+ }
+ }
+}
+
+-(BOOL) enabled {
+ return enabled_;
+}
+
+-(void) setEnabled:(BOOL) enabledValue {
+ if (enabledValue != enabled_) {
+ enabled_ = enabledValue;
+ [soundEngine setEnabled:enabled_];
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ audioSource.enabled = enabled_;
+ }
+ }
+}
+
+-(CDLongAudioSource*) backgroundMusic
+{
+ return backgroundMusic;
+}
+
+//Load background music ready for playing
+-(void) preloadBackgroundMusic:(NSString*) filePath
+{
+ [self.backgroundMusic load:filePath];
+}
+
+-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop
+{
+ [self.backgroundMusic load:filePath];
+
+ if (!willPlayBackgroundMusic || _mute) {
+ CDLOGINFO(@"Denshion::CDAudioManager - play bgm aborted because audio is not exclusive or sound is muted");
+ return;
+ }
+
+ if (loop) {
+ [self.backgroundMusic setNumberOfLoops:-1];
+ } else {
+ [self.backgroundMusic setNumberOfLoops:0];
+ }
+ [self.backgroundMusic play];
+}
+
+-(void) stopBackgroundMusic
+{
+ [self.backgroundMusic stop];
+}
+
+-(void) pauseBackgroundMusic
+{
+ [self.backgroundMusic pause];
+}
+
+-(void) resumeBackgroundMusic
+{
+ if (!willPlayBackgroundMusic || _mute) {
+ CDLOGINFO(@"Denshion::CDAudioManager - resume bgm aborted because audio is not exclusive or sound is muted");
+ return;
+ }
+
+ [self.backgroundMusic resume];
+}
+
+-(void) rewindBackgroundMusic
+{
+ [self.backgroundMusic rewind];
+}
+
+-(void) setBackgroundMusicCompletionListener:(id) listener selector:(SEL) selector {
+ backgroundMusicCompletionListener = listener;
+ backgroundMusicCompletionSelector = selector;
+}
+
+/*
+ * Call this method to have the audio manager automatically handle application resign and
+ * become active. Pass a tAudioManagerResignBehavior to indicate the desired behavior
+ * for resigning and becoming active again.
+ *
+ * If autohandle is YES then the applicationWillResignActive and applicationDidBecomActive
+ * methods are automatically called, otherwise you must call them yourself at the appropriate time.
+ *
+ * Based on idea of Dominique Bongard
+ */
+-(void) setResignBehavior:(tAudioManagerResignBehavior) resignBehavior autoHandle:(BOOL) autoHandle {
+
+ if (!_isObservingAppEvents && autoHandle) {
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:@"UIApplicationWillResignActiveNotification" object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:@"UIApplicationDidBecomeActiveNotification" object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:@"UIApplicationWillTerminateNotification" object:nil];
+ _isObservingAppEvents = TRUE;
+ }
+ _resignBehavior = resignBehavior;
+}
+
+- (void) applicationWillResignActive {
+ self->_resigned = YES;
+
+ //Set the audio sesssion to one that allows sharing so that other audio won't be clobbered on resume
+ [self audioSessionSetCategory:AVAudioSessionCategoryAmbient];
+
+ switch (_resignBehavior) {
+
+ case kAMRBStopPlay:
+
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ if (audioSource.isPlaying) {
+ audioSource->systemPaused = YES;
+ audioSource->systemPauseLocation = audioSource.audioSourcePlayer.currentTime;
+ [audioSource stop];
+ } else {
+ //Music is either paused or stopped, if it is paused it will be restarted
+ //by OS so we will stop it.
+ audioSource->systemPaused = NO;
+ [audioSource stop];
+ }
+ }
+ break;
+
+ case kAMRBStop:
+ //Stop music regardless of whether it is playing or not because if it was paused
+ //then the OS would resume it
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ [audioSource stop];
+ }
+
+ default:
+ break;
+
+ }
+ CDLOGINFO(@"Denshion::CDAudioManager - handled resign active");
+}
+
+//Called when application resigns active only if setResignBehavior has been called
+- (void) applicationWillResignActive:(NSNotification *) notification
+{
+ [self applicationWillResignActive];
+}
+
+- (void) applicationDidBecomeActive {
+
+ if (self->_resigned) {
+ _resigned = NO;
+ //Reset the mode incase something changed with audio while we were inactive
+ [self setMode:_mode];
+ switch (_resignBehavior) {
+
+ case kAMRBStopPlay:
+
+ //Music had been stopped but stop maintains current time
+ //so playing again will continue from where music was before resign active.
+ //We check if music can be played because while we were inactive the user might have
+ //done something that should force music to not play such as starting a track in the iPod
+ if (self.willPlayBackgroundMusic) {
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ if (audioSource->systemPaused) {
+ [audioSource resume];
+ audioSource->systemPaused = NO;
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+
+ }
+ CDLOGINFO(@"Denshion::CDAudioManager - audio manager handled become active");
+ }
+}
+
+//Called when application becomes active only if setResignBehavior has been called
+- (void) applicationDidBecomeActive:(NSNotification *) notification
+{
+ [self applicationDidBecomeActive];
+}
+
+//Called when application terminates only if setResignBehavior has been called
+- (void) applicationWillTerminate:(NSNotification *) notification
+{
+ CDLOGINFO(@"Denshion::CDAudioManager - audio manager handling terminate");
+ [self stopBackgroundMusic];
+}
+
+/** The audio source completed playing */
+- (void) cdAudioSourceDidFinishPlaying:(CDLongAudioSource *) audioSource {
+ CDLOGINFO(@"Denshion::CDAudioManager - audio manager got told background music finished");
+ if (backgroundMusicCompletionSelector != nil) {
+ [backgroundMusicCompletionListener performSelector:backgroundMusicCompletionSelector];
+ }
+}
+
+-(void) beginInterruption {
+ CDLOGINFO(@"Denshion::CDAudioManager - begin interruption");
+ [self audioSessionInterrupted];
+}
+
+-(void) endInterruption {
+ CDLOGINFO(@"Denshion::CDAudioManager - end interruption");
+ [self audioSessionResumed];
+}
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000
+-(void) endInterruptionWithFlags:(NSUInteger)flags {
+ CDLOGINFO(@"Denshion::CDAudioManager - interruption ended with flags %i",flags);
+ if (flags == AVAudioSessionInterruptionFlags_ShouldResume) {
+ [self audioSessionResumed];
+ }
+}
+#endif
+
+-(void)audioSessionInterrupted
+{
+ if (!_interrupted) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session interrupted");
+ _interrupted = YES;
+
+ // Deactivate the current audio session
+ [self audioSessionSetActive:NO];
+
+ if (alcGetCurrentContext() != NULL) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Setting OpenAL context to NULL");
+
+ ALenum error = AL_NO_ERROR;
+
+ // set the current context to NULL will 'shutdown' openAL
+ alcMakeContextCurrent(NULL);
+
+ if((error = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDAudioManager - Error making context current %x\n", error);
+ }
+ #pragma unused(error)
+ }
+ }
+}
+
+-(void)audioSessionResumed
+{
+ if (_interrupted) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session resumed");
+ _interrupted = NO;
+
+ BOOL activationResult = NO;
+ // Reactivate the current audio session
+ activationResult = [self audioSessionSetActive:YES];
+
+ //This code is to handle a problem with iOS 4.0 and 4.01 where reactivating the session can fail if
+ //task switching is performed too rapidly. A test case that reliably reproduces the issue is to call the
+ //iPhone and then hang up after two rings (timing may vary ;))
+ //Basically we keep waiting and trying to let the OS catch up with itself but the number of tries is
+ //limited.
+ if (!activationResult) {
+ CDLOG(@"Denshion::CDAudioManager - Failure reactivating audio session, will try wait-try cycle");
+ int activateCount = 0;
+ while (!activationResult && activateCount < 10) {
+ [NSThread sleepForTimeInterval:0.5];
+ activationResult = [self audioSessionSetActive:YES];
+ activateCount++;
+ CDLOGINFO(@"Denshion::CDAudioManager - Reactivation attempt %i status = %i",activateCount,activationResult);
+ }
+ }
+
+ if (alcGetCurrentContext() == NULL) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Restoring OpenAL context");
+ ALenum error = AL_NO_ERROR;
+ // Restore open al context
+ alcMakeContextCurrent([soundEngine openALContext]);
+ if((error = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDAudioManager - Error making context current%x\n", error);
+ }
+ #pragma unused(error)
+ }
+ }
+}
+
++(void) end {
+ [sharedManager release];
+ sharedManager = nil;
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+@implementation CDLongAudioSourceFader
+
+-(void) _setTargetProperty:(float) newVal {
+ ((CDLongAudioSource*)target).volume = newVal;
+}
+
+-(float) _getTargetProperty {
+ return ((CDLongAudioSource*)target).volume;
+}
+
+-(void) _stopTarget {
+ //Pause instead of stop as stop releases resources and causes problems in the simulator
+ [((CDLongAudioSource*)target) pause];
+}
+
+-(Class) _allowableType {
+ return [CDLongAudioSource class];
+}
+
+@end
+///////////////////////////////////////////////////////////////////////////////////////
+@implementation CDBufferManager
+
+-(id) initWithEngine:(CDSoundEngine *) theSoundEngine {
+ if ((self = [super init])) {
+ soundEngine = theSoundEngine;
+ loadedBuffers = [[NSMutableDictionary alloc] initWithCapacity:CD_BUFFERS_START];
+ freedBuffers = [[NSMutableArray alloc] init];
+ nextBufferId = 0;
+ }
+ return self;
+}
+
+-(void) dealloc {
+ [loadedBuffers release];
+ [freedBuffers release];
+ [super dealloc];
+}
+
+-(int) bufferForFile:(NSString*) filePath create:(BOOL) create {
+
+ NSNumber* soundId = (NSNumber*)[loadedBuffers objectForKey:filePath];
+ if(soundId == nil)
+ {
+ if (create) {
+ NSNumber* bufferId = nil;
+ //First try to get a buffer from the free buffers
+ if ([freedBuffers count] > 0) {
+ bufferId = [[[freedBuffers lastObject] retain] autorelease];
+ [freedBuffers removeLastObject];
+ CDLOGINFO(@"Denshion::CDBufferManager reusing buffer id %i",[bufferId intValue]);
+ } else {
+ bufferId = [[NSNumber alloc] initWithInt:nextBufferId];
+ [bufferId autorelease];
+ CDLOGINFO(@"Denshion::CDBufferManager generating new buffer id %i",[bufferId intValue]);
+ nextBufferId++;
+ }
+
+ if ([soundEngine loadBuffer:[bufferId intValue] filePath:filePath]) {
+ //File successfully loaded
+ CDLOGINFO(@"Denshion::CDBufferManager buffer loaded %@ %@",bufferId,filePath);
+ [loadedBuffers setObject:bufferId forKey:filePath];
+ return [bufferId intValue];
+ } else {
+ //File didn't load, put buffer id on free list
+ [freedBuffers addObject:bufferId];
+ return kCDNoBuffer;
+ }
+ } else {
+ //No matching buffer was found
+ return kCDNoBuffer;
+ }
+ } else {
+ return [soundId intValue];
+ }
+}
+
+-(void) releaseBufferForFile:(NSString *) filePath {
+ int bufferId = [self bufferForFile:filePath create:NO];
+ if (bufferId != kCDNoBuffer) {
+ [soundEngine unloadBuffer:bufferId];
+ [loadedBuffers removeObjectForKey:filePath];
+ NSNumber *freedBufferId = [[NSNumber alloc] initWithInt:bufferId];
+ [freedBufferId autorelease];
+ [freedBuffers addObject:freedBufferId];
+ }
+}
+@end
+
+
+