AVSpeechSynthesizer delegate breaks speech - ios

Setting AVSpeechSynthesizer.delegate = self breaks french hyphenated words.
e.g. “parlez-vous” will be pronounced “parlez”,
"attendez-vous" will be pronounced "attendez",
"est-ce qu'il est la?" will be pronounced "est qu'il est la?"
"Puis-je vous voir demain?" will be pronounced "Puis vous voir demain?"
If you listen you can tell that the second word is silent ... like there seems to be the correct space left for it.
I have sent feedback FB11968008 to Apple.
No problem in the simulator - this is a device only problem occuring on these devices.
iPhone 11 iOS 16.2
iPhone 8 iOS 16.1.2
iPad Air2 iOS 15.7.2
Xcode 14.2
Any thoughts?
CODE DEMONSTRATION:
import SwiftUI
import Speech
struct ContentView: View {
var synth = SpeechSynthesizer()
#State var isDelegateSet = true
var sentences: [String] = [
"Parlez-vous ",
"attendez-vous ",
"est-ce qu'il est la?",
"Puis-je vous voir demain?"
]
var body: some View {
VStack {
List {
ForEach(sentences.indices, id: \.self) { index in
Button(sentences[index]) {
synth.toggleAVSpeechSynthesizerDelegate(isDelegateSet: isDelegateSet)
synth.speak(string: sentences[index])
}
}
Toggle("toggle AVSpeechSynthesizer delegate", isOn: $isDelegateSet)
Text(isDelegateSet ? "AVSpeechSynthesizer.delegate = self \nSpeech is INCORRECT " : "AVSpeechSynthesizer.delegate = nil \nSpeech is correct")
.padding()
}
}
}
}
class SpeechSynthesizer: NSObject, ObservableObject, AVSpeechSynthesizerDelegate {
var synthesizer = AVSpeechSynthesizer()
var utterance = AVSpeechUtterance(string: "")
override init() {
super.init()
synthesizer.delegate = self
}
func speak(string: String) {
synthesizer.stopSpeaking(at: .immediate)
utterance = AVSpeechUtterance(string: string)
utterance.voice = AVSpeechSynthesisVoice(language: "fr-FR")
synthesizer.speak(utterance)
}
func toggleAVSpeechSynthesizerDelegate(isDelegateSet: Bool) {
if isDelegateSet {
synthesizer.delegate = self
}
else {
synthesizer.delegate = nil
}
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
// if synthesizer.delegate = self , phrases are spoken incorrectly. !!
// if synthesizer.delegate = nil , phrases are spoken correctly as delegates are not called. !!
}
}

Related

Wait for ios speechSynthesizer.speak to finish utterance

I have a list of strings that I need to display on the screen and simultaneously convert to speech using speechSynthesizer.speak
The issue I am facing is that I am not able how to figure out how to wait for one utterance to finish and them move to the next one. What happens is that the text is displayed one after the other without syncing with the audio.
func speechAndText(text: String){
let speechUtterance = AVSpeechUtterance(string: text)
speechUtterance.rate = AVSpeechUtteranceMaximumSpeechRate/2.0
speechUtterance.pitchMultiplier = 1.2
speechUtterance.volume = 1
speechUtterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
DispatchQueue.main.async {
self.textView.text = text
}
self.speechSynthesizer.speak(speechUtterance)
}
func speakPara(_ phrases: [String]) {
if let phrase = phrases.first {
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: .default).async {
self.speechAndText(text: phrase)
group.leave()
}
group.wait()
let rest = Array(phrases.dropFirst())
if !rest.isEmpty {
self.speakPara(rest)
}
}
}
The speakParafunction takes the list of strings to be uttered and sends one string at a time to speechAndText to convert to speech and write on the display.
Any help would be appreciated. Thank you.
No need for GCD, you can use the delegate methods of AVSpeechSynthesizer.
Set self as delegate:
self.speechSynthesizer.delegate = self
and implement didFinish by speaking the next text.
extension YourClass : AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,
didFinish utterance: AVSpeechUtterance) {
currentIndex += 1
if currentIndex < phrases.lastIndex {
self.speechAndText(text: phrases[currentIndex])
}
}
}
currentIndex and phrases are two instance properties. phrases stores the array of strings that you want to speak, and currentIndex stores the index of the text that is currently being spoken. speakPara can be implemented like so:
func speakPara(_ phrases: [String]) {
// reset both helper properties
self.phrases = phrases
self.currentIndex = 0
// stop speaking whatever it was speaking and start speaking the first item (if any)
self.speechSynthesizer.stopSpeaking(at: .immediate)
phrases.map { self.speechAndText(text: $0) }
}

