Calling stop() on AVAudioPlayerNode after finished playing causes crash - swift

I have an AVAudioPlayerNode object which sends a callback once it is finished playing. That callback triggers many other functions in the app, one of which sends out a courtesy stop() message. For some reason, calling stop() at the moment the AVAudioPlayerNode finishes causes a crash. In the code here, I have abbreviated it so that the AVAudioPlayerNode just calls stop immediately to demonstrate the effect (rather than including my whole application). You can see clearly that it crashes. I don't understand why. Either a) the node is still playing and stop() stops it or b) it is done playing and stop can be ignored.
My guess is that this is some edge case where it is at the very end of the file buffer, and it is in an ambiguous state where has no more buffers remaining, yet is technically still playing. Perhaps calling stop() tries to flush the remaining buffers and they are empty?
func testAVAudioPlayerNode(){
let testBundle = Bundle(for: type(of: self))
let url = URL(fileURLWithPath: testBundle.path(forResource: "19_lyrics_1", ofType: "m4a")!)
let player = AVAudioPlayerNode()
let engine = AVAudioEngine()
let format = engine.mainMixerNode.outputFormat(forBus: 0)
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: format)
let delegate = FakePlayMonitorDelegate()
do {
let audioFile = try AVAudioFile(forReading: url)
let length = audioFile.length
player.scheduleFile(audioFile, at: nil, completionCallbackType: AVAudioPlayerNodeCompletionCallbackType.dataPlayedBack, completionHandler: {(completionType) in
print("playing = \(player.isPlaying)")
player.stop()
})
try engine.start()
let expectation = self.expectation(description: "playback")
delegate.expectation = expectation
player.play()
self.waitForExpectations(timeout: 6.0, handler: nil)
} catch {
}
}

In my case calling the .stop() method from the Main thread (you can use another) has resolved the issue.
DispatchQueue.main.async {
player.stop()
}
It can be the deadlock. I have noticed that the completionHandler is invoked from the background queue which is a serial queue, let's call it a "render queue".
Seems that the .stop() method is trying to do some work on a "render queue" but at the moment a "render queue" is busy with the completionHandler.

Related

Dispatch Source timer schedule timout

With the following Swift code, I'm trying to create a task that runs every hour:
let queue: DispatchQueue = .main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(3600), leeway: .milliseconds(100)
timer.setEventHandler { [weak self] in
// run code
}
Now, when I have the repeating set at a lower number, say 10 or event 150 seconds, it triggers as expected both in the foreground and background (or, rather, once the foreground hits it triggers, if the timer went off while in the background).
However, when I let the app timeout to the lock screen, and wait for an hour, it doesn't display.
Is there some timeout that Apple has for DispatchSource schedules? If so, what is it? And is there any way to change or get around it?
Edit
I don't want special functionality when it backgrounds, I want the code to keep running as normal and to trigger the event handler when the timeout happens, even if it's in the background
I ended up taking matt's suggestion and saving the time every time the code gets called, as seen below. Worked well!
let timeOfLastCheck = Date()
let queue: DispatchQueue = .main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(3600), leeway: .milliseconds(100)
timer.setEventHandler { [weak self] in
timeOfLastCheck = Date()
// run code
}
And elsewhere, where the timer is actually getting created:
let notificationCenter: NotificationCenter = .default
let activeNotificationToken = notificationCenter.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: nil
) { [weak self] _ in
let now = Date()
if let `self` = self,
let timeInterval = TimeInterval(dispatchTimeInterval: self.interval), // TimeInterval is extended elsewhere to be able to take in a DispatchTimeInterval in the init
now > timeOfLastCheck.addingTimeInterval(timeInterval) {
self.timeOfLastCheck = Date()
// run code
}
}

DispatchGroup On Do, Try, Catch?

