Why does this audio session fail to recognise an interruption?

余生颓废 提交于 2019-12-25 08:48:29

问题


My app synthesises audio from a lookup table. It plays audio successfully but crashes the moment I try to stop playing. Audio playback only needs to exit without restarting so the requirements for handling the interruption are basic. I reread Apple’s Audio Session Programming Guide including the section Responding to Interruptions. However the method handleAudioSessionInterruption does not seem to register an interrupt so I’m obviously missing something.


EDIT See my answer. When I began work on this I knew next to nothing about NSNotificationCenter so I welcome any suggestion for improvement.


Two methods set up the audio session to play in the foreground.

- (void)setUpAudio
{
    if (_playQueue == NULL)
    {
        if ([self setUpAudioSession] == TRUE)
        {
            [self setUpPlayQueue];
            [self setUpPlayQueueBuffers];
        }
    }
}


- (BOOL)setUpAudioSession
{
    BOOL success                        = NO;
    NSError *audioSessionError          = nil;

    AVAudioSession *session             = [AVAudioSession sharedInstance];

    // Set up notifications

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleAudioSessionInterruption:)
                                                 name:AVAudioSessionInterruptionNotification
                                               object:session];

    // Set category

    success                             = [session setCategory:AVAudioSessionCategoryPlayback
                                                         error:&audioSessionError];
    if (!success)
    {
        NSLog(@"%@ Error setting category: %@",
              NSStringFromSelector(_cmd), [audioSessionError localizedDescription]);

        // Exit early
        return success;
    }

    // Set mode

    success                             = [session setMode:AVAudioSessionModeDefault
                                                     error:&audioSessionError];
    if (!success)
    {
        NSLog(@"%@ Error setting mode: %@",
              NSStringFromSelector(_cmd), [audioSessionError localizedDescription]);

        // Exit early
        return success;
    }

    // Set some preferred values

    NSTimeInterval bufferDuration       = .005; // I would prefer a 5ms buffer duration

    success                             = [session setPreferredIOBufferDuration:bufferDuration
                                                                          error:&audioSessionError];
    if (audioSessionError)
    {
        NSLog(@"Error %ld, %@ %i", (long)audioSessionError.code, audioSessionError.localizedDescription, success);
    }

    double sampleRate                   = _audioFormat.mSampleRate; // I would prefer a sample rate of 44.1kHz

    success                             = [session setPreferredSampleRate:sampleRate
                                                                    error:&audioSessionError];
    if (audioSessionError)
    {
        NSLog(@"Error %ld, %@ %i", (long)audioSessionError.code, audioSessionError.localizedDescription, success);
    }

    success                             = [session setActive:YES
                                                       error:&audioSessionError];
    if (!success)
    {
        NSLog(@"%@ Error activating %@",
              NSStringFromSelector(_cmd), [audioSessionError localizedDescription]);
    }

    // Get current values

    sampleRate                          = session.sampleRate;
    bufferDuration                      = session.IOBufferDuration;
    NSLog(@"Sample Rate:%0.0fHz I/O Buffer Duration:%f", sampleRate, bufferDuration);

    return success;
}

And here is the method that handles the interruption when I press the stop button. However it does not respond.


EDIT The correct method needs block, not selector. See my answer.


- (void)handleAudioSessionInterruption:(NSNotification*)notification
{
    if (_playQueue)
    {
        NSNumber *interruptionType      = [[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey];
        NSNumber *interruptionOption    = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey];

        NSLog(@"in-app Audio playback will be stopped by %@ %lu", notification.name, (unsigned long)interruptionType.unsignedIntegerValue);

        switch (interruptionType.unsignedIntegerValue)
        {
            case AVAudioSessionInterruptionTypeBegan:
            {
                if (interruptionOption.unsignedIntegerValue == AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)
                {
                    NSLog(@"notify other apps that audio is now available");
                }
            }
                break;

            default:
                break;
        }
    }
}

回答1:


Answer My method to handle AudioSessionInterruption did not subscribe the observer correctly with NSNotificationCentre. This has been fixed by adding observer using block, not selector.

The solution replaces deprecated AVAudioSession delegate methods in AudioBufferPlayer, an extremely fit for purpose audio player initially developed for direct audio synthesis by Matthias Hollejmans. Several deprecated functions including InterruptionListenerCallback were later upgraded by Mario Diana. The solution (below) uses NSNotification allowing users to exit AVAudioSession gracefully by pressing a button.

Here is the relevant code.

PlayViewController.m

UIButton action performs an orderly shutdown of synth, invalidates the timer and posts the notification that will exit AVAudioSession

- (void)fromEscButton:(UIButton*)button
{
    [self stopConcertClock];

    ...                            // code for Exit PlayViewController not shown
}