AudioKit iOS - receivedMIDINoteOn function

I'm trying to use the receivedMIDINoteOn function to flash a UILabel when the sequencer is playing a note. I have tried using the AKMIDIListener protocol with no success. Also I have made a sub class of AKMIDISampler and send midi to it from the sequencer. It plays the midi but the receivedMIDINoteOn is not called.
This is what I have in the init() of the conductor:
init() {
[ahSampler, beeSampler, gooSampler,flasher] >>> samplerMixer
AudioKit.output = samplerMixer
AudioKit.start()
let midi = AKMIDI()
midi.createVirtualPorts()
midi.openInput("Session 1")
midi.addListener(self)
}
The conductor follows AKMIDIListener protocol
this is the function: it is never called
func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel)
{
print("got it")
}
And this is the sub class of AKMIDISampler, it gets midi and plays the sine synth, but the receivedMIDINoteOn is never called.
class Flasher: AKMIDISampler
{
override func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel)
{
print("Flasher got it!")
}
}
edit: I should have been using the AKCallbackInstrument class instead, and overriding it's start() function.
Ben,
Without seeing your entire project, I would guess that if your project is able to receive and trigger MIDI notes, then it's an issue with only sending its output to the UILabel. I recommend using NotificationCenter to inform the ViewController when a MIDI event has been received in the Conductor class. Be sure to add the DispatchQueue.main.async code, or else the text won't update as expected. This was noted in the AudioKit Google Group here.
Example:
DispatchQueue.main.async(execute: {
nc.post(name: NSNotification.Name(rawValue: "outputMessage"),
object: nil,
userInfo: [
"message": self.outputMIDIMessage,
"midiSignalReceived": self.midiSignalReceived,
"midiTypeReceived": self.midiTypeReceived
])
})
I would also recommend the following:
Move let midi = AKMIDI() into an instance variable outside of the init() at the top of your Conductor class, rather than inside of it. It looks like you're attempting to create it after AudioKit.start().
I posted a sample project that demonstrates how you can change a UILabel's color whenever a MIDI note number has been received via AudioKit:
https://github.com/markjeschke/AKMidiReceiver
Conductor class:
import AudioKit
enum MidiEventType: String {
case
noteNumber = "Note Number",
continuousControl = "Continuous Control",
programChange = "Program Change"
}
class Conductor: AKMIDIListener {
// Globally accessible
static let sharedInstance = Conductor()
// Set the instance variables outside of the init()
let midi = AKMIDI()
var demoSampler = SamplerAudioFileLoader()
var samplerMixer = AKMixer()
var outputMIDIMessage = ""
var midiSignalReceived = false
var midiTypeReceived: MidiEventType = .noteNumber
init() {
// Session settings
AKSettings.bufferLength = .medium
AKSettings.defaultToSpeaker = true
// Allow audio to play while the iOS device is muted.
AKSettings.playbackWhileMuted = true
do {
try AKSettings.setSession(category: .playAndRecord, with: [.defaultToSpeaker, .allowBluetooth, .mixWithOthers])
} catch {
AKLog("Could not set session category.")
}
// File path options are:
// "TX Brass"
// "TX LoTine81z"
// "TX Metalimba"
// "TX Pluck Bass"
demoSampler.loadEXS24Sample(filePath: "TX Brass")
// If you wish to load a wav file, comment the `loadEXS24` method and uncomment this one:
// demoSampler.loadWavSample(filePath: "Kick") // Load Kick wav file
[demoSampler] >>> samplerMixer
AudioKit.output = samplerMixer
AudioKit.start()
// MIDI Configure
midi.createVirtualInputPort(98909, name: "AKMidiReceiver")
midi.createVirtualOutputPort(97789, name: "AKMidiReceiver")
midi.openInput()
midi.openOutput()
midi.addListener(self)
}
// Capture the MIDI Text within a DispatchQueue, so that it's on the main thread.
// Otherwise, it won't display.
func captureMIDIText() {
let nc = NotificationCenter.default
DispatchQueue.main.async(execute: {
nc.post(name: NSNotification.Name(rawValue: "outputMessage"),
object: nil,
userInfo: [
"message": self.outputMIDIMessage,
"midiSignalReceived": self.midiSignalReceived,
"midiTypeReceived": self.midiTypeReceived
])
})
}
// MARK: MIDI received
// Note On Number + Velocity + MIDI Channel
func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel) {
midiTypeReceived = .noteNumber
outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1) noteOn: \(noteNumber) velocity: \(velocity)"
print(outputMIDIMessage)
midiSignalReceived = true
captureMIDIText()
playNote(note: noteNumber, velocity: velocity, channel: channel)
}
// Note Off Number + Velocity + MIDI Channel
func receivedMIDINoteOff(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel) {
midiTypeReceived = .noteNumber
outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1) noteOff: \(noteNumber) velocity: \(velocity)"
print(outputMIDIMessage)
midiSignalReceived = false
captureMIDIText()
stopNote(note: noteNumber, channel: channel)
}
// Controller Number + Value + MIDI Channel
func receivedMIDIController(_ controller: MIDIByte, value: MIDIByte, channel: MIDIChannel) {
// If the controller value reaches 127 or above, then trigger the `demoSampler` note.
// If the controller value is less, then stop the note.
// This creates an on/off type of "momentary" MIDI messaging.
if value >= 127 {
playNote(note: 30 + controller, velocity: 80, channel: channel)
} else {
stopNote(note: 30 + controller, channel: channel)
}
midiTypeReceived = .continuousControl
outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1) controller: \(controller) value: \(value)"
midiSignalReceived = true
captureMIDIText()
}
// Program Change Number + MIDI Channel
func receivedMIDIProgramChange(_ program: MIDIByte, channel: MIDIChannel) {
// Trigger the `demoSampler` note and release it after half a second (0.5), since program changes don't have a note off release.
triggerSamplerNote(program, channel: channel)
midiTypeReceived = .programChange
outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1) programChange: \(program)"
midiSignalReceived = true
captureMIDIText()
}
func receivedMIDISetupChange() {
print("midi setup change")
print("midi.inputNames: \(midi.inputNames)")
let listInputNames = midi.inputNames
for inputNames in listInputNames {
print("inputNames: \(inputNames)")
midi.openInput(inputNames)
}
}
func playNote(note: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel) {
demoSampler.play(noteNumber: note, velocity: velocity, channel: channel)
}
func stopNote(note: MIDINoteNumber, channel: MIDIChannel) {
demoSampler.stop(noteNumber: note, channel: channel)
}
func triggerSamplerNote(_ program: MIDIByte, channel: MIDIChannel) {
playNote(note: 60 + program, velocity: 80, channel: channel)
let releaseNoteDelay = DispatchTime.now() + 0.5 // Change 0.5 to desired number of seconds
DispatchQueue.main.asyncAfter(deadline: releaseNoteDelay) {
self.stopNote(note: 60 + program, channel: channel)
self.midiSignalReceived = false
}
}
}
ViewController with the UILabel:
import UIKit
import AudioKit
class ViewController: UIViewController {
#IBOutlet weak var outputTextLabel: UILabel!
var conductor = Conductor.sharedInstance
var midiSignalReceived = false
var midiTypeReceived: MidiEventType = .noteNumber
override func viewDidLoad() {
super.viewDidLoad()
let nc = NotificationCenter.default
nc.addObserver(forName:NSNotification.Name(rawValue: "outputMessage"), object:nil, queue:nil, using:catchNotification)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
flashBackgroundColor()
midiSignalReceived = false
self.outputTextLabel.text = "Listening for MIDI events..."
}
#objc func catchNotification(notification:Notification) -> Void {
guard
let userInfo = notification.userInfo,
let message = userInfo["message"] as? String,
let midiSignalReceived = userInfo["midiSignalReceived"] as? Bool,
let midiTypeReceived = userInfo["midiTypeReceived"] as? MidiEventType else {
print("No userInfo found in notification")
return
}
DispatchQueue.main.async(execute: {
self.outputTextLabel.text = message
self.midiSignalReceived = midiSignalReceived
self.midiTypeReceived = midiTypeReceived
self.flashBackgroundColor()
})
}
#objc func flashBackgroundColor() {
if midiSignalReceived {
self.outputTextLabel.backgroundColor = UIColor.green
self.view.backgroundColor = UIColor.lightGray
if midiTypeReceived != .noteNumber {
self.perform(#selector(dismissFlashBackgroundColor), with: nil, afterDelay: 0.5)
}
} else {
dismissFlashBackgroundColor()
}
}
#objc func dismissFlashBackgroundColor() {
UIView.animate(withDuration: 0.5) {
self.outputTextLabel.backgroundColor = UIColor.clear
self.view.backgroundColor = UIColor.white
self.midiSignalReceived = false
self.conductor.midiSignalReceived = false
}
}
deinit {
NotificationCenter.default.removeObserver(self,
name: NSNotification.Name(rawValue: "outputMessage"),
object: nil)
}
}
SamplerAudioFileLoader.swift:
import AudioKit
class SamplerAudioFileLoader: AKMIDISampler {
internal func loadWavSample(filePath: String) {
do {
try self.loadWav("Sounds/\(filePath)")
} catch {
print("Could not locate the Wav file.")
}
}
internal func loadEXS24Sample(filePath: String) {
do {
try self.loadEXS24("Sounds/Sampler Instruments/\(filePath)")
} catch {
print("Could not locate the EXS24 file.")
}
}
}
I hope this helps. Please let me know if you have any questions about this.
Take care,
Mark
P.S. If you clone this AKMidiReceiver example, open the Workspace, and no scheme appears in the Xcode project, please follow these steps that were found here:
Click on No Scheme
Click on Manage Scheme
Click on Autocreate Schemes Now
Depending on how you initialize flasher, you may have to run flasher.enableMIDI() optionally with names.