I'm working with the AudioKit framework and looking to use DispatchGroup to make a method work async. I'd like for the player.load method to run only after the audioFile has been created; right now it's throwing an error ~50% of the time and I suspect it's due to timing. I've used DispatchGroup with success in other circumstances, but never in a do/try/catch. Is there a way to make this part of the function work with it? If not, is there a way to set up a closure? Thanks!
func createPlayer(fileName: String) -> AKPlayer {
let player = AKPlayer()
let audioFile : AKAudioFile
player.mixer >>> mixer
do {
try audioFile = AKAudioFile(readFileName: "\(fileName).mp3")
player.load(audioFile: audioFile)
print("AudioFile \(fileName), \(audioFile) loaded")
} catch { print("No audio file read, looking for \(fileName).mp3")
}
player.isLooping = false
player.fade.inTime = 2 // in seconds
player.fade.outTime = 2
player.stopEnvelopeTime = 2
player.completionHandler = {
print("Completion")
self.player.detach()
}
player.play()
return player
}

GCD `asyncAfter` does not start when app in background/inactive

I'm using AVAudioPlayer for playing records. Between each playback session I have interval from 0 to 10 sec. To make this interval I'm using AVAudioPlayerDelegate and when playing is finished I'm starting new playback after delay:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
guard let session = playbackSessionId,
let audioTrack = audioTrack,
let failureHandler = playingFailure,
let successHandler = playingSuccess else {
playingFinished(flag, error: nil)
return
}
print("audioPlayerDidFinishPlaying fired")
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + LoopInterval.currentInterval) { [weak self] in
print("asyncAfter fired")
guard let strongSelf = self,
let currentSession = strongSelf.playbackSessionId, session == currentSession else { return }
strongSelf.startPlayingRecordInLoop(audioTrack, success: successHandler, failure: failureHandler)
}
}
After app goes to the background (home button), audioPlayerDidFinishPlaying fires, but DispatchQueue.global(qos: .utility).asyncAfter not. So in console I see:
audioPlayerDidFinishPlaying fired
As soon as app become active, asyncAfter fires and I see next log message:
asyncAfter fired
When app is active, all works as expected.
Hope it'll help someone. I found problem: when app goes in background it stops background tasks, and fires them only after becomes active. To avoid need you should keep your app running in background and awaiting for your long-running background task.
backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
})
This method lets your app continue to run for a period of time after it transitions to the background. You should call this method at times where leaving a task unfinished might be detrimental to your app’s user experience. For example, your app could call this method to ensure that had enough time to transfer an important file to a remote server or at least attempt to make the transfer and note any errors. You should not use this method simply to keep your app running after it moves to the background.
After task finished you should call endBackgroundTask. If you won't end background task until backgroundTimeRemaining becomes 0, app will be terminated:
UIApplication.shared.endBackgroundTask(backgroundTaskID)
Each call to this method must be balanced by a matching call to the endBackgroundTask(:) method. Apps running background tasks have a finite amount of time in which to run them. (You can find out how much time is available using the backgroundTimeRemaining property.) If you do not call endBackgroundTask(:) for each task before time expires, the system kills the app. If you provide a block object in the handler parameter, the system calls your handler before time expires to give you a chance to end the task.
That's what I did in my case:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
guard let session = playbackSessionId,
let audioTrack = audioTrack,
let failureHandler = playingFailure,
let successHandler = playingSuccess else {
playingFinished(flag, error: nil)
return
}
backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
guard let taskId = self?.backgroundTaskID else { return }
UIApplication.shared.endBackgroundTask(taskId)
})
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + LoopInterval.currentInterval) { [weak self] in
guard let strongSelf = self,
let currentSession = strongSelf.playbackSessionId, session == currentSession else { return }
strongSelf.startPlayingRecordInLoop(audioTrack, success: successHandler, failure: failureHandler)
if let backgroundTaskID = strongSelf.backgroundTaskID {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
strongSelf.backgroundTaskID = nil
}
}
}

Xcode 8 Swift 3 Pitch-altering sounds

