How to monitor for AVAudioPlayerNode finished playing - ios

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

Instead of using avPlayer.scheduleFile(file, at: nil), use the form of the method with a completion handler:
avPlayer.scheduleFile(file, at: nil) {
//call your completion function here
print("Done playing")
}

Related

Ok so I'm new to Swift. I've been trying to make an audio player and the code just gives me this error. What can I do?

import UIKit
import AVFoundation
class AVAudioPlayer : NSObject {
}
class ViewController: UIViewController {
#IBAction func keyPressed(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
func playSound() {
let player: AVAudioPlayer?
guard let sound = Bundle.main.url(forResource: "C", withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOfURL: sound, fileTypeHint: AVFileType.wav.rawValue)
guard let player = player else { return }
func play() -> Bool {}
} catch let error {
print(error.localizedDescription)
}
Xcode gives me an error in this line player = try AVAudioPlayer(contentsOfURL: sound, fileTypeHint: AVFileType.wav.rawValue) saying that Argument passed to call that takes no arguments
Replace '(contentsOfURL: sound, fileTypeHint: AVFileType.wav.rawValue)' with '' and when I try to fix it, it gives me this error: Cannot assign value of type 'AVAudioPlayer.Type' to type 'AVAudioPlayer'. Can somebody help me? PS sorry for bold, Stack wouldn't let me post because it had "too much code in it".
After the help from Sweeper, my code looks like this:
import AVFoundation
class ViewController: UIViewController {
var player: AVAudioPlayer?
#IBAction func keyPressed(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
func playSound() {
guard let sound = Bundle.main.url(forResource: "C", withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer
guard let player = player else { return }
func play() -> Bool {}
} catch let error {
print(error.localizedDescription)
}
As I was saying in the comments, now it gives me this error: Use of unresolved identifier 'player'
You need to provide init(contentsOf:) throws method to initialize the player. Here is the code you need:
class ViewController: UIViewController {
var player: AVAudioPlayer?
func playSound() {
guard let sound = Bundle.main.url(forResource: "C", withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: sound)
player?.play()
} catch let error {
print(error.localizedDescription)
}
}
}

How to track when song is finished AVAudioPlayer

I want to track when playing song is finished. I tried different solutions from the web but they could not solve my problem.
I implemented audioPlayerDidFinishPlaying method but it is not working.
How can I understand if playing song is finished?
I am playing songs with playSound function
playSound func:
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
audioPlayerDidFinishPlaying func:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is not working, not printing "finished"
}
How can I solve my problem? How to track when playing song is finished
EDIT: I am adding whole code.
//
// ViewController.swift
import UIKit
import SwiftVideoBackground
import AudioToolbox
import AVFoundation
class ViewController: UIViewController,AVAudioPlayerDelegate {
var player: AVAudioPlayer?
#IBOutlet weak var backgroundVideo: BackgroundVideo!
#IBOutlet weak var initialLabel: UILabel!
#IBOutlet weak var statementLabel: UILabel!
var mp3: [String] = ["turk_milleti_demokrattir","xyz"]
var fav: [String] = ["0","0"]
var name: [String] = ["Türk milleti demokrattır","xy"]
var toggleState = 1
#IBOutlet weak var playB: UIButton!
var counter = 0
var duration = 0.1
override func viewDidLoad() {
super.viewDidLoad()
player?.delegate = self
playB.setImage(UIImage(named: "playbtn.png"), for: .normal)
statementLabel.text = name[counter]
backgroundVideo.createBackgroundVideo(name: "abc", type: "mp4")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func likeButton(_ sender: Any) {
fav[counter] = "1"
print(fav[0...1])
}
#IBAction func playButton(_ sender: Any) {
let name = mp3[counter]
playSound(name: name)
let playBtn = sender as! UIButton
if toggleState == 1 {
player?.play()
toggleState = 2
playBtn.setImage(UIImage(named: "pausebtn.png"), for: .normal)
} else {
player?.pause()
toggleState = 1
playBtn.setImage(UIImage(named:"playbtn.png"),for: .normal)
}
}
#IBAction func nextButton(_ sender: Any) {
counter = counter + 1
if counter == mp3.count {
counter = 0
}
toggleState = 2
playB.setImage(UIImage(named: "pausebtn.png"), for: .normal)
playSound(name: mp3[counter])
statementLabel.text = name[counter]
}
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is not working, not printing "finished"
}
}
I solved my problem with help of Leo Dabus.
I changed my edited code. I moved player?.delegate = self
to playSound func. Finally, it is working.
playSound & audioPlayerDidFinishPlaying function:
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
player?.delegate = self
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is working now! printed "finished"!
}
Do not forget to add AVAudioPlayerDelegate to ViewController!
class ViewController: UIViewController,AVAudioPlayerDelegate {
You are not setting the player's delegate correctly.
In viewDidLoad, your player is going to be nil, so this line:
player?.delegate = self
Will do nothing (The question mark is optional chaining, so if player == nil, it does nothing.)
You need to set the delegate after loading the player.

Audio not playing in simulator

I have three buttons and I'm trying to get a sound to play with each button.
The sounds don't play on the simulator wondering what was going wrong in my code.
import UIKit
import AVFoundation
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func mmm_sound(sender: UIButton) {
playSound("mmm")
}
#IBAction func aww_sound(sender: UIButton) {
playSound("aww")
}
#IBAction func maniacal_sound(sender: UIButton) {
playSound("hah")
}
//sound function
func playSound(soundName: String)
{
let sound = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource(soundName, ofType: "wav")!)
do{
let audioPlayer = try AVAudioPlayer(contentsOfURL:sound)
audioPlayer.prepareToPlay()
audioPlayer.play()
}catch {
print("Error getting the audio file")
}
}
}
You should make AVAudioPlayer as global variable as local variable of 'AVAudioPlayer' get deallocate before it plays, your code can like this
//at top
var audioPlayer = AVAudioPlayer()
func playSound(soundName: String)
{
let sound = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource(soundName, ofType:"wav")!)
do{
audioPlayer = try AVAudioPlayer(contentsOfURL:sound)
audioPlayer.prepareToPlay()
audioPlayer.play()
}catch {
print("Error getting the audio file")
}
}

