An issue with AVSpeechSynthesizer, Any workarounds?

I am using AVSpeechSynthesizer to play text. I have an array of utterances to play.

    NSMutableArray *utterances = [[NSMutableArray alloc] init];
    for (NSString *text in textArray) {
        AVSpeechUtterance *welcome = [[AVSpeechUtterance alloc] initWithString:text];
        welcome.rate = 0.25;
        welcome.voice = voice;
        welcome.pitchMultiplier = 1.2;
        welcome.postUtteranceDelay = 0.25;
        [utterances addObject:welcome];
    }
    lastUtterance = [utterances lastObject];
    for (AVSpeechUtterance *utterance in utterances) {
        [speech speakUtterance:utterance];
    }

I have a cancel button to stop speaking. When I click the cancel button when the first utterance is spoken, the speech stops and it clears all the utterances in the queue. If I press the cancel button after the first utterance is spoken (i.e. second utterance), then stopping the speech does not flush the utterances queue. The code that I am using for this is:

  • How to run a fresh install of the application every time unit tests are run?
  • How to loop through Array<Dictionary<String,String>> using Swift
  • Xcode Parse.com save to csv/any file?
  • App “Alarmy” is able to play iTunes song from background state… How?
  • localStorage cleared on app restart with Cordova 1.7 and iOS 5.1.1
  • ld: duplicate symbol _objc_retainedObject on iOS 4.3 , but not on iOS 5.0
  •   [speech stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
    

    Can someone confirm if this is a bug in the API or am I using the API incorrectly? If it is a bug, is there any workaround to resolve this issue?

    5 Solutions Collect From Internet About “An issue with AVSpeechSynthesizer, Any workarounds?”

    quite likely to be a bug, in that the delegate method synthesizer didCancelSpeechUtterance isn’t called after the first utterance;

    A workaround would be to chain the utterances rather than have them in an array and queue them up at once.

    Use the delegate method synthesizer didFinishSpeechUtterance to increment an array pointer and speak the the next text from that array. Then when trying to stop the speech, set a BOOL that is checked in this delegate method before attempting to speak the next text.

    For example:

    1) implement the protocol in the view controller that is doing the speech synthesis

    #import <UIKit/UIKit.h>
    @import AVFoundation;
    @interface ViewController : UIViewController <AVSpeechSynthesizerDelegate>
    
    @end
    

    2) instantiate the AVSpeechSynthesizer and set its delegate to self

    speechSynthesizer   = [AVSpeechSynthesizer new];
    speechSynthesizer.delegate = self;
    

    3) use an utterance counter, set to zero at start of speaking

    4) use an array of texts to speak

    textArray           = @[@"Mary had a little lamb, its fleece",
                            @"was white as snow",
                            @"and everywhere that Mary went",
                            @"that sheep was sure to go"];
    

    5) add delegate method didFinishSpeechUtterance to speak the next utterance from the array
    of texts and increment the utterance counter

    - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance{
        if(utteranceCounter < utterances.count){
            AVSpeechUtterance *utterance = utterances[utteranceCounter];
            [synthesizer speakUtterance:utterance];
            utteranceCounter++;
        }
    }
    

    5) to stop speaking, set the utterance counter to the count of the texts array and attempt to get the synthesizer to stop

    utteranceCounter = utterances.count;
    
    BOOL speechStopped =  [speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
    if(!speechStopped){
        [speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryWord];
    }
    

    6) when speaking again, reset the utterance counter to zero

    I found a workaround :

    - (void)stopSpeech
    {
        if([_speechSynthesizer isSpeaking]) {
            [_speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
            AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:@""];
            [_speechSynthesizer speakUtterance:utterance];
            [_speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];    
        }
    }
    

    Call stopSpeakingAtBoundary:, enqueue an empty utterance and call stopSpeakingAtBoundary: again to stop and clean the queue.

    I did something similar to what SPA mentioned. Speaking one item at a time from a loop.
    Here is the idea..

    NSMutableArray *arr; //array of NSStrings, declared as property
    AVSpeechUtterance *currentUtterence;  //declared as property
    AVSpeechSynthesizer *synthesizer; //property
    
    - (void) viewDidLoad:(BOOL)anim
    {
        [super viewDidLoad:anim];
        synthesizer = [[AVSpeechSynthesizer alloc]init];
    
        //EDIT -- Added the line below
        synthesizer.delegate = self;
    
        arr = [self populateArrayWithString]; //generates strings to speak
    }
    
    //assuming one thread will call this
    - (void) speakNext
    {
       if (arr.count > 0)
       {
            NSString *str = [arr objectAtIndex:0];
            [arr removeObjectAtIndex:0];
            currentUtterence = [[AVSpeechUtterance alloc] initWithString:str];
    
            //EDIT -- Commentted out the line below
            //currentUtterence.delegate = self;
            [synthesizer speakUtterance:utteranc];
        }
    }
    
    - (void)speechSynthesizer:(AVSpeechSynthesizer *)avsSynthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance
    {
        if ([synthesizer isEqual:avsSynthesizer] && [utterance isEqual:currentUtterence])
            [self speakNext];
    }
    
    - (IBOutlet) userTappedCancelledButton:(id)sender
    {
        //EDIT <- replaced the object the method gets called on.
        [synthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
        [arr removeAllObjects];
    }
    

    didCancelSpeechUtterance does not work with the same AVSpeechSynthesizer object even though the utterances are chained in the didFinishSpeechUtterance method.

    -(void)speakInternal
    {
        speech = [[AVSpeechSynthesizer alloc] init];
        speech.delegate = self;
        [speech speakUtterance:[utterances objectAtIndex:position++]];
    }
    

    In speakInternal, I am creating AVSpeechSynthesizer object multiple times to ensure that didCancelSpeechUtterance works. Kind of a workaround.

    All answers here failed, and what I came up with is stopping the synthesizer and then re-instantiate it:

    - (void)stopSpeech
    {
        if([_speechSynthesizer isSpeaking]) {
            [_speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
            _speechSynthesizer = [AVSpeechSynthesizer new];
            _speechSynthesizer.delegate = self;
        }
    }