Is there a way to set AVSpeechBoundary to a certain value or after the sentence is completed?

I want to pause speech utterance and it has to complete the current sentence it is speaking out then it has to pause but the API provides only two pause types immediate and word not current sentence. I tried this,
myutterance = AVSpeechUtterance(string:readTextView.text)
synth .speak(myutterance)
synth .pauseSpeaking(at: AVSpeechBoundary.immediate)
But it pauses immediately after text is completed.
I just tried whatever you did and it read the whole sentence without giving enough pause,
let someText = "Some Sample text. That will read and pause after every sentence"
let speechSynthesizer = AVSpeechSynthesizer()
let myutterance = AVSpeechUtterance(string:someText)
speechSynthesizer .speak(myutterance)
//Normal reading without pause
speechSynthesizer .pauseSpeaking(at: AVSpeechBoundary.immediate)
To pause after every sentence you can break the whole text to simple components and read them individually in a loop like below by using the postUtteranceDelay property.
//To pause after every sentence
let components = someText.components(separatedBy: ".")
for str in components{
let myutterance = AVSpeechUtterance(string:str)
speechSynthesizer .speak(myutterance)
myutterance.postUtteranceDelay = 1 //Set it to whatever value you would want.
}
To pause after completing the currently speaking sentence we need to do a tiny hack,
var isPaused:Bool = false
let someText = "Some Sample text. That will read and pause after every sentence. The next sentence should'nt be heard if you had pressed the pause button. Let us try this."
let speechSynthesizer = AVSpeechSynthesizer()
var currentUtterance:AVSpeechUtterance?
override func viewDidLoad() {
super.viewDidLoad()
speechSynthesizer.delegate = self
}
#IBAction func startSpeaking(_ sender: Any) {
//Pause after every sentence
let components = someText.components(separatedBy: ".")
for str in components{
let myutterance = AVSpeechUtterance(string:str)
speechSynthesizer .speak(myutterance)
myutterance.postUtteranceDelay = 1
}
}
#IBAction func pauseSpeaking(_ sender: Any) {
isPaused = !isPaused
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance){
print("location\(characterRange.location)")
print("Range + Length\(characterRange.location + characterRange.length)")
print("currentUtterance!.speechString\(currentUtterance!.speechString)")
print("currentUtterance!.count\(currentUtterance!.speechString.characters.count)")
if isPaused && (characterRange.location + characterRange.length) == currentUtterance!.speechString.characters.count{
speechSynthesizer.stopSpeaking(at:.word)
}
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance){
currentUtterance = utterance
}

