Подтвердить что ты не робот

Sprite Kit и воспроизведение звука приводят к завершению работы приложения

с использованием ARC

Просто проблема, с которой я столкнулся - У меня есть SKScene, в котором я играю звук fx, используя метод класса SKAction

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

Теперь, когда я пытаюсь перейти на задний план, независимо от того, что звук закончился, видимо, iOS завершает мое приложение из-за gpus_ReturnNotPermittedKillClient.

Теперь, только когда я комментирую эту строку и не запускаю действие, iOS отлично работает в фоновом режиме (конечно, приостановлено, но без прерывания).

Что я делаю неправильно?

EDIT: iOS не прекратит приложение, если строка не была запущена - скажем, если она была в if statement, которая не была запущена (soundOn == YES) или что-то в этом роде, когда bool false

4b9b3361

Ответ 1

Проблема заключается в том, что AVAudioSession не может быть активным, пока приложение переходит в фоновый режим. Это не сразу видно, потому что Sprite Kit не упоминает, что он использует AVAudioSession внутри.

Исправление довольно простое, а также применительно к ObjectAL = > отключить AVAudioSession, когда приложение находится в фоновом режиме, и повторно активировать сеанс аудио, когда приложение переходит на передний план.

Упрощенный AppDelegate с этим исправлением выглядит так:

#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: это исправление будет включено в Kobold Kit v7.0.3.

Ответ 2

Я обнаружил, что все это касается дезактивации AVAudioSession в приложении AppDelegate applicationDidEnterBackground:, но часто с ошибкой (без деактивации):

Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn’t be completed. (OSStatus error 560030580.)

который все еще приводит к описанному здесь сбою: При вводе фона Spritekit падает.

Итак, недостаточно для установкиActive: NO - мы должны деактивировать его эффективно (без этой ошибки). Я сделал простое решение, добавив специальный метод экземпляра в AppDelegate, который дезактивирует AVAudioSession, пока нет ошибки.

Короче говоря, это выглядит так:

- (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:

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)

Это очень коротко, потому что он не заботится о stackoverflow:), если AVAudioSession не хочет закрываться после нескольких тысяч попыток (тогда также неизбежна авария). Таким образом, это можно рассматривать только как взлом, пока Apple не исправит его. Кстати, стоит также взять под контроль запуск AVAudioSession.

Полное решение может выглядеть так:

- (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 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;
    }
}

Эта проблема, однако, представляет собой кусок торта по сравнению с прерываниями AVAudioSession в приложении SpriteKit. Если мы не справимся с этим, рано или поздно мы столкнемся с большими проблемами с утечками памяти и процессором 99% (56% из [SKSoundSource queuedBufferCount] и 34% из [SKSoundSource isPlaying] - см. Инструменты), потому что SpriteKit упрям ​​и "воспроизводит" звуки, даже если они не могут быть воспроизведены:)

Насколько я нашел, самый простой способ - setCategory: AVAudioSessionCategoryPlayAndRecord withOptions: AVAudioSessionCategoryOptionMixWith Other. Любые другие категории AVAudioSession, я думаю, должны избегать playFileNamed: вообще. Это можно сделать, создав собственную SKNode runAction: категорию для воспроизведения звукового метода, например, с помощью AVAudioPlayer. Но это отдельная тема.

Мое полное все-в-одном решение с реализацией AVAudioPlayer находится здесь: http://iknowsomething.com/ios-sdk-spritekit-sound/

Изменить: Исправлен недостающий пар.

Ответ 3

У меня была аналогичная проблема с воспроизведением звука (хотя я не использую звук в SKAction node) с тем же фоном в результате.

Я попытался решить эту проблему, установив свойство paused моего SKScene на YES, но когда звук воспроизводится, в SpriteKit появляется ошибка. В этой ситуации метод обновления фактически вызван после паузы установлен на YES. Вот мой код обновления:

- (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];
}

Когда этот NSLog будет прослежен, приложение затем сбой с ошибкой GL.

Единственный способ, которым я нашел решение, довольно тяжело. Я должен удалить и освободить весь свой SKView при входе в фоновый режим.

В applicationDidEnterBackground я вызываю функцию в моем ViewController, которая делает это:

[self.playView removeFromSuperview];

Убедитесь, что у вас нет сильных ссылок на SKView, поскольку он должен быть освобожден для этого.

В applicationWillEnterForeground Я вызываю функцию, которая перестраивает мой SKView следующим образом:

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];

Да, это похоже на общий взлом, но я думаю, что в SpriteKit есть ошибка.

Надеюсь, это поможет.

ИЗМЕНИТЬ

Отличный принятый ответ выше. К сожалению, это не работает для меня, потому что я использую звуковые очереди и нуждаюсь в музыке для продолжения воспроизведения, когда мое приложение находится в фоновом режиме.

Все еще ждет исправления от Apple.

Ответ 4

У меня была эта проблема, и, как видно из других ответов, кажется, что есть два решения: когда приложение становится неактивным, либо (1) перестает воспроизводить аудио, либо (2) срывает все ваши SKView. Затем вам может понадобиться отменить этот ответ, когда приложение снова станет активным.

Даже экраны SKView, которые в настоящее время не отображаются (например, те, которые связаны с контроллерами представлений ранее в стеке контроллера навигации), вызывают сбой.

Итак, я реализовал решение (2) здесь: https://github.com/jawj/GJMLessCrashySKView

Это простой подкласс UIView, который предлагает базовые функции SKView и пересылает их на свой собственный просмотр SKView. Тем не менее, очень важно, чтобы он сбрасывал этот SKView всякий раз, когда он выходил из экрана, или приложение уходит в отставку, и восстанавливает его, если и когда он возвращается на экран, и приложение активно. Если он отображается на экране, он заменяет снесенный SKView неподвижным снимком самого себя и снова исчезает, когда SKView перестраивается, так что, когда приложение неактивно, но все еще видимо (например, отображается предупреждение о батарее UIAlert, или мы 'дважды нажал кнопку "домой" для переключателя приложений) все выглядит нормально.

Это решение также фиксирует (возможно, не связанную) бонусную головную боль, которая заключается в том, что когда SKViews исчезают и снова появляются (например, поскольку контроллер модального представления представлен и затем уволен), они иногда замерзают.

Ответ 5

У меня была та же проблема, поэтому я использовал этот код для воспроизведения звуков вместо SKAction

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

и он решил мои проблемы, я надеюсь, что это поможет

Ответ 6

Мне удалось решить эту проблему, вызвав паузу и воспроизведение, когда фоновая/передняя часть приложения добавлена ​​в дополнение к активному/неактивному AVAudioSession (как указано в решении LearnCocos2D).

- (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];
}

Ответ 7

У меня была одна и та же проблема, и я решил ее по-другому (так как другие решения не работали). Я понятия не имею, почему мое решение работало так, если у кого-то есть объяснение, которое было бы удивительным.

В основном, я создавал новые экземпляры SKAction для каждого SKShapeNode, который требовал звуков. Поэтому я создал класс singleton с переменными экземпляра для каждого необходимого мне звука. Я ссылаюсь на эти переменные в каждом SKShapeNode и voila!

Здесь одноэлементный код:

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;
}

Затем раздаются звуки:

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

Ура!