The AV Foundation Framework received a few extras when iOS 7 launched. One of those is the AVSpeechSynthesizer class which lets you have the device read out text in the voice of Siri.
The class is quite small in that it only has 4 methods and 3 properties which are:
Synthesizing Speech
speakUtterance:
Controlling Speech Synthesis
continueSpeaking
pauseSpeakingAtBoundary:
paused property
speaking property
stopSpeakingAtBoundary:
Managing the Delegate
delegate property
In todays tutorial we’ll take a quick look at how you can work with this class and what each of the methods can do. We’ll create a simple app that has a text box where you can type some text and hit play, pause and also send new text. The AVSpeechSynthesizer class maintains a queue of utterances to be spoken. If you send another request while one is still playing then the new request(s) will just sit in a queue and start playing once another has finished.
AVSpeechSynthesizer Tutorial
As always, we’ll begin with a Single View Application. Lets now go to the ViewController.h file and add the following import:
#import
Lets go over to the storyboard now and add some basic interface controls as seen above. All we need is a UITextView and a UIButton. Make sure that the play button is above the keyboard and in view as it will be used to dismiss the keyboard as well.
Connect these up to the header and implementation. The UIButton is an IBAction and an IBOutlet, so we need to connect to both. The reason for this is that we need it in the implementation so we can detect button presses. We also need it in the header so we can change the button text from Play to Pause and vice versa.
The UITextView works differently to a text field. For a text field you would typically connect that as an IBAction to your implementation. The UITextView is required to be connected to just the header file. What we do is type the text and when we hit play, it will read whatever text is currently in the UITextView. To slide the keyboard away, we need to dismiss it when the play button is pressed.
Connect it up to your header as well as the button to the implementation and header and you’ll end up with the following code: (connect up by CTRL+dragging to the appropriate file and this will be created for you.)
ViewController.h
@property (weak, nonatomic) IBOutlet UIButton *playPauseButton;
@property (weak, nonatomic) IBOutlet UITextView *textToSpeak;
ViewController.m
- (IBAction)playPauseButtonPressed:(UIButton *)sender {
}
Running the app will now let you type in some text although you wont be able to dismiss the keyboard. This is correct of a UITextView and dismissing the keyboard needs to be handled in code. Luckily it is just a single line of code added to the playButtonPressed method. Add this line to that method:
[self.textToSpeak resignFirstResponder];
When you tap the play button, the keyboard will now slide out of the way. It’s a simple solution although there are many ways to solve it such as using a gesture recogniser, a Done button like you have in the messages app or by some other way.
We now need to get the app to speak. Lets do this by adding an AVSpeechSynthesizer property to our project. We can add this to the header file as follows:
@property (strong, nonatomic) AVSpeechSynthesizer *synthesizer;
We then need to alloc/init the object. I’ll do this in the viewDidLoad method:
self.synthesizer = [[AVSpeechSynthesizer alloc] init];
Moving in to the playButtonPressed method, we now need to add some fairly boilerplate code:
AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:@"Hello"];
[self.synthesizer speakUtterance:utterance];
Although this wont be our final code as it would just speak “Hello”, I will use this to give you the basics of how it works and we’ll then branch out to getting the text of the UITextView as well as tweaking some of the properties of the AVSpeechUtterance class.
Line 1 we create an AVSpeechUtterance and alloc/init that with an NSString.
On line 2, we call the speakUtterance method with our utterance on self.synthesizer.
An alternative way to do this in a single line is to use a class method in AVSpeechUtterance called speechUtteranceWithString:.
[self.synthesizer speakUtterance:[AVSpeechUtterance speechUtteranceWithString:@"Hello"]];
This method lets us just pass the message direct to speakUtterance without having to create a variable. If your speech can be disposed of after speaking it then I’d recommend doing it this way. However, if you need to customise the voice, pitch and other settings in AVSpeechUtterance or if for some reason you want to keep the utterance in a variable for use elsewhere then use the instance method.
The AVSpeechUtterance has several properties that can be set to change how the text is spoken. These properties are:
pitchMultiplier property
postUtteranceDelay property
preUtteranceDelay property
rate property
speechString property
voice property
volume property
All the details of what each does can be found in the AVSpeechUtterance class reference.
Interesting ones to look at are voice, rate, volume. Voice can be used to set the language or voice. If you don’t specify then it uses the system default (in the UK that would be the male UK voice).
Now that we have our basic part of the app working, we now need to feed the text of the UITextView in to our utterance.
[self.synthesizer speakUtterance:[AVSpeechUtterance speechUtteranceWithString:self.textToSpeak.text]];
This is done quite easily by just accessing the text property of the textToSpeak UITextView object as seen in the line of code above.
Typing some text in to the box and hitting play will now start speaking whatever text you have available in there. You can change some of the utterance properties as follows:
- (IBAction)playPauseButtonPressed:(UIButton *)sender {
[self.textToSpeak resignFirstResponder];
AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:self.textToSpeak.text];
utterance.rate = AVSpeechUtteranceMinimumSpeechRate;
utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"en-au"];
[self.synthesizer speakUtterance:utterance];
}
Line 4 slows down the speech. Line 5 changes the voice to Australian.
Pausing and Resuming Speech
To pause/resume/cancel speech we need to implement a couple of methods in the AVSpeechSynthesizer class. These are:
pauseSpeakingAtBoundary:
continueSpeaking:
What we will first do is modify the playPauseButtonPressed to switch the text on the button to Pause when Play is pressed and vice-versa.
To track this we’ll use a variable added just below the @implementation line:
BOOL speechPaused = NO;
if (speechPaused == NO) {
[self.playPauseButton setTitle:@"Pause" forState:UIControlStateNormal]
speechPaused = YES;
} else {
[self.playPauseButton setTitle:@"Play" forState:UIControlStateNormal];
speechPaused = NO;
}
Here we are setting the title of the button to either Pause or Play depending on the current state. You just need to ensure your button is wide enough to accommodate the Pause string which is longer than the Play string.
Now that the button title is changing as needed, we now need to pause and continue the speech. We do this by adding two lines of code in our code above:
if (speechPaused == NO) {
[self.playPauseButton setTitle:@"Pause" forState:UIControlStateNormal];
[self.synthesizer continueSpeaking];
speechPaused = YES;
} else {
[self.playPauseButton setTitle:@"Play" forState:UIControlStateNormal];
speechPaused = NO;
[self.synthesizer pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate];
}
We have added line 3 and line 8 which control the pausing and continuing of speech. We we did for the pause is passed it the AVSpeechBoundaryImmediate message which stops the speech where it is.
Although this code works, it has a bug which means that when you press pause and play it re-queues the speech. The continueSpeech method continues it where it left off, but the code carries on through the remainder of the method, creates another utterance and queues it to be played. Not ideal.
We can fix that by nesting the creation of the utterance and passing that to the synthesizer in a control statement. The synthesizer has a property called paused. What we can do is check to see if the speech is paused and if so, just skip over adding another utterance to the queue.
if (self.synthesizer.speaking == NO) {
AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:self.textToSpeak.text];
//utterance.rate = AVSpeechUtteranceMinimumSpeechRate;
utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"en-au"];
[self.synthesizer speakUtterance:utterance];
}
If the synthesizer is not speaking then we create a new utterance. Note that if the synthesizer is paused, it is still classed as speaking.
Finally, to iron out another bug we need to use the AVSpeechSynthesizerDelegate protocol. The reason we are using this is to detect when the speech has finished so that we can change the Pause button back to a Play button.
Add the following lines of code:
ViewController.h
@interface ViewController : UIViewController
ViewController.m (viewDidLoad method)
self.synthesizer.delegate = self;
ViewController.m
-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {
[self.playPauseButton setTitle:@"Play" forState:UIControlStateNormal];
speechPaused = NO;
}
The first section of code lets ViewController.h know that we conform to the AVSpeechSynthesizerDelegate protocol.
The next section lets us know the delegate is self. If you don’t assign self to the delegate then the delegate method(s) will not get called.
The final section implements the speechSynthesizer:didFinishSpeechUtterance: method. This method is called when the synthesizer finishes speaking its utterance. All we do here is set the button title back to Play.
Testing
Testing is relatively simple. Load up the app, enter some text and hit Play. Hit Pause to pause it and Play to resume it. At the end of playing the utterance, the button will switch back to Play and you can enter more or other text to read.
You can download the full project from here.
Full Code:
ViewController.h
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *playPauseButton;
@property (weak, nonatomic) IBOutlet UITextView *textToSpeak;
@property (strong, nonatomic) AVSpeechSynthesizer *synthesizer;
@end
ViewController.m
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
BOOL speechPaused = 0;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.synthesizer = [[AVSpeechSynthesizer alloc] init];
speechPaused = NO;
self.synthesizer.delegate = self;
}
- (IBAction)playPauseButtonPressed:(UIButton *)sender {
[self.textToSpeak resignFirstResponder];
if (speechPaused == NO) {
[self.playPauseButton setTitle:@"Pause" forState:UIControlStateNormal];
[self.synthesizer continueSpeaking];
speechPaused = YES;
} else {
[self.playPauseButton setTitle:@"Play" forState:UIControlStateNormal];
speechPaused = NO;
[self.synthesizer pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate];
}
if (self.synthesizer.speaking == NO) {
AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:self.textToSpeak.text];
//utterance.rate = AVSpeechUtteranceMinimumSpeechRate;
utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"en-au"];
[self.synthesizer speakUtterance:utterance];
}
}
-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {
[self.playPauseButton setTitle:@"Play" forState:UIControlStateNormal];
speechPaused = NO;
NSLog(@"Playback finished");
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
Norman Pettus says
Very nice explanation! I was able to use some of the examples in an app I am writing and they worked perfectly.
Ben Pre says
Can i use the AVspeechsynthezier with other languages?
Matthew says
Yes, I believe there are quite a few. Calling this class method on AVSpeechSynthesisVoice should list them for you:
+ (NSArray *)speechVoices
MatthewN says
Really sorry for the lack of response… I thought I had responded. Yes, I believe you can work with all languages that Siri supports.
Shabna Zaheer says
So if I wanted to click a button and have it pause shouldnt it just work when doing:
– (IBAction)pauseButtonPressed:(id)sender {
[self.synthesizer pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate];
}
I have the AVSpeechSynthesizer stuff all in the didSelectAnnotationView (I’m making a map so when one clicks on the annotation it plays but I want it so when you click a button it pauses).
It’s now working however :/
chandramouli N says
Simply Superb..
Filip says
Hello!
Very nice and educational way of doing this tutorial!
I wonder if you know how to rewrite the code so, instead of the “play/pause” button i can use my “play/pause” on my headset, would really help my work in school, because I`m doing an application for people with impaired vision.
Have a great day! and thanks in advance!
Best regards. Filip
sanjana says
Speech initialization error: 2147483665
Ewerton says
I have the same error! Did you know what we have to do?
Jacob Topping says
Sanjana,
I saw the same thing, and here’s what I found:
You will see this error when trying to run in the simulator (and it will not play the sound, or be audible). However, when you run the app on a physical device, it will work and play, and not give the error above.
Karen says
Hello, I would like to ask what if I would like the rate of utterance changes as the rate I type? Cheers,
Karen
k says
I am trying this with my xCode app and doesnt work in the simulator. Just starting my IOS Dev, so may be i am missing something obvious here.
Vijay Reddy says
Do you have a Swift version of this app?I get an error:Speech initialization error whenever i try to run the app.Is AVFoundation supported in IOS 8?
seb says
application does nothin on xcode 8 ios 10