AVAudioSession never stopped

I'm trying to set my AVAudioSession to inactive to get back to normal state.
My utterance function:
class SSpeech : NSObject, AVSpeechSynthesizerDelegate {
var group = DispatchGroup();
var queue = DispatchQueue(label: "co.xxxx.speech", attributes: [])
class var sharedInstance: SSpeech {
struct Static {
static var instance: SSpeech?
}
if !(Static.instance != nil) {
Static.instance = SSpeech()
}
return Static.instance!
}
required override init() {
super.init();
self.speechsynt.delegate = self;
}
deinit {
print("deinit SSpeech")
}
let audioSession = AVAudioSession.sharedInstance();
var speechsynt: AVSpeechSynthesizer = AVSpeechSynthesizer()
var queueTalks = SQueue<String>();
func pause() {
speechsynt.pauseSpeaking(at: .word)
}
func talk(_ sentence: String, languageCode code:String = SUtils.selectedLanguage.code, withEndPausing: Bool = false) {
if SUser.sharedInstance.currentUser.value!.speechOn != 1 {
return
}
queue.async{
self.queueTalks.enQueue(sentence)
do {
let category = AVAudioSessionCategoryPlayback;
var categoryOptions = AVAudioSessionCategoryOptions.duckOthers
if #available(iOS 9.0, *) {
categoryOptions.formUnion(AVAudioSessionCategoryOptions.interruptSpokenAudioAndMixWithOthers)
}
try self.audioSession.setCategory(category, with: categoryOptions)
try self.audioSession.setActive(true);
} catch _ {
return;
}
self.utteranceTalk(sentence, initSentence: false, speechsynt: self.speechsynt, languageCode:code, withEndPausing: withEndPausing)
do {
try self.audioSession.setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
} catch _ {
return;
}
}
}
func utteranceTalk(_ sentence: String, initSentence: Bool, speechsynt: AVSpeechSynthesizer, languageCode:String = "en-US", withEndPausing: Bool = false){
if SUser.sharedInstance.currentUser.value!.speechOn != 1 {
return
}
let nextSpeech:AVSpeechUtterance = AVSpeechUtterance(string: sentence)
nextSpeech.voice = AVSpeechSynthesisVoice(language: languageCode)
if !initSentence {
nextSpeech.rate = 0.4;
}
if(withEndPausing){
nextSpeech.postUtteranceDelay = 0.2;
}
speechsynt.speak(nextSpeech)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance:AVSpeechUtterance) {
print("Speaker has finished to talk")
queue.async {
do {
try self.audioSession.setActive(false, with: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation)
}
catch {}
}
}
}
}
My method is correctly called, but my audioSession still active when the utterance is finished. i've tried lot of thing but nothing work :(.
I would suggest using an AvAudioPlayer. They have very easy start and stop commands.
first declare the audio player as a variable
var SoundEffect: AVAudioPlayer!
then select the file you need
let path = Bundle.main.path(forResource: "Untitled2.wav", ofType:nil)!
let url = URL(fileURLWithPath: path)
let sound = try AVAudioPlayer(contentsOf: url)
SoundEffect = sound
sound.numberOfLoops = -1
sound.play()
and to stop the audio player
if SoundEffect != nil {
SoundEffect.stop()
SoundEffect = nil
}
You cannot stop or deactive AudioSession, your app gets it upon launching. Documentation:
An audio session is the intermediary between your app and iOS used to configure your app’s audio behavior. Upon launch, your app automatically gets a singleton audio session.
So method -setActive: does not make your AudioSession "active", it just puts its category and mode configuration into action. For getting back to the "normal state", you could set default settings or just call setActive(false, with:.notifyOthersOnDeactivation), that will be enough.
A part from documentation of AVAudioSession:
Discussion
If another active audio session has higher priority than yours (for
example, a phone call), and neither audio session allows mixing,
attempting to activate your audio session fails. Deactivating your
session will fail if any associated audio objects (such as queues,
converters, players, or recorders) are currently running.
My guess is that the failure to deactivate the session is the running process(es) of your queue as I highlighted in the document quote.
Probably you should make the deactivation process synchronous instead of asynchronous OR make sure that all the running actions under your queue has been processed.
Give this a try:
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance:AVSpeechUtterance) {
print("Speaker has finished to talk")
queue.sync { // <---- `async` changed to `sync`
do {
try self.audioSession.setActive(false, with: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation)
}
catch {}
}
}
}

