Sprite Kit & playing sound leads to app termination
Asked Answered
L

7

27

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

Leucippus answered 24/9, 2013 at 8:44 Comment(5)
developer.apple.com/library/ios/qa/qa1766/_index.htmlCalley
it is quite irrelevant to sprite kit- it does the Open-GL part, not meLeucippus
It's hard to tell from the information here, but this might be a bug on Apple's side. I'd suggest providing details (and a sample project) at bugreport.apple.com.Agonizing
do you have any other action going on at this scene? I play sound also in my Sprite Kit game and when game goes to background it just fades out the sounds...Begot
Sprite kit is just acting weird. No way to tell- when I add the sample project a sound it goes perfectly but when I put it in my already made game, bam crash!Leucippus
H
28

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.

Hollywood answered 9/10, 2013 at 22:11 Comment(2)
This solution does not seem applicable in iOS7.1 anymore. Worked fine in iOS 7 however my app now crashes again #22407611Chemotaxis
I am still getting few crash reports in iOS8 even after applying this. :-/Triaxial
S
14

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.

Sometime answered 25/1, 2014 at 11:1 Comment(2)
BTW, I found that AVAudioPlayer renders sounds quite differently (way smoother) than SpriteKit (Audio Queue Services?).Sometime
I've been pounding my head on the wall over this. I set paused=YES and disabled audio (even though I wasn't using it!), and still consistent crash when backgrounding. It turned out to be TestFlight causing this! I commented out my TestFlight takeoff command in AppDelegate, and no more crashes! Argh.Gleeful
O
1

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.

Oxendine answered 30/9, 2013 at 5:8 Comment(3)
@harrym17 yes I have filed a bug report (no response yet). I agree it's a hack (and said so), but it solves the problem for me at least. My scene is easy to recreate. Others are having this problem and no one has yet found a proper solution that I am aware of.Oxendine
This is not working :( PS are you using NSNotifications? Because I think this has something to do with the crash...Leucippus
I'm not using NSNotifications. I'm sorry this doesn't help you. :( Apple engineering requested a test case from me and I gave them one so I hope they fix it on their end. It is easy to reproduce with little more than the default SpriteKit template project and an MP3 file.Oxendine
K
1

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.

Keefer answered 16/4, 2014 at 11:19 Comment(0)
C
1

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];
}
Complaint answered 23/5, 2014 at 0:46 Comment(0)
D
0

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

Dipsomania answered 10/5, 2014 at 11:44 Comment(0)
L
0

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!

Legitimize answered 31/7, 2015 at 14:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.