I'm trying to make a simple game with a hit sound that has a different pitch whenever you hit something. I thought it'd be simple, but it ended up with a whole lot of stuff (most of which I completely copied from someone else):
func hitSound(value: Float) {
let audioPlayerNode = AVAudioPlayerNode()
audioPlayerNode.stop()
engine.stop() // This is an AVAudioEngine defined previously
engine.reset()
engine.attach(audioPlayerNode)
let changeAudioUnitTime = AVAudioUnitTimePitch()
changeAudioUnitTime.pitch = value
engine.attach(changeAudioUnitTime)
engine.connect(audioPlayerNode, to: changeAudioUnitTime, format: nil)
engine.connect(changeAudioUnitTime, to: engine.outputNode, format: nil)
audioPlayerNode.scheduleFile(file, at: nil, completionHandler: nil) // File is an AVAudioFile defined previously
try? engine.start()
audioPlayerNode.play()
}
Since this code seems to stop playing any sounds currently being played in order to play the new sound, is there a way I can alter this behaviour so it doesn't stop playing anything? I tried removing the engine.stop and engine.reset bits, but this just crashes the app. Also, this code is incredibly slow when called frequently. Is there something I could do to speed it up? This hit sound is needed very frequently.
You're resetting the engine every time you play a sound! And you're creating extra player nodes - it's actually much simpler than that if you only want one instance of the pitch shifted sound playing at once:
// instance variables
let engine = AVAudioEngine()
let audioPlayerNode = AVAudioPlayerNode()
let changeAudioUnitTime = AVAudioUnitTimePitch()
call setupAudioEngine() once:
func setupAudioEngine() {
engine.attach(self.audioPlayerNode)
engine.attach(changeAudioUnitTime)
engine.connect(audioPlayerNode, to: changeAudioUnitTime, format: nil)
engine.connect(changeAudioUnitTime, to: engine.outputNode, format: nil)
try? engine.start()
audioPlayerNode.play()
}
and call hitSound() as many times as you like:
func hitSound(value: Float) {
changeAudioUnitTime.pitch = value
audioPlayerNode.scheduleFile(file, at: nil, completionHandler: nil) // File is an AVAudioFile defined previously
}
p.s. pitch can be shifted two octaves up or down, for a range of 4 octaves, and lies in the numerical range of [-2400, 2400], having the unit "cents".
p.p.s AVAudioUnitTimePitch is very cool technology. We definitely didn't have anything like it when I was a kid.
UPDATE
If you want multi channel, you can easily set up multiple player and pitch nodes, however you must choose the number of channels before you start the engine. Here's how you'd do two (it's easy to extend to n instances, and you'll probably want to choose your own method of choosing which channel to interrupt when all are playing):
// instance variables
let engine = AVAudioEngine()
var nextPlayerIndex = 0
let audioPlayers = [AVAudioPlayerNode(), AVAudioPlayerNode()]
let pitchUnits = [AVAudioUnitTimePitch(), AVAudioUnitTimePitch()]
func setupAudioEngine() {
var i = 0
for playerNode in audioPlayers {
let pitchUnit = pitchUnits[i]
engine.attach(playerNode)
engine.attach(pitchUnit)
engine.connect(playerNode, to: pitchUnit, format: nil)
engine.connect(pitchUnit, to:engine.mainMixerNode, format: nil)
i += 1
}
try? engine.start()
for playerNode in audioPlayers {
playerNode.play()
}
}
func hitSound(value: Float) {
let playerNode = audioPlayers[nextPlayerIndex]
let pitchUnit = pitchUnits[nextPlayerIndex]
pitchUnit.pitch = value
// interrupt playing sound if you have to
if playerNode.isPlaying {
playerNode.stop()
playerNode.play()
}
playerNode.scheduleFile(file, at: nil, completionHandler: nil) // File is an AVAudioFile defined previously
nextPlayerIndex = (nextPlayerIndex + 1) % audioPlayers.count
}

swift AVFoundation pitched audio memory issue

