Unable to get AVAudioPlayer to stop playing - ios

I have a class defined as Music.swift coded as follows:
import Foundation
import AVFoundation
class Music {
var isPlaying: Bool = false
public var backgrndSound = AVAudioPlayer()
func isMusicPlaying() -> Bool {
isPlaying = UserDefaults.standard.bool(forKey: "isPlaying")
return isPlaying
}
func StartPlaying() {
let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do {
self.backgrndSound = try AVAudioPlayer(contentsOf: url)
self.backgrndSound.numberOfLoops = -1
self.backgrndSound.play()
UserDefaults.standard.setValue(true, forKey: "isPlaying")
} catch {
// couldn't load file :(
}
}
func StopPlaying() {
self.backgrndSound.pause()
self.backgrndSound.stop()
}
}
On first load of the app, the music is automatically started with a call to StartPlaying(). That works just fine. Afterwards I have a settings menu that has a switch for the music play :
import UIKit
import AVFoundation
class SettingsViewController: UIViewController {
#IBOutlet weak var swMusic: UISwitch!
var myMusic = Music()
override func viewDidLoad() {
super.viewDidLoad()
swMusic.isOn = UserDefaults.standard.bool(forKey: "isPlaying")
}
#IBAction func musicSwitch(_ sender: Any) {
if swMusic.isOn == true {
// turn on music
myMusic.StartPlaying()
} else {
myMusic.StopPlaying()
}
}
}
When I tap the switch it does fire StopPlaying() but the music in the background continues to play despite the tap.
I am not sure why that is, unless the AV object isn't accessible from the original creation and therefore can't stop it properly; but so far I have been unable to figure that out either.
Any help or suggestions would be greatly appreciated.

By instantiating a new instance of the Music class in SettingsViewController you're effectively creating a new AVAudioPlayer instance that knows nothing about the one already instantiated.
Consider this code, which contains static properties and class methods:
import Foundation
import AVFoundation
class Music
{
public static var backgrndSound: AVAudioPlayer?
// AVAudioPlayer already has an isPlaying property
class func isMusicPlaying() -> Bool
{
return backgrndSound?.isPlaying ?? false
}
class func StartPlaying()
{
let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do
{
backgrndSound = try AVAudioPlayer(contentsOf: url)
backgrndSound?.numberOfLoops = -1
backgrndSound?.play()
}
catch
{
// couldn't load file :(
}
}
class func StopPlaying()
{
backgrndSound?.pause()
backgrndSound?.stop()
}
}
then access this using:
Music.isMusicPlaying()
Music.startPlaying()
Music.stopPlaying()
i.e. you'd not do var myMusic = Music()
This way there will always be a single instance of the AVAudioPlayer, Music.backgrndSound
This sample code changes backgrndSound to an optional ... you're effectively creating an unused AVAudioPlayer instance that is discarded as soon as you startPlaying.
It also removes the unnecessary isPlaying property, as AVAudioPlayer already has a property for this purpose.

Related

audioPlayerDidFinishPlaying func issues

I am trying to make my app so that when a user touches a UIImageView A certain sound will play. However, while that sound is playing, I want the UIImageView.isUserInteractionEnabled to be assigned to false. Once the sound is done playing I want the UIImageView.isUserInteractionEnabled to be assigned to true. When ever I run the following code I get an error in the card class, even if I force unwrap. Below is the class that contains the image view I want to disable.
class SecondViewController: UIViewController , UIGestureRecognizerDelegate {
#IBOutlet weak var imgPhoto: UIImageView!
func imageTapped(tapGestureRecognizer: UITapGestureRecognizer)
{
imgPhoto.isUserInteractionEnabled = false
itemList[imageIndex].playSound()
}
}
This is the class where the playSound func is located.
import Foundation; import UIKit; import AVFoundation
class Card: NSObject
{
var player: AVAudioPlayer?
var svc = SecondViewController()
var image: UIImage
var soundUrl: String
init(image: UIImage, soundUrl: String, isActive:Bool = true) {
self.image = image
self.soundUrl = soundUrl
}
func playSound()
{
guard let url = Bundle.main.url(forResource: self.soundUrl, withExtension: "m4a") else { return }
do
{
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url)
player?.delegate = self
guard let player = player else { return }
player.prepareToPlay()
player.play()
audioPlayerDidFinishPlaying(player)
print("play")
} catch let error {
print(error.localizedDescription)
}
}
}
extension Card: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer){
svc.imgPhoto.isUserInteractionEnabled = true
}
}
this is the error I get
The problem is this line:
var svc = SecondViewController()
That svc is not your SecondViewController, the existing one in the interface with an imgPhoto. Instead, you are creating a new, separate, and above all blank view controller with an empty view, so its imgPhoto is nil and you crash when you refer to it.
What you want to do is use the "delegate" pattern to hold a reference to the real SecondViewController so you can talk to it.

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.

Change of AVPlayer current item (item url) and start playback

