2

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
Asperi
  • 228,894
  • 20
  • 464
  • 690
phiredrop
  • 182
  • 1
  • 11

1 Answers1

0

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

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • 1
    This fires when the Scheduling is done (which happens within milliseconds of the call), not when the media has finished playing. – jho Jun 12 '23 at 16:53
  • I was missing the `completionCallbackType` -- I've edited to reflect this. – jnpdx Jun 25 '23 at 19:35
  • Even with the type set to PlayedBack, I'm still not getting callback method triggered when the media has finished playing, only after scheduling is done. – jho Jun 26 '23 at 06:11
  • @jho I just tested with iOS 16.1 and it works as expected, with the callback is triggered after it finishes playing. I'm not able to reproduce the issue you're having, which suggests there may be other issues with your setup. – jnpdx Jun 26 '23 at 07:20