Unduck AVAudioSession with SKTTSPlayer - ios

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

Related

Why does Swift AVAudioPlayer not change state after MPRemoteCommandCenter play command?

The environment for this is iOS 13.6 and Swift 5. I have a very simple app that successfully plays an MP3 file in the foreground or background. I added MPRemoteCommandCenter play and pause command handlers to it. I play the sound file in the foreground and then pause it.
When I tap the play button from the lock screen, my code calls audioPlayer.play(), which returns true. I hear the sound start playing again, but the currentTime of the player does not advance. After that, the play and pause buttons on the lock screen do nothing. When I foreground the app again, the play button plays from where it was before I went to the lock screen.
Here is my AudioPlayer class:
import AVFoundation
import MediaPlayer
class AudioPlayer: RemoteAudioCommandDelegate {
var audioPlayer = AVAudioPlayer()
let remoteCommandHandler = RemoteAudioCommandHandler()
var timer:Timer!
func play(title: String) {
let path = Bundle.main.path(forResource: title, ofType: "mp3")!
let url = URL(fileURLWithPath: path)
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
try AVAudioSession.sharedInstance().setActive(true)
audioPlayer = try AVAudioPlayer(contentsOf: url)
remoteCommandHandler.delegate = self
remoteCommandHandler.enableDisableRemoteCommands(true)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateNowPlayingInfo), userInfo: nil, repeats: true)
} catch let error as NSError {
print("error = \(error)")
}
}
func play() {
print ("audioPlayer.play() returned \(audioPlayer.play())")
}
func pause() {
audioPlayer.pause()
}
func stop() {
audioPlayer.stop()
}
func currentTime() -> TimeInterval {
return audioPlayer.currentTime
}
func setCurrentTime(_ time:TimeInterval) {
audioPlayer.currentTime = time
}
#objc func updateNowPlayingInfo() {
// Hard-code the nowPlayingInfo since this is a simple test app
var nowPlayingDict =
[MPMediaItemPropertyTitle: "Tin Man",
MPMediaItemPropertyAlbumTitle: "The Complete Greatest Hits",
MPMediaItemPropertyAlbumTrackNumber: NSNumber(value: UInt(10) as UInt),
MPMediaItemPropertyArtist: "America",
MPMediaItemPropertyPlaybackDuration: 208,
MPNowPlayingInfoPropertyPlaybackRate: NSNumber(value: 1.0 as Float)] as [String : Any]
nowPlayingDict[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: audioPlayer.currentTime as Double)
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingDict
}
}
Here is my RemoteCommandHandler class:
import Foundation
import MediaPlayer
protocol RemoteAudioCommandDelegate: class {
func play()
func pause()
}
class RemoteAudioCommandHandler: NSObject {
weak var delegate: RemoteAudioCommandDelegate?
var remoteCommandCenter = MPRemoteCommandCenter.shared()
var playTarget: Any? = nil
var pauseTarget: Any? = nil
func enableDisableRemoteCommands(_ enabled: Bool) {
print("Called with enabled = \(enabled)")
remoteCommandCenter.playCommand.isEnabled = enabled
remoteCommandCenter.pauseCommand.isEnabled = enabled
if enabled {
addRemoteCommandHandlers()
} else {
removeRemoteCommandHandlers()
}
}
fileprivate func addRemoteCommandHandlers() {
print( "Entered")
if playTarget == nil {
print( "Adding playTarget")
playTarget = remoteCommandCenter.playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
print("addRemoteCommandHandlers calling delegate play")
self.delegate?.play()
return .success
}
}
if pauseTarget == nil {
print( "Adding pauseTarget")
pauseTarget = remoteCommandCenter.pauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
print("addRemoteCommandHandlers calling delegate pause")
self.delegate?.pause()
return .success
}
}
}
fileprivate func removeRemoteCommandHandlers() {
print( "Entered")
if playTarget != nil {
print( "Removing playTarget")
remoteCommandCenter.playCommand.removeTarget(playTarget)
playTarget = nil
}
if pauseTarget != nil {
print( "Removing pauseTarget")
remoteCommandCenter.pauseCommand.removeTarget(pauseTarget)
pauseTarget = nil
}
}
}
I will gladly supply further required info, because I'm baffled at why this relatively straightforward code (in my mind) code doesn't work.
Assistance is much appreciated!
After some more debugging, I found that the AVAudioPlayer started to play the sound from the lock screen, but stopped again shortly after.
I mitigated the problem by adding a Timer. The timer checks if the last command by the user was play, but the sound is not playing. I also change the status when the user selects pause or the song stops playing at its natural end.
I am still at a loss for an actual fix for this problem.

