phiredrop
phiredrop

Reputation: 382

How to monitor for AVAudioPlayerNode finished playing

Oh hey! I've been stuck trying to figure out how to have my AVAudioPlayerNode() automatically stop playback when it reaches the end of the file in my SwiftUI project. As you can see below, my audio files that don't go through the AVAudioEngine but rather the AVAudioPlayer have an audioPlayerDidFinishPlaying function and that works great. However for the audio files that go through the engine using the play function don't have anything like this, and I can't figure out how to get it to behave similarly.

I've tried building something like

    func audioPlayerDidFinishPlaying2(_ player: AVAudioPlayerNode, successfully flag: Bool) {
        if flag {
            isPlaying = false
            print("Playback Engine Stopped")
        }
    }

But this doesn't appear to do anything.

Stopping the engine's playback manually by calling stopPlayback2() works just fine.

I'm calling the engine player using

do {
    try self.audioPlayer.play(self.audioURL)
  }
    catch let error as NSError {
      print(error.localizedDescription)
  }

I've looked at other SO posts [here][1] and [here][2] but neither solutions are working for me. I would really appreciate your input if you have any suggestions! Thanks!!

AudioPlayer.swift

class AudioPlayer: NSObject, ObservableObject, AVAudioPlayerDelegate {
    
    let objectWillChange = PassthroughSubject<AudioPlayer, Never>()
    var isPlaying = false {
        didSet {
            objectWillChange.send(self)
        }
    }

    
    var audioPlayer: AVAudioPlayer!
    
    func startPlayback (audio: URL) {

        let playbackSession = AVAudioSession.sharedInstance()
        
        do {
            try playbackSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
        } catch {
            print("Playing over the device's speakers failed")
        }
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: audio)
            audioPlayer.delegate = self
            audioPlayer.play()
            isPlaying = true
        } catch {
            print("Playback failed.")
        }
    }
    
    func stopPlayback() {
        
        audioPlayer.stop()
        isPlaying = false
        
    }
    
    func stopPlayback2() {
        engine.stop()
        isPlaying = false
    }
    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        if flag {
            isPlaying = false
            print("Playback Stopped")
        }
    }

    let engine = AVAudioEngine()
    let speedControl = AVAudioUnitVarispeed()
    let pitchControl = AVAudioUnitTimePitch()
    
    func play(_ url: URL) throws {
        let file = try! AVAudioFile(forReading: url)

        let avPlayer = AVAudioPlayerNode()

        engine.attach(avPlayer)
        engine.attach(pitchControl)
        engine.attach(speedControl)

        engine.connect(avPlayer, to: speedControl, format: nil)
        engine.connect(speedControl, to: pitchControl, format: nil)
        engine.connect(pitchControl, to: engine.mainMixerNode, format: nil)

        avPlayer.scheduleFile(file, at: nil)

        isPlaying = true
        try engine.start()
        avPlayer.play()
    }
    ```


  [1]: https://stackoverflow.com/questions/34238432/avaudioengine-avaudioplayernode-didfinish-method-like-avaudioplayer
  [2]: https://stackoverflow.com/questions/59080708/calling-stop-on-avaudioplayernode-after-finished-playing-causes-crash

Upvotes: 3

Views: 2259

Answers (1)

jnpdx
jnpdx

Reputation: 52575

Instead of using avPlayer.scheduleFile(file, at: nil), use the form of the method with a completion handler:

avPlayer.scheduleFile(file, at: nil, completionCallbackType: .dataPlayedBack) { _ in
    //call your completion function here
    print("Done playing")
}

Documentation: https://developer.apple.com/documentation/avfaudio/avaudioplayernodecompletioncallbacktype/dataplayedback

Upvotes: 3

Related Questions