Question

using ARC

Just a problem I've run into- I have an SKScene in which I play a sound fx using SKAction class method

[SKAction playSoundFileNamed:@"sound.wav" waitForCompletion:NO];

Now when I try to go to background, no matter that the sound was over, apparently iOS is terminating my app due to gpus_ReturnNotPermittedKillClient.

Now only when I comment this line and not running the action iOS runs it great in background (of course, paused, but without termination).

What am I doing wrong?

EDIT: iOS will not terminate the app if the line wasn't run- say, if it was in an if statement that wasn't run (soundOn == YES) or something like that, when the bool is false

Was it helpful?

Solution

The problem is AVAudioSession can't be active while the app enters background. This isn't immediately obvious because Sprite Kit makes no mention that it uses AVAudioSession internally.

The fix is quite simple, and also applies to ObjectAL => set the AVAudioSession to inactive while the app is in background, and reactivate the audio session when the app enters foreground.

A simplified AppDelegate with this fix looks like so:

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

- (void)applicationWillResignActive:(UIApplication *)application
{
    // prevent audio crash
    [[AVAudioSession sharedInstance] setActive:NO error:nil];
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // prevent audio crash
    [[AVAudioSession sharedInstance] setActive:NO error:nil];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    // resume audio
    [[AVAudioSession sharedInstance] setActive:YES error:nil];
}

PS: this fix will be included in Kobold Kit v7.0.3.

OTHER TIPS

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.

I've had a similar problem with playing audio (though I'm not using audio in an SKAction node) with the same background crash as a result.

I tried to solve this by setting the paused property of my SKScene to YES, but when audio is playing there appears to be a bug in SpriteKit. In this situation, the update method actually gets called after paused is set to YES. Here is my update code:

- (void)update:(CFTimeInterval)currentTime
{
    /* Called before each frame is rendered */

    if (self.paused)
    {
        // Apple bug?
        NSLog(@"update: called while SKView.paused == YES!");
        return;
    }

    // update!
    [_activeLayer update];
}

When that NSLog is traced out, the app will then crash with the GL error.

The only way I've found to solve it is quite heavy handed. I have to remove and deallocate my entire SKView when entering the background.

In applicationDidEnterBackground I call a function in my ViewController that does this:

[self.playView removeFromSuperview];

Ensure that you do not have any strong references to the SKView as it must be deallocated for this to work.

In applicationWillEnterForeground I call a function that rebuilds my SKView like this:

CGRect rect = self.view.frame;

if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation))
{
    CGFloat temp = rect.size.width;
    rect.size.width = rect.size.height;
    rect.size.height = temp;
}

SKView *skView = [[SKView alloc] initWithFrame:rect];

skView.autoresizingMask = UIViewAutoresizingFlexibleHeight |
                        UIViewAutoresizingFlexibleWidth;

[self.view insertSubview:skView atIndex:0];
self.playView = skView;

// Create and configure the scene.
self.myScene = [CustomScene sceneWithSize:skView.bounds.size];
self.myScene.scaleMode = SKSceneScaleModeResizeFill;

// Present the scene.
[skView presentScene:self.myScene];

Yeah, this feels like a total hack, but I think there is a bug in SpriteKit.

I hope this helps.

EDIT

Great accepted answer above. Unfortunately it doesn't work for me because I am using Audio Queues and need music to continue playing when my app is in the background.

Still waiting for a fix from Apple.

I've had this problem, and as seen in the other answers it seems there are two solutions: when the app becomes inactive, either (1) stop playing audio, or (2) tear down all your SKViews. You may then need to undo that response when the app becomes active again.

Even SKViews that are not currently on screen (e.g. those that are associated with view controllers earlier in a navigation controller stack) will cause a crash.

So, I've implemented solution (2) here: https://github.com/jawj/GJMLessCrashySKView

It's a simple UIView subclass that offers the basic functions of an SKView and forwards them on to its own SKView subview. Crucially, though, it tears down this SKView whenever it goes off screen or the app resigns active, and rebuilds it if and when it comes back on screen and the app is active. If on-screen, it replaces the torn down SKView with a still snapshot of itself, and fades that out again when the SKView is rebuilt, so that when the app is inactive but still visible (e.g. a battery warning UIAlert is shown, or we've double-clicked the home button for the app switcher) everything looks normal.

This solution also fixes a (possibly-unrelated) bonus headache, which is that when SKViews are disappeared and reappeared (e.g. because a modal view controller is presented and then dismissed), they sometimes freeze up.

I was able to resolve this by calling pause and play when background/foregrounding the application in addition to setting the AVAudioSession active/inactive (as mentioned in LearnCocos2D's solution).

- (void)applicationWillResignActive:(UIApplication *)application
{
    [self.audioPlayer pause];
    [[AVAudioSession sharedInstance] setActive:NO error:nil];
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [self.audioPlayer pause];
    [[AVAudioSession sharedInstance] setActive:NO error:nil];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    [[AVAudioSession sharedInstance] setActive:YES error:nil];
    [self.audioPlayer play];
}

i had the same problem , so i used this code to play sounds instead of SKAction

SystemSoundID soundID;
NSString *filename = @"soundFileName";
CFBundleRef mainBundle = CFBundleGetMainBundle ();
CFURLRef soundFileUrl = CFBundleCopyResourceURL(mainBundle, (__bridge             CFStringRef)filename, CFSTR("wav"), NULL);
AudioServicesCreateSystemSoundID(soundFileUrl, &soundID);
AudioServicesPlaySystemSound(soundID);

and it solved my problems , i hope this helps

I was having the exact same issue and I solved it a different way (since the other solutions didn't work). I have no idea why my solution worked so if someone has an explanation that would be awesome.

Basically, I was creating new SKAction instances for every SKShapeNode that required sounds. So I created a singleton class with instance variables for each sound I needed. I references those variables in each SKShapeNode and voila!

Here's the singleton code:

CAAudio.h

#import <Foundation/Foundation.h>
#import <SpriteKit/SpriteKit.h>

@interface CAAudio : NSObject

@property (nonatomic) SKAction *correctSound;
@property (nonatomic) SKAction *incorrectSound;

+ (id)sharedAudio;

@end

CAAudio.m

+ (id)sharedAudio {
    static CAAudio *sharedAudio = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        sharedAudio = [self new];

        sharedAudio.correctSound =
            [SKAction playSoundFileNamed:@"correct.wav" waitForCompletion:YES];

        sharedAudio.incorrectSound =
            [SKAction playSoundFileNamed:@"incorrect.wav" waitForCompletion:YES];
    });

    return sharedAudio;
}

The sounds are then accessed like so:

SKAction *sound = [[CAAudio sharedAudio] correctSound];

Cheers!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top