- (void)stopConcertClock
{
    [_synthLock lock];
    [_synth stopAllNotes];
    [_synthLock unlock];
    [timer invalidate];
    timer = nil;
    [self postAVAudioSessionInterruptionNotification];
    NSLog(@"Esc button pressed or sequence ended. Exit PlayViewController ");
}


- (void) postAVAudioSessionInterruptionNotification
{
    [[NSNotificationCenter defaultCenter]
     postNotificationName:@"AVAudioSessionInterruptionNotification"
     object:self];
}

Initialising the AVAudioSession includes subscribing for a single interruption notification before starting startAudioPlayer in AudioBufferPlayer

- (id)init
{
    if (self = [super init])
    {
        NSLog(@"PlayViewController starts MotionListener and AudioSession");

        [self startAudioSession];
    }
    return self;
}


- (void)startAudioSession
    {
    // Synth and the AudioBufferPlayer must use the same sample rate.

    _synthLock = [[NSLock alloc] init];

    float sampleRate = 44100.0f;

    // Initialise synth to fill the audio buffer with audio samples.

    _synth = [[Synth alloc] initWithSampleRate:sampleRate];

    // Initialise the audio buffer.

    _player = [[AudioBufferPlayer alloc] initWithSampleRate:sampleRate 
                                                   channels:1
                                             bitsPerChannel:16
                                           packetsPerBuffer:1024];
    _player.gain = 0.9f;

    __block __weak PlayViewController *weakSelf = self;


    _player.block = ^(AudioQueueBufferRef buffer, AudioStreamBasicDescription audioFormat)
{
    PlayViewController *blockSelf   = weakSelf;

    if (blockSelf != nil)
    {
        // Lock access to the synth. This callback runs on an internal Audio Queue thread and we don't
        // want another thread to change the Synth's state while we're still filling up the audio buffer.

    [blockSelf -> _synthLock lock];

        // Calculate how many packets fit into this buffer. Remember that a packet equals one frame
        // because we are dealing with uncompressed audio; a frame is a set of left+right samples
        // for stereo sound, or a single sample for mono sound. Each sample consists of one or more
        // bytes. So for 16-bit mono sound, each packet is 2 bytes. For stereo it would be 4 bytes.

    int packetsPerBuffer = buffer -> mAudioDataBytesCapacity / audioFormat.mBytesPerPacket;

        // Let the Synth write into the buffer. The Synth just knows how to fill up buffers
        // in a particular format and does not care where they come from.

    int packetsWritten              = [blockSelf -> _synth fillBuffer:buffer->mAudioData frames:packetsPerBuffer];

        // We have to tell the buffer how many bytes we wrote into it.

    buffer -> mAudioDataByteSize    = packetsWritten * audioFormat.mBytesPerPacket;

    [blockSelf -> _synthLock unlock];
    }
};

// Set up notifications

    [self subscribeForBlockNotification];

    [_player startAudioPlayer];
}


- (void)subscribeForBlockNotification
{
    NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];

    id __block token = [center addObserverForName:@"AVAudioSessionInterruptionNotification"

                                       object:nil
                                        queue:[NSOperationQueue mainQueue]
                                   usingBlock:^(NSNotification *note) {
                                       NSLog(@"Received the notification!");
                                       [_player stopAudioPlayer];
                                       [center removeObserver:token];
                                   }];
}

PlayViewController.h

These are relevant interface settings

@interface PlayViewController : UIViewController <EscButtonDelegate>

{    
    ... 

    // Initialisation of audio player and synth

    AudioBufferPlayer* player;
    Synth*             synth;
    NSLock*            synthLock;
}

    ...

    - (AudioBufferPlayer*)player;
    - (Synth*)synth;

@end

AudioBufferPlayer.m

- (void)stopAudioPlayer
{
    [self stopPlayQueue];
    [self tearDownPlayQueue];
    [self tearDownAudioSession];
}


- (void)stopPlayQueue
{
    if (_audioPlaybackQueue != NULL)
    {
        AudioQueuePause(_audioPlaybackQueue);
        AudioQueueReset(_audioPlaybackQueue);
        _playing = NO;
    }
}


- (void)tearDownPlayQueue
{
    AudioQueueDispose(_audioPlaybackQueue, NO);
    _audioPlaybackQueue = NULL;
}


- (BOOL)tearDownAudioSession
{
    NSError *deactivationError = nil;
    BOOL success = [[AVAudioSession sharedInstance] setActive:NO
                                                  withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
                                                        error:nil];
    if (!success)
    {
        NSLog(@"%s AVAudioSession Error: %@", __FUNCTION__, deactivationError);
    }
    return success;
}


来源:https://stackoverflow.com/questions/46174735/why-does-this-audio-session-fail-to-recognise-an-interruption

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!