Change AVSpeechUtterance rate in real time - ios

I am currently developing an iOS app that converts text to speech using AVSynthesizer.
What I want to do is that while the synthesizer is speaking, utterance rate can be changed and with a slider and the speed of the speaking changes.
I am doing this in the IBAction of the slider:
self.utterance = sender.value
but the synthesizer doesn't change the speed. I've been looking for information but I haven't found something yet.
What cand I do? Thanks in advance.

Ok, so after playing a bit with this cool feature I wasn't aware of, I found out a way to change the rate of utterance. The main problem is that the utterance is currently enqueued by synthetizer, the rate can't be changed. Corresponds to the documentation:
/* Setting these values after a speech utterance has been enqueued will have no effect. */
open var rate: Float // Values are pinned between AVSpeechUtteranceMinimumSpeechRate and AVSpeechUtteranceMaximumSpeechRate.
open var pitchMultiplier: Float // [0.5 - 2] Default = 1
open var volume: Float // [0-1] Default = 1
So the workaround would be to stop the synthesizer and feed him new utterance with trimmed string.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var synthesizer: AVSpeechSynthesizer!
var string: String!
var currentRange: NSRange = NSRange(location: 0, length: 0)
#IBAction func sl(_ sender: UISlider) {
synthesizer.stopSpeaking(at: .immediate)
if currentRange.length > 0 {
let startIndex = string.index(string.startIndex, offsetBy: NSMaxRange(currentRange))
let newString = string.substring(from: startIndex)
string = newString
synthesizer.speak(buildUtterance(for: sender.value, with: string))
}
}
func buildUtterance(for rate: Float, with str: String) -> AVSpeechUtterance {
let utterance = AVSpeechUtterance(string: str)
utterance.rate = rate
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
return utterance
}
override func viewDidLoad() {
super.viewDidLoad()
string = "I am currently developing an iOS app that converts text to speech using AVSynthesizer.What I want to do is that while the synthesizer is speaking, utterance rate can be changed and with a slider and the speed of the speaking changes. I am doing this in the IBAction of the slider: self.utterance = sender.value but the synthesizer doesn't change the speed. Ive been looking for information but I haven't found something yet. What can I do? Thanks in advance."
synthesizer = AVSpeechSynthesizer()
synthesizer.delegate = self
synthesizer.speak(buildUtterance(for: AVSpeechUtteranceDefaultSpeechRate, with: string))
}
}
extension ViewController: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
debugPrint(characterRange.toRange())
currentRange = characterRange
}
}
implement the delegate method willSpeakRangeOfSpeechString of AVSpeechSynthesizerDelegate and define the delegate of synthesizer as self: synthesizer.delegate = self
In that delegate method, save the characterRange which would be spoken next.
Bind IBAction func sl(_ sender: UISlider) to your slider's event touchUpInside.
in that IBAction, stop speaking, and get the substring of your current text being spoken from index it would've continue.
Build new utterance and start speaking it
Profit.

swift 3
import UIKit
import AVFoundation
class ViewController: UIViewController{
#IBOutlet weak var sliderVolume: UISlider! //for volume
#IBOutlet weak var sliderRate: UISlider! //for rate
#IBOutlet weak var sliderPitch: UISlider! //for pitch
#IBOutlet weak var txtForSpeak: UITextField!
let speechSynth = AVSpeechSynthesizer()
#IBAction func btnStartToSpeak(_ sender: UIButton) {
let speechUtt = AVSpeechUtterance(string: self.txtForSpeak.text!)
speechUtt.rate = self.sliderRate.value
speechUtt.volume = self.sliderVolume.value
speechUtt.pitchMultiplier = self.sliderPitch.value
self.speechSynth.speak(speechUtt)
}
}

Related

IPA string is not properly voiced on iOS