AVAudioPlayer 'forgets' that its playing when triggered by MPRemoteCommand

I'm trying to play an audiofile and control it's playback via the Remote Command Centre available on the lock screen.
If I do the following:
Begin playback
Pause playback
Lock device
Begin playback from lockscreen (MPRemoteCommandCenter)
It is then impossible to pause playback from lockscreen. The button flickers and nothing happens.
How can I fix this?
Further details below:
It appears that when attempting to pause the audio, the AVAudioPlayer returns 'false' for audioPlayer.isPlaying.
This occurs on iOS13.1 on my iPhoneXR, iPhoneSE and iPhone8. I have no other devices to test against
The logs indicate that AVAudioPlayer.isPlaying initially returns true when playback is started, but subsequently returns false. The player's currentTime also appears stuck at around the time playback was started.
My entire view controller is below (~100 lines). This is the minimum necessary to reproduce the problem.
The this example project demonstrating the error is also available on Github here.
import UIKit
import MediaPlayer
class ViewController: UIViewController {
#IBOutlet weak var playPauseButton: UIButton!
#IBAction func playPauseButtonTap(_ sender: Any) {
if self.audioPlayer.isPlaying {
pause()
} else {
play()
}
}
private var audioPlayer: AVAudioPlayer!
private var hasPlayed = false
override func viewDidLoad() {
super.viewDidLoad()
let fileUrl = Bundle.main.url(forResource: "temp/intro", withExtension: ".mp3")
try! self.audioPlayer = AVAudioPlayer(contentsOf: fileUrl!)
let audioSession = AVAudioSession.sharedInstance()
do { // play on speakers if headphones not plugged in
try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
} catch let error as NSError {
print("Override headphones failed, probably because none are available: \(error.localizedDescription)")
}
do {
try audioSession.setCategory(.playback, mode: .spokenAudio)
try audioSession.setActive(true)
} catch let error as NSError {
print("Warning: Setting audio category to .playback|.spokenAudio failed: \(error.localizedDescription)")
}
playPauseButton.setTitle("Play", for: .normal)
}
func play() {
playPauseButton.setTitle("Pause", for: .normal)
self.audioPlayer.play()
if(!hasPlayed){
self.setupRemoteTransportControls()
self.hasPlayed = true
}
}
func pause() {
playPauseButton.setTitle("Play", for: .normal)
self.audioPlayer.pause()
}
// MARK: Remote Transport Protocol
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(".......................")
print(self.audioPlayer.currentTime)
let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
print("\(address) not playing: \(!self.audioPlayer.isPlaying)")
guard !self.audioPlayer.isPlaying else { return .commandFailed }
print("attempting to play")
let success = self.audioPlayer.play()
print("play() invoked with success \(success)")
print("now playing \(self.audioPlayer.isPlaying)")
return success ? .success : .commandFailed
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(".......................")
print(self.audioPlayer.currentTime)
let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
print("\(address) playing: \(self.audioPlayer.isPlaying)")
guard self.audioPlayer.isPlaying else { return .commandFailed }
print("attempting to pause")
self.pause()
print("pause() invoked")
return .success
}
private func setupRemoteTransportControls() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget(self, action: #selector(self.handlePlay))
commandCenter.pauseCommand.addTarget(self, action: #selector(self.handlePause))
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = "Major title"
nowPlayingInfo[MPMediaItemPropertyTitle] = "Minor Title"
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.audioPlayer.duration
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
This logs the following (with my // comments added):
.......................
1.438140589569161 // audio was paused here
0x0000000283361cc0 not playing: true // player correctly says its not playing
attempting to play // so it'll start to play
play() invoked with success true // play() successfully invoked
now playing true // and the player correctly reports it's playing
.......................
1.4954875283446711 // The player thinks it's being playing for about half a second
0x0000000283361cc0 playing: false // and has now paused??? WTF?
.......................
1.4954875283446711 // but there's definitely sound coming from the speakers. It has **NOT** paused.
0x0000000283361cc0 playing: false // yet it thinks it's paused?
// note that the memory addresses are the same. This seems to be the same player. ='(
I'm at my wits' end. Help me StackOverflow—You're my only hope.
Edits: I've also tried
Always returning .success
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
guard !self.audioPlayer.isPlaying else { return .success }
self.audioPlayer.play()
return .success
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(self.audioPlayer.isPlaying)
guard self.audioPlayer.isPlaying else { return .success }
self.pause()
return .success
}
Ignoring the audioPlayer state and just doing as the remote command centre says
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
self.audioPlayer.play()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
return .success
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
self.audioPlayer.pause()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
return .success
}
Both of these result in the bug persisting the first time pause is tapped on the lock screen. Subsequent taps reset the audio to the original paused position and then work normally.
Update
Replacing AVAudioPlayer with AVPlayer appears to make the problem go away entirely! I'm reasonably sure this is a bug in AVAudioPlayer now.
The necessary steps to switch to AVPlayer are in this public diff
I've used one of my developer-question tickets and submitted this question to Apple. I'll post an answer when I hear back from them.
Update 2
Apple Dev Support confirmed that as of 5 Dec 2019 there's no known workaround for this issue. I've submitted an issue to feedbackassistant.apple.com and will update this answer when something changes.
This is indeed a bug in AVAudioPlayer.
Another workaround if you dont want to switch to AVPlayer is to simply check if playing before pausing and if not, call play just before pause. It's not pretty but it works:
if (!self.player.isPlaying) [self.player play];
[self.player pause];

Recorded audio fails to play on device but not iOS simulator

I'm using AVAudioRecorder to record audio in one view controller and then display recorded audio on a table in another
var numberOfRecords = 0
var recordingSession:AVAudioSession!
var audioRecorder:AVAudioRecorder!
#IBAction func recordAudio(_ sender: UIButton) {
// Check if we have an active audio recorder
if audioRecorder == nil {
numberOfRecords = numberOfRecords + 1
let filename = getDirectory().appendingPathComponent("\(numberOfRecords).m4a")
let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC), AVSampleRateKey: 12000, AVNumberOfChannelsKey: 1, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
// Start audio recording
do {
audioRecorder = try AVAudioRecorder(url: filename, settings: settings)
print(filename)
audioRecorder.delegate = self
audioRecorder.record()
} catch {
displayAlert(title: "Oops!", message: "Recording Failed")
}
} else {
// Stopping audio recording
audioRecorder.stop()
audioRecorder = nil
UserDefaults.standard.set(numberOfRecords, forKey: "myNumber")
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Record
recordingSession = AVAudioSession.sharedInstance()
if let number:Int = UserDefaults.standard.object(forKey: "myNumber") as? Int {
numberOfRecords = number
}
AVAudioSession.sharedInstance().requestRecordPermission { (hasPermission) in
if hasPermission {
}
}
}
That's the code to record audio in the first view controller and here's the code to playback the audio in the second view controller
var audioPlayer:AVAudioPlayer!
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let path = getDirectory().appendingPathComponent("\(indexPath.row + 1).m4a")
do {
audioPlayer = try AVAudioPlayer(contentsOf: path)
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print(error)
}
When I try to play it back on my iPhone I get this error:
Error Domain=NSOSStatusErrorDomain Code=1685348671 "(null)"
I've been having this issue for many weeks and still haven't found a solution. I've tried a lot of things online like changing the AVSampleRateKey, the file type it saves as and nothing works. I really don't know what I'm doing wrong. I don't know if it's the fact that I'm recording in one view controller and playing in another but it only plays back the audio in the Xcode simulator and not my physical device.
more code to viewdidload:
do {
try recordingSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try recordingSession.setActive(true)
AVAudioSession.sharedInstance().requestRecordPermission { (hasPermission) in
if hasPermission {
}
}
} catch {
}
My guess is that you are mishandling your audio session. It is crucial. You must not retain a reference to your audio session like that; it is not a thing, but a pipeline to talk to the media services daemon. For recording, you must configure a Recording category on the session, and you must activate the session. For playback, change your category to a playback category and activate that session.

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 {}
}
}
}

