iOS Swift AVAudioPlayer how to reset currentTime to zero when finished playing - ios

My code is supposed to play a C Major scale wave file on a button press. Hitting a separate stop button will stop playback and reset the currentTime to zero.
#IBAction func onPlayButtonClick(sender: UIButton)
{
play.enabled = false
let path = NSBundle.mainBundle().pathForResource("cmajor", ofType: "wav")!
let url = NSURL(fileURLWithPath: path)
do
{
scalePlayer = try AVAudioPlayer(contentsOfURL: url)
scalePlayer.prepareToPlay()
scalePlayer.enableRate = true
scalePlayer.rate = 0.75
scalePlayer.play()
}
catch
{
}
}
It works as intended, but how do I set the currentTime to zero when the file is finished playing? I couldn't find anything on the developer docs.

You have to set a delegate:
scalePlayer.delegate = self
Then you implement this callback
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
//set the current time here
scalePlayer.currentTime = 0
}
Update
Here is an example how you can implement this (based on your code):
class MyViewController: UIViewController, AVAudioPlayerDelegate {
#IBOutlet weak var play: UIButton!
private var scalePlayer: AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
let path = NSBundle.mainBundle().pathForResource("cmajor", ofType: "wav")!
let url = NSURL(fileURLWithPath: path)
scalePlayer = try? AVAudioPlayer(contentsOfURL: url)
scalePlayer?.delegate = self
}
#IBAction func onPlayButtonClick(sender: UIButton) {
play.enabled = false
scalePlayer?.prepareToPlay()
scalePlayer?.enableRate = true
scalePlayer?.rate = 0.75
scalePlayer?.play()
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
scalePlayer?.currentTime = 0
}
}

Related

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.

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.

AVPlayer play with update label cause an issue

I have an issue with AVPlayer play and same time updates label.
I have timer that updates label with progress of player, but that stuck my video at same position...check below image..After button click timer starts and cause an issue..
Code:
func playVideo() {
let path = NSBundle.mainBundle().pathForResource("videoname", ofType:"mp4")
let fileURL = NSURL(fileURLWithPath: path!)
moviePlayer = AVPlayer(URL: fileURL)
// Play the video
moviePlayer!.play()
}
#IBAction func progresss(sender: UIButton) {
self.meterTimer = NSTimer.scheduledTimerWithTimeInterval(0.1,
target:self,
selector:"updateAudioMeter:",
userInfo:nil,
repeats:true)
}
func updateAudioMeter(timer:NSTimer) {
lbltime.text = "\(CMTimeGetSeconds(moviePlayer!.currentItem.currentTime()))"
}
Sample project link
Source code
Note:please add video in bundle if you are use sample project.I just delete video because of file size
Question
How to avoid this? and why this happens because when I used MPMovieController it works great.
From your sample code the problem is below function:
override func viewDidLayoutSubviews() {
let layer = AVPlayerLayer(player: moviePlayer)
layer.frame = self.viewPlayer.frame
self.viewPlayer.layer.addSublayer(layer)
}
This will resize your layer view every time when your label update so just remove and add that code into playVideo method and your method will be:
func playVideo() {
let path = NSBundle.mainBundle().pathForResource("video", ofType: "MOV")
let url = NSURL.fileURLWithPath(path!)
moviePlayer = AVPlayer(URL: url)
moviePlayer!.allowsExternalPlayback = false
var introPlayerLayer = AVPlayerLayer(player: moviePlayer)
viewPlayer.layer.addSublayer(introPlayerLayer)
introPlayerLayer.frame = viewPlayer.bounds
moviePlayer!.play()
}
And call this method in viewDidAppear instead of viewDidLoad method and your complete code will be:
import UIKit
import MediaPlayer
import AVFoundation
class ViewController: UIViewController {
var moviePlayer : AVPlayer?
var meterTimer : NSTimer!
#IBOutlet weak var viewPlayer: UIView!
#IBOutlet weak var lbltime: UILabel!
override func viewDidAppear(animated: Bool) {
playVideo()
}
func playVideo() {
let path = NSBundle.mainBundle().pathForResource("video", ofType: "MOV")
let url = NSURL.fileURLWithPath(path!)
moviePlayer = AVPlayer(URL: url)
moviePlayer!.allowsExternalPlayback = false
var introPlayerLayer = AVPlayerLayer(player: moviePlayer)
viewPlayer.layer.addSublayer(introPlayerLayer)
introPlayerLayer.frame = viewPlayer.bounds
moviePlayer!.play()
}
#IBAction func progresss(sender: UIButton) {
moviePlayer?.seekToTime(kCMTimeZero)
moviePlayer?.addPeriodicTimeObserverForInterval(CMTimeMakeWithSeconds(1, 1), queue: nil, usingBlock: {
(CMTime) -> Void in
self.updateProgressBar()
})
}
func updateProgressBar(){
var timeNow = Int(self.moviePlayer!.currentTime().value) / Int(self.moviePlayer!.currentTime().timescale)
var currentMins = timeNow / 60
var currentSec = timeNow % 60
var duration: String = "\(currentMins):\(currentSec)"
self.lbltime.text = duration
}
}
I have written a sample code for Objective-C, you can convert same in swift for your project accordingly.
// Implement following code in AVPlayer observer method for `AVPlayerStatusReadyToPlay` state
[avPlayer addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC)
queue:NULL
usingBlock:
^(CMTime time)
{
// update label value here, it keeps invoking periodically internally.
CMTime playerDuration = [self playerItemDuration];
}];
// Method to get Player current time
- (CMTime)playerItemDuration
{
AVPlayerItem *thePlayerItem = [avPlayer currentItem];
if (thePlayerItem.status == AVPlayerItemStatusReadyToPlay)
{
return([avPlayerItem duration]);
}
return(kCMTimeInvalid);
}
if you simply want to update it then invoke playerItemDuration to get current time, but make it asynchronous process.

