I found that it's all about deactivating AVAudioSession in AppDelegate applicationDidEnterBackground:, but often it fails with error (no deactivation in effect):
Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn’t be completed. (OSStatus error 560030580.)
which still leads to the crash described here: Spritekit crashes when entering background.
So, it's not enough to setActive:NO - we have to deactivate it effectively (without that error). I made a simple solution by adding dedicated instance method to the AppDelegate which deactivates AVAudioSession as long as there is no error.
In short it looks like this:
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSLog(@"%s", __FUNCTION__);
[self stopAudio];
}
- (void)stopAudio {
NSError *error = nil;
[[AVAudioSession sharedInstance] setActive:NO error:&error];
NSLog(@"%s AudioSession Error: %@", __FUNCTION__, error);
if (error) [self stopAudio];
}
NSLog proof:
2014-01-25 11:41:48.426 MyApp[1957:60b] -[ATWAppDelegate applicationDidEnterBackground:]
2014-01-25 11:41:48.431 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn’t be completed. (OSStatus error 560030580.)"
2014-01-25 11:41:48.434 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn’t be completed. (OSStatus error 560030580.)"
2014-01-25 11:41:48.454 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn’t be completed. (OSStatus error 560030580.)"
2014-01-25 11:41:49.751 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: (null)
This is really short, because it doesn't care about stackoverflow :) if AVAudioSession don't want to close after several thousands tries (crash is inevitable then also). So, this can be only considered as a hack until Apple fix it.
By the way, it's worth also to take control over starting AVAudioSession.
Full solution can look like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"%s", __FUNCTION__);
[self startAudio];
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSLog(@"%s", __FUNCTION__);
// SpriteKit uses AVAudioSession for [SKAction playSoundFileNamed:]
// AVAudioSession cannot be active while the application is in the background,
// so we have to stop it when going in to background
// and reactivate it when entering foreground.
// This prevents audio crash.
[self stopAudio];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
NSLog(@"%s", __FUNCTION__);
[self startAudio];
}
- (void)applicationWillTerminate:(UIApplication *)application {
NSLog(@"%s", __FUNCTION__);
[self stopAudio];
}
static BOOL isAudioSessionActive = NO;
- (void)startAudio {
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *error = nil;
if (audioSession.otherAudioPlaying) {
[audioSession setCategory: AVAudioSessionCategoryAmbient error:&error];
} else {
[audioSession setCategory: AVAudioSessionCategorySoloAmbient error:&error];
}
if (!error) {
[audioSession setActive:YES error:&error];
isAudioSessionActive = YES;
}
NSLog(@"%s AVAudioSession Category: %@ Error: %@", __FUNCTION__, [audioSession category], error);
}
- (void)stopAudio {
// Prevent background apps from duplicate entering if terminating an app.
if (!isAudioSessionActive) return;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *error = nil;
[audioSession setActive:NO error:&error];
NSLog(@"%s AVAudioSession Error: %@", __FUNCTION__, error);
if (error) {
// It's not enough to setActive:NO
// We have to deactivate it effectively (without that error),
// so try again (and again... until success).
[self stopAudio];
} else {
isAudioSessionActive = NO;
}
}
This problem, however, is a piece of cake comparing to AVAudioSession interruptions in SpriteKit app. If we don't handle it, sooner or later we get into big troubles with memory leaks and CPU 99% (56% from [SKSoundSource queuedBufferCount] and 34% from [SKSoundSource isPlaying] - see Instruments), because SpriteKit is stubborn and "plays" sounds even they can't be played :)
As far as I found the easiest way is to setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers. Any other AVAudioSession categories needs, I think, to avoid playFileNamed: at all. This can be done by making your own SKNode runAction: category for playing sounds method for example with AVAudioPlayer. But this is separate topic.
My full all-in-one solution with AVAudioPlayer implementation is here: http://iknowsomething.com/ios-sdk-spritekit-sound/
Edit: Fixed missing paren.