I'm trying to use AVSpeechSynthesizer with AVSpeechSynthesisIPANotationAttribute but it doesn't seem to work properly.
Here is the IPA notation for the pronunciation of the word "participation" taken from Wiktionary: pɑɹˌtɪsɪˈpeɪʃən. But AVSpeechSynthesizer does not read it correctly.
When I add a custom pronunciation using Settings -> Accessibility -> Spoken Content -> Pronunciations on my iPhone, it gives me this notation: pəɻ.ˈtɪ.sɪ.ˈpe͡ɪ.ʃən, which the iPhone reads correctly.
Why does "pəɻ.ˈtɪ.sɪ.ˈpe͡ɪ.ʃən" work but a string like "pɑɹˌtɪsɪˈpeɪʃən" that is taken from a dictionary does not?
Here is a sample code you can try:
import UIKit
import AVKit
class ViewController: UIViewController {
let synthesizer = AVSpeechSynthesizer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func buttonPressed(_ sender: Any) {
let pronunciationKey = NSAttributedString.Key(rawValue: AVSpeechSynthesisIPANotationAttribute)
let attrStr = NSMutableAttributedString(string: "foo",
attributes: [pronunciationKey: "pɑɹˌtɪsɪˈpeɪʃən"])
let utterance = AVSpeechUtterance(attributedString: attrStr)
synthesizer.speak(utterance)
}
}

How to know when an AVSpeechUtterance has finished, so as to continue app activity?

When an AVSpeechUtterance is speaking, I want to wait until it's finished before doing something else.
There is a property of AVSpeechSynthesizer that's seemingly indicative of when speech is occuring:
isSpeaking
As stupid and simple as this question might sound, I'm wondering how do I use/check this property to wait until speech has concluded before going on?
ALTERNATIVELY:
There's a delegate, that I'm also clueless as to how to use, which has an ability to do something when an utterance has finished:
AVSpeechSynthesizerDelegate
There's an answer, here, that says to use this. But that doesn't help me because I don't know how to use a Delegate.
UPDATE:
This is how I setup my speaking class:
import AVFoundation
class CanSpeak: NSObject, AVSpeechSynthesizerDelegate {
let voices = AVSpeechSynthesisVoice.speechVoices()
let voiceSynth = AVSpeechSynthesizer()
var voiceToUse: AVSpeechSynthesisVoice?
override init(){
voiceToUse = AVSpeechSynthesisVoice.speechVoices().filter({ $0.name == "Karen" }).first
}
func sayThis(_ phrase: String){
let utterance = AVSpeechUtterance(string: phrase)
utterance.voice = voiceToUse
utterance.rate = 0.5
voiceSynth.speak(utterance)
}
}
UPDATE 2: Wrongheaded workaround...
using the above mentioned isSpeaking property, in the gameScene:
voice.sayThis(targetsToSay)
let initialPause = SKAction.wait(forDuration: 1.0)
let holdWhileSpeaking = SKAction.run {
while self.voice.voiceSynth.isSpeaking {print("STILL SPEAKING!")}
}
let pauseAfterSpeaking = SKAction.wait(forDuration: 0.5)
let doneSpeaking = SKAction.run {print("TIME TO GET ON WITH IT!!!")}
run(SKAction.sequence(
[ initialPause,
holdWhileSpeaking,
pauseAfterSpeaking,
doneSpeaking
]))
Delegate pattern is one of the most common used design pattern in object-oriented programming, it's not as hard as it seems. For your case, you can simply let your class (a game scene) to be a delegate of the CanSpeak class.
protocol CanSpeakDelegate {
func speechDidFinish()
}
Next set AVSpeechSynthesizerDelegate to your CanSpeak class, declare CanSpeakDelegate and then use AVSpeechSynthesizerDelegate delegate function.
class CanSpeak: NSObject, AVSpeechSynthesizerDelegate {
let voices = AVSpeechSynthesisVoice.speechVoices()
let voiceSynth = AVSpeechSynthesizer()
var voiceToUse: AVSpeechSynthesisVoice?
var delegate: CanSpeakDelegate!
override init(){
voiceToUse = AVSpeechSynthesisVoice.speechVoices().filter({ $0.name == "Karen" }).first
self.voiceSynth.delegate = self
}
func sayThis(_ phrase: String){
let utterance = AVSpeechUtterance(string: phrase)
utterance.voice = voiceToUse
utterance.rate = 0.5
voiceSynth.speak(utterance)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.delegate.speechDidFinish()
}
}
Lastly in your game scene class, simply conform to CanSpeakDelegate and set it as the delegate of your CanSpeak class.
class GameScene: NSObject, CanSpeakDelegate {
let canSpeak = CanSpeak()
override init() {
self.canSpeak.delegate = self
}
// This function will be called every time a speech finishes
func speechDidFinish() {
// Do something
}
}
Sets up delegate to your AVSpeechSynthesizer instance.
voiceSynth.delegate = self
Then, implements didFinish method as below:
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,
didFinish utterance: AVSpeechUtterance) {
// Implements here.
}
Try AVSpeechSynthesizerDelegate with method:
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance;