AVFoundation: prevent background music playing twice when returning to first screen

At the moment I have a home screen which plays background music using AVFoundation. If you click play the music stops (which is what I want).
If you move to the instructions screen the music continues (which I want), however when you click to return to home screen the background music continues (which I want), but a new track starts over the top.
In my mind what I ideally need is an if statement which prevents the music from restarting when I return to the home screen (if it is already playing). I have scoured the internet but I can't find any suggestions that will work.
This is the what I am currently working with,
class firstPageViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
backgroundMusic = self.setupAudioPlayerWithFile("background", type:"mp3")
backgroundMusic.volume = 0.3
backgroundMusic.numberOfLoops = -1
backgroundMusic.play()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
var backgroundMusic = AVAudioPlayer()
func setupAudioPlayerWithFile(file:NSString, type:NSString) -> AVAudioPlayer {
//1
var path = NSBundle.mainBundle().pathForResource(file as String, ofType: type as String)
var url = NSURL.fileURLWithPath(path!)
//2
var error: NSError?
//3
var audioPlayer:AVAudioPlayer?
audioPlayer = AVAudioPlayer(contentsOfURL: url, error: &error)
//4
return audioPlayer!
}
#IBAction func startGame(sender: UIButton) {
backgroundMusic.stop()
}
#IBAction func instructionsButton(sender: UIButton) {
}
var isMusicPlaying = 0
class firstPageViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
backgroundMusic = self.setupAudioPlayerWithFile("background", type:"mp3")
if isMusicPlaying == 0 {
backgroundMusic.volume = 0.3
backgroundMusic.numberOfLoops = -1
backgroundMusic.play()
}
if backgroundMusic.playing == true {
isMusicPlaying = 1
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
var backgroundMusic = AVAudioPlayer()
func setupAudioPlayerWithFile(file:NSString, type:NSString) -> AVAudioPlayer {
//1
var path = NSBundle.mainBundle().pathForResource(file as String, ofType: type as String)
var url = NSURL.fileURLWithPath(path!)
//2
var error: NSError?
//3
var audioPlayer:AVAudioPlayer?
audioPlayer = AVAudioPlayer(contentsOfURL: url, error: &error)
//4
return audioPlayer!
}

Make a playlist (start next song) in swift

I have created a sound player in swift with AVFoundation. I am trying to start the next song in array when the playing song is finished. I was trying to implement this code
if (audioPlayer.currentTime >= audioPlayer.duration){
var recentSong = songPlaylist[selectedSongNumber + 1]
audioPlayer = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath:
NSBundle.mainBundle().pathForResource(recentSong, ofType: "mp3")!), error: nil)
audioPlayer.play()
}
but I am not being able to implement this code (I do not know where to implement it).Here is my complete code
import UIKit
import AVFoundation
import AVKit
public var audioPlayer = AVPlayer()
public var selectedSongNumber = Int()
public var songPlaylist:[String] = ["song1", "song2"]
public var recentSong = "song1"
let playImage = UIImage(named: "Play.png") as UIImage!
let pauseImage = UIImage(named: "Pause.png") as UIImage!
class FirstViewController: UIViewController {
#IBOutlet weak var musicSlider: UISlider!
#IBOutlet weak var PlayPause: UIButton!
var audioPlayer = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath:
NSBundle.mainBundle().pathForResource(recentSong, ofType: "mp3")!), error: nil)
override func viewDidLoad() {
super.viewDidLoad()
musicSlider.maximumValue = Float(audioPlayer.duration)
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("updateMusicSlider"), userInfo: nil, repeats: true)
if (audioPlayer.currentTime >= audioPlayer.duration){
var recentSong = songPlaylist[selectedSongNumber + 1]
audioPlayer = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath:
NSBundle.mainBundle().pathForResource(recentSong, ofType: "mp3")!), error: nil)
audioPlayer.play()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func PlayPauseButton(sender: AnyObject) {
if (audioPlayer.playing == false){
audioPlayer.play()
PlayPause.setImage(pauseImage, forState: .Normal)
}else{
audioPlayer.pause()
PlayPause.setImage(playImage, forState: .Normal)
}
}
#IBAction func StopButton(sender: AnyObject) {
audioPlayer.stop()
audioPlayer.currentTime = 0
PlayPause.setImage(playImage, forState: .Normal)
}
#IBAction func musicSliderAction(sender: UISlider) {
audioPlayer.stop()
audioPlayer.currentTime = NSTimeInterval(musicSlider.value)
audioPlayer.play()
}
func updateMusicSlider(){
musicSlider.value = Float(audioPlayer.currentTime)
}
}
I am updating my code with something different:
import UIKit
import AVFoundation
class ViewController: UIViewController, AVAudioPlayerDelegate {
var counter = 0
var song = ["1","2","3"]
var player = AVAudioPlayer()
#IBOutlet weak var musicSlider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
musicSlider.value = 0.0
}
func updateMusicSlider(){
musicSlider.value = Float(player.currentTime)
}
#IBAction func playSong(sender: AnyObject) {
music()
}
#IBAction func sliderAction(sender: AnyObject) {
player.stop()
player.currentTime = NSTimeInterval(musicSlider.value)
player.play()
}
func music(){
var audioPath = NSBundle.mainBundle().pathForResource("\(song[counter])", ofType: "mp3")!
var error : NSError? = nil
player = AVAudioPlayer(contentsOfURL: NSURL(string: audioPath), error: &error)
musicSlider.maximumValue = Float(player.duration)
var timer = NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: Selector("updateMusicSlider"), userInfo: nil, repeats: true)
player.delegate = self
if error == nil {
player.delegate = self
player.prepareToPlay()
player.play()
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool)
{
println("Called")
if flag {
counter++
}
if ((counter + 1) == song.count) {
counter = 0
}
music()
}
}
You can do it this way.
Hope It will help and HERE is sample project for more Info.
You need to implement AVAudioPlayerDelegate Protocol's method:
optional func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer!, successfully flag: Bool)
Documentation link
Play your next music item here.
But I will not recommend, since AVAudioPlayer can only play one item at a time. You need to instantiate again with another music item after completion. I will suggest you to use AVQueuePlayer. Detaled answer has been given here. Hope it helps!
I created a sound player in swift with AVFoundation. I used progressView to time the song and then when it hits 0.98743 it will update to the next song automatically this is the Github link: https://github.com/ryan-wlr/MusicPlayerIOS
func updateProgressView() {
if (progressView.progress > a.advanced(by: 0.98743)) {
audioPlayerDidFinishPlayeing()
}
if audioPlayer.isPlaying {
let progress = Float(audioPlayer.currentTime/audioPlayer.duration)
progressView.setProgress(progress, animated: true)
}
}

Resources