Conditional audio playing using AV Audio Player in Swift

I would like to be able to play audio on pressing a button a maximum of twice. After pressing the button twice it should no longer play audio even when pressed. I have this code currently, but it doesn't work and I am unsure where I am going wrong:
var soundFileURLRef: NSURL!
var audioPlayer = AVAudioPlayer?()
var audioCounter = 0
override func viewDidLoad() {
super.viewDidLoad()
// setup for audio
let playObjects = NSBundle.mainBundle().URLForResource("mathsinfo", withExtension: "mp3")
self.soundFileURLRef = playObjects
do {
audioPlayer = try AVAudioPlayer(contentsOfURL: soundFileURLRef)
} catch _ {
audioPlayer = nil
}
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
}
//function for counting times audio is played
func countAudio() {
if ((audioPlayer?.play()) != nil) {
++audioCounter
}
}
//MARK: Actions
//action for button playing the audio
#IBAction func playMathsQuestion(sender: AnyObject) {
countAudio()
if audioCounter < 2 {
audioPlayer?.play()
}
}
The countAudio function in your code have the side effect of playing the sound asynchronously since you call audioPlayer?.play(). The audioCounter variable is only incremented when audioPlayer is nil. Try this version
var soundFileURLRef: NSURL!
var audioPlayer: AVAudioPlayer?
var audioCounter = 0
override func viewDidLoad() {
super.viewDidLoad()
// setup for audio
let playObjects = NSBundle.mainBundle().URLForResource("mathsinfo", withExtension: "mp3")
self.soundFileURLRef = playObjects
audioPlayer = try? AVAudioPlayer(contentsOfURL: soundFileURLRef)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
}
//MARK: Actions
//action for button playing the audio
#IBAction func playMathsQuestion(sender: AnyObject) {
if (audioPlayer != nil) {
if (audioCounter < 2 && audioPlayer!.play()) {
++audioCounter
}
}
}
You can see that here we first check to ensure the audioPlayer is not nil. After this we only do the audioPlayer!.play() call when audioCounter < 2. The call to play() returns a boolean that is true when the audio is successfully queued for play (see the documentation), and in that case we increment audioCounter.
I also simplified the initialization of audioPlayer by using the try? version of try which returns nil when the callee throws.

AVAudioPlayer produces lag despite prepareToPlay() in Swift

Playing a very short sound (~0.5s) produces a hiccup (like a lag) in my SpriteKit iOS game programmed in Swift. In other questions, I read that I should prepareToPlay() the sound, which I did.
I even used a variable (soundReady) to check if the sound is prepared before playing it. I also re-prepare the sound whenever it is finished playing (audioPlayerDidFinishPlaying()). Here are the relevant parts of the code:
class GameScene: SKScene, AVAudioPlayerDelegate {
var splashSound = NSURL()
var audioPlayer = AVAudioPlayer()
var soundReady = false
override func didMoveToView(view: SKView) {
let path = NSBundle.mainBundle().pathForResource("plopSound", ofType: "m4a")
splashSound = NSURL(fileURLWithPath: path)
audioPlayer = AVAudioPlayer(contentsOfURL: splashSound, error: nil)
audioPlayer.delegate = self
soundReady = audioPlayer.prepareToPlay()
}
func playSound(){
if(soundReady){
audioPlayer.play()
soundReady = false
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool){
//Prepare to play after Sound finished playing
soundReady = audioPlayer.prepareToPlay()
}
}
I have no idea where I've gone wrong on this one. I feel like I have tried everything (including, but not limited to: only preparing once, preparing right after playing, not using a variable, but just prepareToPlay()).
Additional information:
The sound plays without delay.
How quickly the sound is played after the last finish does not seem to impact the lag.
I ran into this same problem and played the sound in the backgroundQueue.
This is a good example: https://stackoverflow.com/a/25070476/586204.
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
audioPlayer.play()
})
Just adding a Swift 3 version of the solution from #brilliantairic.
DispatchQueue.global().async {
audioPlayer.play()
}
When I called play multiple times it would cause bad access. I believe the player is being deallocated since this is not thread safe. I created a serial queue to alleviate this problem.
class SoundManager {
static let shared = SoundManager()
private init() {
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try? AVAudioSession.sharedInstance().setActive(true)
}
private let serialQueue = DispatchQueue(label: "SoundQueue", qos: .userInitiated)
private var player: AVAudioPlayer?
static func play(_ sound: Sound) {
shared.play(sound)
}
func play(_ sound: Sound) {
guard let url = Bundle.main.url(forResource: sound.fileName, withExtension: "mp3")
else { return }
do {
try serialQueue.sync {
self.player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
DispatchQueue.main.async {
self.player?.play()
}
}
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
}

Resources