Search code examples
swiftaudiomemorypitch

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()
    }
}

Solution

  • 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()
    
    }