Unduck AVAudioSession with SKTTSPlayer

It has been a while since I tried to properly mix the SKTTSPlayer with the AVAudioSession and I have many problems that I can't resolve.
I want the SKTTSPlayer to duck the other audio when it is playing and unduck the other audio when it has finished playing.
Normally, with the AVSpeechSynthesizer, it is pretty easy to duck and unduck the other audio, here is a block of code that does this work pretty easily :
override func viewDidLoad() {
super.viewDidLoad()
var audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback, withOptions: .DuckOthers)
} catch {
// TODO: Handle error
}
synth.delegate = self
speak("I really want to unduck this session")
}
func setAudioSessionActive(beActive: Bool) {
do {
try AVAudioSession.sharedInstance().setActive(beActive)
print("Setting AVAudiosession active: ", beActive)
} catch let error as NSError {
print("Setting AVAudiosession state failed: \(error.description)")
}
}
func speak(string: String) {
let utterance = AVSpeechUtterance(string: string)
utterance.rate = AVSpeechUtteranceDefaultSpeechRate / 2
utterance.voice = AVSpeechSynthesisVoice(language: language)
synth.speakUtterance(utterance)
}
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didFinishSpeechUtterance utterance: AVSpeechUtterance) {
if (synthesizer.speaking) {
return
}
setAudioSessionActive(false)
}
As you can see, this can do the work pretty easily, you activate the session once and then everytime an utterance is finished, you desactivate the seesion.
This is not so simple with the SKTTSPlayer. Even if it seems that the SKTTSPlayer is an object of AVSpeechSynthesizer modified by Skobbler, you cannot unduck a session by just desactivate it. If you does that, the session will unDuck and after Duck again.
Here is a block of code that can reproduce the error :
override func viewDidLoad() {
super.viewDidLoad()
var audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback, withOptions: .DuckOthers)
} catch {
// TODO: Handle error
}
let advisorSettings = SKAdvisorSettings()
advisorSettings.language = SKAdvisorLanguage.EN_US
advisorSettings.advisorType = SKAdvisorType.TextToSpeech
SKRoutingService.sharedInstance().advisorConfigurationSettings = advisorSettings
let ttsSettings = SKAdvisorTTSSettings()
ttsSettings.rate = 0.08
ttsSettings.pitchMultiplier = 0.8
ttsSettings.volume = 1
ttsSettings.preUtteranceDelay = 0.1
ttsSettings.postUtteranceDelay = 0.1
SKTTSPlayer.sharedInstance().textToSpeechConfig = ttsSettings
SKTTSPlayer.sharedInstance().delegate = self
speak("I really want to unduck this session")
}
func setAudioSessionActive(beActive: Bool) {
do {
try AVAudioSession.sharedInstance().setActive(beActive)
print("Setting AVAudiosession active: ", beActive)
} catch let error as NSError {
print("Setting AVAudiosession state failed: \(error.description)")
}
}
func speak(string: String) {
SKTTSPlayer.sharedInstance().playString(string, forLanguage: SKAdvisorLanguage.EN_US)
}
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didFinishSpeechUtterance utterance: AVSpeechUtterance) {
setAudioSessionActive(false)
}
func TTSPlayer(TTSPlayer: SKTTSPlayer!, willPlayUtterance utterance: AVSpeechUtterance!) {
NSLog("will play")
}
The only workaround I found for this problem is to create a blanksound with an AVAudioPlayer. This sound is played everytime an utterance is finished and the session is unDuck when the blanksound is finished.
override func viewDidLoad() {
super.viewDidLoad()
var audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback, withOptions: .DuckOthers)
} catch {
// TODO: Handle error
}
let advisorSettings = SKAdvisorSettings()
advisorSettings.language = SKAdvisorLanguage.EN_US
advisorSettings.advisorType = SKAdvisorType.TextToSpeech
SKRoutingService.sharedInstance().advisorConfigurationSettings = advisorSettings
let ttsSettings = SKAdvisorTTSSettings()
ttsSettings.rate = 0.08
ttsSettings.pitchMultiplier = 0.8
ttsSettings.volume = 1
ttsSettings.preUtteranceDelay = 0.1
ttsSettings.postUtteranceDelay = 0.1
SKTTSPlayer.sharedInstance().textToSpeechConfig = ttsSettings
SKTTSPlayer.sharedInstance().delegate = self
speak("I really want to unduck this session")
}
func setAudioSessionActive(beActive: Bool) {
do {
try AVAudioSession.sharedInstance().setActive(beActive)
print("Setting AVAudiosession active: ", beActive)
} catch let error as NSError {
print("Setting AVAudiosession state failed: \(error.description)")
}
}
func speak(string: String) {
SKTTSPlayer.sharedInstance().playString(string, forLanguage: SKAdvisorLanguage.EN_US)
}
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didFinishSpeechUtterance utterance: AVSpeechUtterance) {
initializeAudioPlayer()
}
func TTSPlayer(TTSPlayer: SKTTSPlayer!, willPlayUtterance utterance: AVSpeechUtterance!) {
NSLog("will play")
}
func initializeAudioPlayer(){
let blankSoundURL = NSBundle.mainBundle().pathForResource("blankSound", ofType: "mp3")
if !NSFileManager.defaultManager().fileExistsAtPath(blankSoundURL!)
{
return
}
else
{
do {
audioPlayer = try? AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: blankSoundURL!), fileTypeHint: nil)
audioPlayer.delegate = self
audioPlayer.prepareToPlay()
audioPlayer.play()
}
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
setAudioSessionActive(false)
}
This is, I think, pretty difficult for nothing and it makes the AVAudioSession hard to work with.
Do you guys have any ideas why I can't unDuck a session with the SKTTSPlayer in the normal way ?
Thank you for reading, I know this is a pretty long post, but I think it might help someone.
Using the TTS option
Set our audio engine to TTS (this way you'll receive the text that needs
to be played)
Implement the didUpdateFilteredAudioInstruction callback ­ here is where
the text instruction is received.
At this time you will have the play the instruction using your custom
TTS engine ­ this is implementation specific to your 3'rd party engine
Return false (this will stop the default tts engine from playing the
instruction again)
The end code will look something like this:
(BOOL)routingService:(SKRoutingService )routingService
didUpdateFilteredAudioInstruction:(NSString)instruction
forLanguage:(SKAdvisorLanguage)language {
//your code goes here - you need to play the "instruction" in a
certain "language"
return false;
}
Using your own AVSpeechSynthesizer:
If it's a TTS engine issue then the only way of addressing this would be
to change the TTS engine you are using (we have limited control over it ­
see
http://developer.skobbler.com/docs/ios/2.5.1/Classes/SKAdvisorTTSSettings.h
tml ). See also the iOS API documentation:
https://developer.apple.com/library/prerelease/ios/documentation/AVFoundati
on/Reference/AVSpeechSynthesizer_Ref/index.html

Resources