How to make slider value change object or function?

I am new to the site and Swift so any feedback on question technique is gratefully received.
I am trying to make the value of a slider change both the audio that it being played and the text label (or image).
I have created my outlets:
#IBOutlet weak var audioSlider: UISlider!
#IBOutlet weak var audioValue: UILabel!
And my action:
#IBAction func audioSliderValueChanged(sender: UISlider) {
var currentValue = (sender.value)
audioValue.text = (StringInterpolationConvertible: audioValue))
All connected to one slider. I am unsure of how to approach this issue - would an if else work?
EDIT: Han Yong Code:
There was already a 'do' for other sounds within the app using AVFoundation. I imported Foundation (AVFoundation was already imported)
I have added:
#IBOutlet weak var audioSlider: UISlider!
#IBOutlet weak var audioValue: UILabel!
var audioSliderPlayer:AVAudioPlayer = AVAudioPlayer()
var currentTime: Double {
get {
return CMTimeGetSeconds(audioSliderPlayer.currentTime)
}
set {
let newTime = CMTimeMakeWithSeconds(newValue, 1)
audioSliderPlayer.seekToTime(newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
}
}
And added the following after the do function (within viewDidLoad):
{timeObserverToken = audioSliderPlayer.addPeriodicTimeObserverForInterval(interval, queue: dispatch_get_main_queue()) {
[weak self] time in
self?.timeSlider.value = Float(CMTimeGetSeconds(time))
}}
}
I have updated the action:
#IBAction func timeSliderDidChange(sender: UISlider) {
currentTime = Double(sender.value)
}
However, I am still having issues with errors. Also, how would I be able to specify which audio it plays dependent on value?
I guess you want the audioValue.text show the same value of slider, right?
Try this:
#IBAction func audioSliderValueChanged(sender: UISlider) {
var currentValue = (sender.value)
audioValue.text = "\(currentValue)"
}
If you use AVFoundation and AVPlayer, sample source that apple provide will be helpful.
In this source, assign var that has get and set method. In set method, update your label and player state. (Sample source snippet don't update label, but you can make it.)
var currentTime: Double {
get {
return CMTimeGetSeconds(player.currentTime())
}
set {
let newTime = CMTimeMakeWithSeconds(newValue, 1)
player.seekToTime(newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
}
}
If slider changed, it trigger variable's set method. Label and player status will be updated.
#IBAction func timeSliderDidChange(sender: UISlider) {
currentTime = Double(sender.value)
}
Add addPeriodicTimeObserverForInterval method to AVPlayer. It will be called every interval you set and trigger your variable's setter.
timeObserverToken = player.addPeriodicTimeObserverForInterval(interval, queue: dispatch_get_main_queue()) {
[weak self] time in
self?.timeSlider.value = Float(CMTimeGetSeconds(time))
}

Is there a way for the text in a label to automatically update the display

I'm trying to make an app that lets users input 4 numbers and display what cellular network is that number. Is there a way to automatically display the numbers in the label after the user inputs the numbers without clicking a button? I actually have a code that lets the user input the number but the user must click the enter button for it to display the result on the label. Is there a way for it to automatically display the result? By the way, I'm actually new to swift so I apologize. Here is the code:
import UIKit
import Firebase
class ViewController: UIViewController, FBSDKLoginButtonDelegate {
#IBOutlet weak var numField: UITextField!
#IBOutlet weak var answerField: UILabel!
#IBAction func enterButton(sender: AnyObject) {
matchNumber()
}
func matchNumber(){
let number: String = numField.text!
let numRef = Firebase(url: "https://npi2.firebaseio.com/num_details")
numRef.queryOrderedByKey().observeEventType(.ChildAdded, withBlock: { snapshot in
let network = snapshot.value as! String!
if snapshot.key == self.numField.text! {
self.answerField.text = network
}
else {
self.answerField.text = "Missing"
}
})
}
Add a target to your numField, something like this...
override func viewDidLoad() {
super.viewDidLoad()
self.numField.addTarget(self, action: "editingChanged:", forControlEvents: .EditingChanged)
}
This will trigger the editingChanged method every time a character changes in the numField.
func editingChanged(textField:UITextField) {
if let text = textField.text {
if text.characters.count == 4 {
matchNumber()
}
}
}
Finally we check if there is a string in the input and if it is 4 characters long before running the matchNumber method.
You could use the didSet method.
var numberstring = String() {
didSet {
yourlabel.text = numberstring
}
}
If you are using textfield to take input then you can use textfieldDidEndEditing delegate method.

Making a non-instantaneous change with an #IBAction button in Swift

I am trying to change the value of the speech rate property of my AVSpeechUtterance upon clicking a button in my app. I then want this value to carry over when I press the "speak" button.
If that explanation wasn't clear enough imagine this:
There are three buttons: one, two and three.
When the user presses one, the value of a the rate of an utterance changes (The same goes for the two and three).
Then after pushing one of the first three buttons, the user pushes another button that activates an utterance. This utterance will carry over the rate value and apply it to the speech.
Here is the code I have tried but doesn't work:
import UIKit
import AVFoundation
class SecondViewController: UIViewController {
#IBOutlet var minutesLabel: UITextField!
#IBOutlet var secondsField: UITextField!
func speak(sender: AnyObject) {
let speechUtterance = AVSpeechUtterance(string:exampleSpeech)
speechSynthesizer.speakUtterance(speechUtterance)
}
#IBOutlet var result: UILabel!
#IBAction func verySlow(sender: UIButton) {
let verySlowButtonPressed = true
let talkingSpeed = 90
let minutesValue : Double = (minutesLabel.text as NSString!).doubleValue
let secondsValue = (secondsField.text as NSString!).doubleValue
let secondsToMinutes = secondsValue / 60
let compiledTime = Double(minutesValue) + Double(secondsToMinutes)
let resultingWords = Double(compiledTime) * Double(talkingSpeed)
let resultCeiling = ceil(resultingWords)
result.text = "\(String(format: "%.0f", resultCeiling)) words"
if verySlowButtonPressed {
speechUtterance.rate = 0.25
speechUtterance.pitchMultiplier = 0.25
speechUtterance.volume = 0.75
}
}
#IBAction func speakButton(sender: AnyObject) {
speak(exampleSpeech)
}
In your speak function you are creating a new AVSpeechUtterance instance every time it's called. When your verySlow function is called, it's setting the rate, pitchMultiplier, and volume on what I'm presuming is a class property called speechUtterance. Since you're creating a new AVSpeechUtterance in speak each time it's called, those properties that you are setting in verySlow are not going to be set on the same instance.
One way to solve this would be to make rate, pitchMultiplier, and volume properties in your class, set those in your verySlow function, and then set them on speechUtterance after you create it in your speak function.
Something like:
var rate: Float?
var pitchMultiplier: Float?
var volume: Float?
func speak(sender: AnyObject) {
let speechUtterance = AVSpeechUtterance(string:exampleSpeech)
if let rate = self.rate {
speechUtterance.rate = rate
}
if let pitchMultiplier = self.pitchMultiplier {
speechUtterance.pitchMultiplier = pitchMultiplier
}
if let volume = self.volume {
speechUtterance.volume = volume
}
speechSynthesizer.speakUtterance(speechUtterance)
}
#IBAction func verySlow(sender: UIButton) {
// ... whatever other code you need here ...
self.rate = 0.25
self.pitchMultiplier = 0.25
self.volume = 0.75
}
Not sure but these are the value for speech rate, try using them. I hope it helps.
iOS 9
Very Slow -- 0.42
Slower -- 0.5
My Normal -- 0.53
Faster -- 0.56
Value are different for previous versions of iOS, please keep that in mind while implementing the solution.

Resources