Hi still new to swift and programming in general.
I have this function that plays a piece of audio at a specified pitch. It gets called by a NStimer so plays once a second. (function contained in a SoundPlayer class and then NStimer setup and used in viewController)
func playPitchedAudio(audioFile: AVAudioFile, pitch: Float){
audioEngine.stop()
audioEngine.reset()
let audioPlayerNode = AVAudioPlayerNode()
let changePitchEffect = AVAudioUnitTimePitch()
changePitchEffect.pitch = pitch
audioEngine.attachNode(audioPlayerNode)
audioEngine.attachNode(changePitchEffect)
audioEngine.connect(audioPlayerNode, to: changePitchEffect, format: nil)
audioEngine.connect(changePitchEffect, to: audioEngine.outputNode, format: nil)
audioPlayerNode.scheduleFile(audioFile, atTime: nil, completionHandler: nil)
do {
try audioEngine.start()
} catch {
print("error")
}
audioPlayerNode.play()
}
It runs fine and works but adds a few mb to memory every time its called and never regains the space. Did some research on memory leaks but couldn't find anything that helps my my specific scenario so hoping someone can point me in the right direction.
I assumed it was something to do with creating a new node and TimePitch everytime its called so moved them into the class that contains the function but got a "libc++abi.dylib: terminating with uncaught exception of type NSException" error when the sound attempted to play for the second time.
any help much appreciated, Thanks!
extra stuff.
// defined in class to be used by function
var pitchedAudioPlayer = AVAudioPlayerNode()
var audioEngine = AVAudioEngine()
//Timer Start
self.timer.invalidate()
self.timer = NSTimer.scheduledTimerWithTimeInterval(tempo, target: self, selector: #selector(ViewController.timeTriggerPointer), userInfo: nil, repeats: true)
//Timer calls... (along with some other unrelated stuff)
func timeTriggerPointer() {
soundPlayer.playPitchedAudio(pitchFilePath, pitch: -1000.0)
}
Solution -
import AVFoundation
class SoundPlayer {
var pitchedAudioPlayer = AVAudioPlayerNode()
var audioEngine = AVAudioEngine()
let audioPlayerNode = AVAudioPlayerNode()
let changePitchEffect = AVAudioUnitTimePitch()
init() {
audioEngine.attachNode(audioPlayerNode)
audioEngine.attachNode(changePitchEffect)
audioEngine.connect(audioPlayerNode, to: changePitchEffect, format: nil)
audioEngine.connect(changePitchEffect, to: audioEngine.outputNode, format: nil)
}
func playPitchedAudio(audioFile: AVAudioFile, pitch: Float){
audioPlayerNode.stop()
changePitchEffect.pitch = pitch
audioPlayerNode.scheduleFile(audioFile, atTime: nil, completionHandler: nil)
do {
try audioEngine.start()
} catch {
print("error")
}
audioPlayerNode.play()
}
}
A "leak" is a highly technical thing: a piece of unreferenced memory which can never be released (because it is unreferenced). I doubt that you have a leak. You just have more and more objects, that's all.
You are repeating this code over and over (once each time the timer fires):
let audioPlayerNode = AVAudioPlayerNode()
let changePitchEffect = AVAudioUnitTimePitch()
audioEngine.attachNode(audioPlayerNode)
audioEngine.attachNode(changePitchEffect)
The audio engine itself, however, remains in place (it's declared outside the function, as a property of your view controller). So you're adding two more nodes to this same audio engine every time the timer fires. Nodes take up memory, so your memory keeps rising. No surprise here. If you don't want that to happen, don't do that.
I assumed it was something to do with creating a new node and TimePitch everytime its called
There you go. So you already solved your own problem.
Think for a moment about what you're trying to do. You have an audio engine with nodes. The only thing that might to change on each run is the pitch of the AVAudioUnitTimePitch node and the file to be played. So create the whole audio engine and nodes beforehand, and leave it in place. Keep a reference to the AVAudioUnitTimePitch as a property. In the timer function, just change that pitch value!
let theTimePitchNode = AVAudioUnitTimePitch()
// and you have also added that node to the engine in your setup
func playPitchedAudio(audioFile: AVAudioFile, pitch: Float){
audioPlayerNode.stop()
theTimePitchNode.pitch = pitch
audioPlayerNode.scheduleFile(audioFile, atTime: nil, completionHandler: nil)
do {
try audioEngine.start()
} catch {
print("error")
}
audioPlayerNode.play()
}