I have a UITableView with a posts that contain URLs to a mp3 files. With a tap on a button in a UITableViewCell I plan to fetch a singleton instance of AVPlayer. How to assign new URL to a AVPlayer and start playback? AVPlayer's currentItem is only a { get } property.
This is what I have now.
import Foundation
import AVFoundation
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static func shared() -> AVPlayer{
return AVPlayer()
}
func playItem(at itemURL: URL) {
if StreamMusicPlayer.shared().isPlaying {
StreamMusicPlayer.shared().pause()
}
//Change url of AVPlayerItem
//And and assign it to shared() instance then begin playback
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
This might be what you want. It's not perfect.
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static var shared = AVPlayer()
static func playItem(at itemURL: URL) {
StreamMusicPlayer.shared = AVPlayer(url: itemURL)
StreamMusicPlayer.shared.play()
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
I hade to change the static function to a normal variable. Every time you access .shared() it would initialize and return a new instance.
Then you can do:
StreamMusicPlayer.playItem(at: URL(string: "http://antoon.io/e/mp3/")!)
// Delay so you can hear it changing
StreamMusicPlayer.playItem(at: URL(string: "http://antoon.io/e/mp3/2.mp3")!)

Swift: How to stop Background music when changing views

I'm trying to get the background music to stop playing when changing to another view.
I have a global variable to play the music
import Foundation
import AVFoundation
class MusicHelper {
static let sharedHelper = MusicHelper()
var audioPlayer: AVAudioPlayer?
func playBackgroundMusic() {
let aSound = NSURL(fileURLWithPath: Bundle.main.path(forResource: "The Walking Dead", ofType: "mp3")!)
do {
audioPlayer = try AVAudioPlayer(contentsOf:aSound as URL)
audioPlayer!.numberOfLoops = -1
audioPlayer!.prepareToPlay()
audioPlayer!.play()
} catch {
print("Cannot play the file")
}
}
}
And then I load the music into the view here
import UIKit
import SwiftGifOrigin
import AVFoundation
class ViewController: UIViewController {
var audioPlayer: AVAudioPlayer?
#IBOutlet weak var introGif: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Gif intro
self.introGif.image = UIImage.gifWithName("WalkingDeadIntro")
_ = try! Data(contentsOf: Bundle.main.url(forResource: "WalkingDeadIntro", withExtension: "gif")!)
//self.bottomImageView.image = UIImage.gif(data: imageData)
// Load background Music
MusicHelper.sharedHelper.playBackgroundMusic()
}
Just write below code to stop the player whenever view changes provided that audioPlayer is a global variable as you mentioned.
override func viewWillDisappear( ) {
audioPlayer.stop( )
}

Swift : Player audio file from another class file

First of all, I just want to let you know I'm a newbie in iOS programming ;)
I would like to know if the following scenario is possible or which way I need to update my code to make it work.
I would like to play an audio file located in the player class file within the ViewController file.
I added the file "test.wav" to the root of project.
My problem
When I play the sound by tapping a button, the program can't find the sound file. I have the following error message:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
Main swift file: ViewController.swift
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player1: Player!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func playAudio(sender: AnyObject) {
player1.playAudioFile()
}
}
Class file: Player.swift
import Foundation
import AVFoundation
class Player {
// Variables
var vc: ViewController!
var audioFile: AVAudioPlayer!
// Initializer
init (){
}
// Methods
func playAudioFile() {
if audioFile.playing {
audioFile.stop()
} else {
audioFile.play()
}
}
// Intialize ViewController
init (vc:ViewController!) {
// Set path for the attack sound
let audioSnd = NSBundle.mainBundle().pathForResource("test", ofType: "wav")
let audioFileURL = NSURL(fileURLWithPath: audioSnd!)
do {
try audioFile = AVAudioPlayer(contentsOfURL: audioFileURL, fileTypeHint: nil)
audioFile.prepareToPlay()
} catch let err as NSError {
print(err.debugDescription)
}
}
}
More information
My program has a class "Player", with subclasses such as "Human" and "Monster".
By default, my class player has some audio files for attacks, dying, etc.
Under some conditions, the "player" can become a human or a monster and get custom attack and dying sounds.
Thanks a lot for your help! ;)
Why are you initializing you player with a viewController ? You dont need that.
Change the Player class to:
import Foundation
import AVFoundation
class Player {
// Variables
var audioFile: AVAudioPlayer!
// Initializer
init() {
// Set path for the attack sound
let audioSnd = NSBundle.mainBundle().pathForResource("test", ofType: "wav")
let audioFileURL = NSURL(fileURLWithPath: audioSnd!)
do {
try audioFile = AVAudioPlayer(contentsOfURL: audioFileURL, fileTypeHint: nil)
audioFile.prepareToPlay()
} catch let err as NSError {
print(err.debugDescription)
}
}
// Methods
func playAudioFile() {
if audioFile.playing {
audioFile.stop()
} else {
audioFile.play()
}
}
}
And your ViewController.swift to:
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player1: Player!
override func viewDidLoad() {
super.viewDidLoad()
// Note, that you need to initialize the player1 variable
player1 = Player()
}
#IBAction func playAudio(sender: AnyObject) {
player1.playAudioFile()
}
}
Swift is functional programming, so it is way better to separate the audio operation and viewController into different file/class. Then they could be reused in any scenarios.
Audio part
import AVFoundation
class AudioPlayer {
// Singleton to keep audio playing anywhere
static let shared = AudioPlayer()
var player: AVAudioPlayer?
private init() {}
func play(url: URL) {
do {
try AVAudioSession.sharedInstance().setMode(.default)
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
player = try AVAudioPlayer(contentsOf: url)
guard let player = player else { return }
player.prepareToPlay()
player.play()
} catch {
print("error occurred")
}
}
}
ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func playAudio(sender: AnyObject) {
let audioSnd = NSBundle.mainBundle().pathForResource("test", ofType: "wav")
// don't use force unwrap
guard let audioFileURL = NSURL(fileURLWithPath: audioSnd) else { return }
// call the shared instance here
AudioPlayer.shared.play(url: url)
}
}

Resources