iOS, swift: play background music and sound effects without delay

I am trying to create a game in iOS without using SpriteKit.
I am stuck in getting the sound effects to play in a timely manner. I've been using the following code which I have found online and the background music plays great. However, when I use the
"playSoundEffect" method it plays ok the first time but then starts to lag behind and becomes out of sync. I guess that happens because it initializes an AVAudioPlayer every time.
Anyone have a good idea in how to play sound effects in a timely manner, while also playing background music? Thanks!
import AVFoundation
public class SKTAudio: NSObject, AVAudioPlayerDelegate {
public var backgroundMusicPlayer: AVAudioPlayer?
public var soundEffectPlayer: AVAudioPlayer?
private var mainLoopFileName:String! {
let randomSong = Int(arc4random_uniform(3))
switch randomSong {
//case 0: return "Test.mp3"
//case 1: return "Test2.mp3"
case 0: return "SneakySnitch.mp3"
case 1: return "FasterDoesIt.mp3"
case 2: return "MonkeysSpinningMonkeys.mp3"
default:
break
}
return "SneakySnitch.mp3"
}
public class func sharedInstance() -> SKTAudio {
return SKTAudioInstance
}
public func playBackgroundMusic() {
let filename = mainLoopFileName
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
if (url == nil) {
println("Could not find file: \(filename)")
return
}
var error: NSError? = nil
backgroundMusicPlayer = AVAudioPlayer(contentsOfURL: url, error: &error)
if let player = backgroundMusicPlayer {
player.numberOfLoops = 0
player.delegate = self
player.prepareToPlay()
player.play()
} else {
println("Could not create audio player: \(error!)")
}
}
public func pauseBackgroundMusic() {
if let player = backgroundMusicPlayer {
if player.playing {
player.pause()
}
}
}
public func resumeBackgroundMusic() {
if let player = backgroundMusicPlayer {
if !player.playing {
player.play()
}
}
}
public func playSoundEffect(filename: String) {
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
if (url == nil) {
println("Could not find file: \(filename)")
return
}
var error: NSError? = nil
soundEffectPlayer = AVAudioPlayer(contentsOfURL: url, error: &error)
if let player = soundEffectPlayer {
player.numberOfLoops = 0
player.prepareToPlay()
player.play()
} else {
println("Could not create audio player: \(error!)")
}
}
// MARK: AVAudioPlayerDelegate
public func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool) {
println("finished playing \(flag)")
delay(5.0, {
self.playBackgroundMusic()
})
}
public func audioPlayerDecodeErrorDidOccur(player: AVAudioPlayer!, error: NSError!) {
println("\(error.localizedDescription)")
}
}
You could using AVPlayer to play your sound file. Keep one player, but change its AVPlayerItem to a new item when you need to play a new sound. It might be faster than recreating the player every time.
While AVAudioPlayer/AVPlayer is the simplest option, it will not give you the shortest delay or perfect synchronization when playing audio files. You should look into Audio Queues or Audio Units within Core Audio for more accurate sound playback.
The problem is that you are playing the second sound effect before the first one is finished. You are killing the reference to the first when you set the new AVAudioPlayer to your soundEffectPlayer variable and the first will stop playing.
If you don't mind loosing the availability of controlling the volume of the sound you could use this:
var mySound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(url, &mySound)
// Play
AudioServicesPlaySystemSound(mySound)
Otherwise you can use AVAudioPlayer if you add each sound that you create into an array, keeping the reference. Then you can delete it when the sound is done by implementing AVAudioPlayer delegate.
func playSound(){
let path : NSString? = NSBundle.mainBundle().pathForResource("sound", ofType: "wav")!
let url = NSURL(fileURLWithPath: path!)
var error : NSError?
let sound = AVAudioPlayer(contentsOfURL: url, error: &error)
sound.delegate = self
sound.volume = 0.5
self.soundsEffectsArray.addObject(sound)
sound.prepareToPlay()
sound.play()
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool) {
self.soundsEffectsArray.removeObject(player)
}

Resources