Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to consistently stop AVAudioSession after AVSpeechUtterance

What I want to do is allow my app to speak an utterance using AVSpeechSynthesizer while background audio apps are playing audio. While my app is speaking, I'd like the background apps' audio to "dim" and then return to their original volume after my app has finished speaking.

In my AudioFeedback class, I initialize I setup the AVAudioSessions like so:

    self.session = [AVAudioSession sharedInstance];
    NSError *error;
    [self.session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionDuckOthers error:&error];

Whenever I want to speak a new utterance, I do the following. I followed the suggestion from An issue with AVSpeechSynthesizer, Any workarounds? to create a new AVSpeechSynthesizer every time to "make sure" that cancelations are always received (it just seems to work, I'm not sure why).

- (AVSpeechUtterance *) utteranceWithString: (NSString *) string
{
    AVSpeechUtterance *utterance = [AVSpeechUtterance  speechUtteranceWithString:string];
    utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"en-ES"];
    [utterance setRate:(AVSpeechUtteranceDefaultSpeechRate+AVSpeechUtteranceMinimumSpeechRate)/2.0];
    return utterance;
}

- (void) sayString: (NSString *) string cancelPrevious: (BOOL) cancelPrevious
{
    [self.session setActive:enabled error:nil];
    if (cancelPrevious) {
        AVSpeechSynthesizer *oldSynthesizer = self.voice;
        self.voice = nil;
        [oldSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
        self.voice = [[AVSpeechSynthesizer alloc] init];
        self.voice.delegate = self;
    }

    // Keep track of the final utterance, we'll use this to determine whether or not we should stop the audio session
    self.finalUtterance = [self utteranceWithString:string];
    [self.voice speakUtterance:self.finalUtterance];
}

In my AVSpeechSynthesizer delegate method, I check to see if I should stop the audio session to return the background audio to normal volume if the current AVSpeechSynthesizer and the current AVSpeechUtterance match the last known synthesizer and utterance.

-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance
{
    NSError *error;
    // Only stop the audio session if this is the last created synthesizer
    if (synthesizer == self.voice  && self.finalUtterance == utterance) {
        if ([self.session setActive:enabled error:&error]]) {
            NSLog(@"Stopped the audio session: Speech synthesizer still speaking %d", synthesizer.speaking);
        } else {
            NSLog(@"ERROR failed to stop the audio session: %@. Speech synthesizer still speaking %d", error, synthesizer.speaking);
        }
    }
}

The problem I'm having is that sometimes, the audio session will stop without issues, and other times, the audio session will fail to stop with the following error:

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

I am not sure how to guarantee that I can stop the AVAudioSession. I've tried to keep calling [[AVAudioSession sharedInstance] setActive:NO error:&error] for as long as I am unable to stop the audio session, but that just doesn't seem to work. Any help would be greatly appreciated. Thank you!

like image 325
kross Avatar asked Feb 19 '14 21:02

kross


1 Answers

I've found if I try again in a couple of seconds it will work. I'm using this code.

-(void)configureAVAudioSession:(bool)active {

    AVAudioSession *audioSession = [AVAudioSession sharedInstance];

    if (deactivationAttempt < 3) {  // This doesn't always work first time, but don't want to keep trying forever if it decides not to work ever.

        NSError *activationError = nil;
        success = [audioSession setActive:active error:&activationError];

        if (!success) {
            NSLog(@"(de)activation ERROR");
            ++deactivationAttempt;
            [self performSelector:@selector(configureAVAudioSession:) withObject:false afterDelay:2.0];
        }
        else {  // Success!
            deactivationAttempt = 0;
        }
    }
    else {  // Failed, but reset the counter in case we have more luck next time
        deactivationAttempt = 0;
    }
}

From the docs:

Deactivating your session will fail if any associated audio objects (such as queues, converters, players or recorders) are currently running.

I guess it takes a while for the utterance to be cleared from the queue.

like image 131
That Guy Avatar answered Sep 27 '22 20:09

That Guy