`audioPlayerDidFinishPlaying` Not Being Called [duplicate] - ios

This question already has an answer here:
AVAudioPlayer.play() does not play sound
(1 answer)
Closed 3 years ago.
Having an issue where audioPlayerDidFinishPlaying isn't getting called...
I've looked at some other SO threads:
Swift: AudioPlayerDidFinished will not be called
AVAudioPlayerDelegate doesn't call the method
...but still having the same issue.
I've set my ViewController to conform to AVAudioPlayerDelegate:
class ViewController: UIViewController, AVAudioPlayerDelegate {
var audioEngine = AVAudioEngine()
...
}
And create the player here; setting the delegate right after creating the player itself:
func startAudioEngine(audioFileURL: URL) {
var player: AVAudioPlayer?
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: audioFileURL, fileTypeHint: AVFileType.mp3.rawValue)
guard let player = player else { return }
player.delegate = self
player.prepareToPlay()
player.play()
} catch let error {
print(error.localizedDescription)
}
}

Your problem is twofold:
var player: AVAudioPlayer? is declared local to func startAudioEngine(), so as soon as the function exists, you lose reference to player.
You are setting the delegate on the optionally bound player
Change to this (or something like this):
class ViewController: UIViewController, AVAudioPlayerDelegate {
var audioEngine = AVAudioEngine()
var player: AVAudioPlayer?
}
Then after you assign player, declare your delegate self.player?.delegate = self. Make sure to include self. so that the delegate is set on the class property and not the optional binding;

Related

Swift need closure for audioplayer

I am building an app with several view controllers. I need to control music from all of them so I have created a dedicated music class which I use to setup / play / stop / pause.
I have recorded audio questions and answers and I need to be able to play the question and then the answer mp3 files.
So I believe that here a few way to accomplish this delegate and protocols, using the func audioPlayerDidFinishPlaying and using closures. From what I can understand closures are the best option for what I am trying to achieve.
My starting point in the MakeMusic Class is:
class MakeMusicClass : NSObject, AVAudioPlayerDelegate {
static let shared = MakeMusicClass()
var audioPlayer = AVAudioPlayer()
override init() { }
func setup(Selection: String) {
do {
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: Selection, ofType: "mp3")!))
audioPlayer.prepareToPlay()
audioPlayer.delegate=self
} catch {
print (error)
}
}
func play() {
audioPlayer.play()
}
My calling file is:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
MakeMusicClass.shared.setup(Selection: "Question11")
MakeMusicClass.shared.play()
MakeMusicClass.shared.setup(Selection: "Answer11")
MakeMusicClass.shared.play()
To get this working I understand that I need to add a closure to the called class:
func play() {
var closure = { in
audioPlayer.play()
}
}
And I need to update where I need to call the function something like:
override func viewDidLoad() {
super.viewDidLoad()
MakeMusicClass.shared.setup(Selection: "Question11")
MakeMusicClass.shared.play() {
MakeMusicClass.shared.setup(Selection: "Answer11")
MakeMusicClass.shared.play()
}
I have spent ages trying to get my head around this, but I am struggling. My code here is clearly not working as there is something fundamental that I am missing. I have tried passing void and parameters, but I don't understand what parameters should be passed. The closest I have come is using the audioPlayerDidFinishPlaying within the makemusic class trigger the next audio file, but I don't know that this is ideal.
I think your best bet will be in your MakeMusicClass taking an Array as an initialiser e.g. a question and and answer, and then use AVAudioPlayerDelegate to trigger the next file, which I think is what you are trying to get at (and very close ^________*).
For example:
class AudioLooper: NSObject, AVAudioPlayerDelegate {
var debugView = true
var audioLoopPlayer: AVAudioPlayer!
var audioFileIndex: Int = 0
var audioFileArray: [String] = []
//-------------------------------------------------
//MARK: Audio Player Initialisation & Functionality
//-------------------------------------------------
/// Function To Initialise The Audio Loop Player
///
/// - Parameter audioFiles: [String] - The Array Of Audio Files To Play
func initAudioPlayerWith(audioFiles: [String]) {
audioFileArray = audioFiles
if debugView { print("Audio Files To Play In Sequence == \(audioFileArray)") }
}
/// Function To Play An Array Of Audio Files
///
/// - Parameter index: (Int) - The Current Index Of The Audio Sequence
func playAudioLoopAt(index: Int) {
let currentAudioFile = "\(audioFileArray[audioFileIndex])"
if let audioFilePath = Bundle.main.path(forResource: currentAudioFile, ofType: "mp3"){
do {
let audioFileUrl = NSURL.fileURL(withPath: audioFilePath)
// Set An Instance Of AVAudioSession Which Allows Playback Even When Device Is Muted
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
audioLoopPlayer = try AVAudioPlayer(contentsOf: audioFileUrl)
audioLoopPlayer.prepareToPlay()
audioLoopPlayer.delegate = self
audioLoopPlayer.play()
if debugView { print("Playing \(currentAudioFile) ") }
audioFileIndex += 1
} catch {
print("Error Playing Audio")
}
} else {
print("Error Finding File: \(currentAudioFile)")
}
}
/// Function To Continue The Audio Sequence
#objc func continueAudioLoop(){
playAudioLoopAt(index: audioFileIndex)
}
/// Function To Stop The Audio Looop Player
func stopAudioPlayer() {
if audioLoopPlayer != nil {
audioFileIndex=0
audioLoopPlayer.stop()
audioLoopPlayer = nil
}
}
//-----------------------------
//MARK: AVAudio Player Delegate
//-----------------------------
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if audioFileIndex < audioFileArray.count {
self.perform(#selector(continueAudioLoop), with: self, afterDelay: 0.5)
}else{
audioFileIndex=0
}
}
}
In my example you simply init like so:
audioLooper = AudioLooper()
audioLooper?.initAudioPlayerWith(audioFiles: ["question", "answer"])
audioLooper?.playAudioLoopAt(index: 0)
Where audioLooper is declared like so : var audioLooper:AudioLooper?
Clearly my example is not a Singleton, but it should give you an idea of how you can adjust your MakeMusicClass to make it fit...
You can also add a Delegate Method like so to inform the ViewController the audio has finished or perform some other task like update the next question etc e.g:
#objc protocol AudioLooperDelegate {
#objc optional func update()
}
Then in your ViewController:
var delegate: AudioLooperDelegate?
And in the AudioLooper Class you can then add the delegate method where needed e.g:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if audioFileIndex < audioFileArray.count {
self.perform(#selector(continueAudioLoop), with: self, afterDelay: 0.5)
}else{
audioFileIndex=0
delegate?.updateUI!()
}
}
}

Swift in iOS with AVAudioPlayer: Singleton not working the way I hoped it would

I am using a standard Master-Detail project, listing songs in the Master and playing them in Detail. Each song has up to four parts playing simultaneously, with independent volume control, so I have four AVAudioPlayer objects in Detail, each with a slider with an IBOutlet and an IBAction to implement the volume control.
The problem is that when you click on a song (in the list on the Master), the previous song doesn't stop. Both songs play, although the volume controls now only control the most recent song. This can go on for any number of songs.
I want to get rid of the currently playing song when a new song is clicked on.
I thought I might be able to accomplish this by creating the players inside a Singleton, in such a way that there would only ever be four players. Since, according to the documentation, each player can only play one sound file at a time, I hoped the previous sound file would stop playing when the new one started. But it's not working. The same behavior described above is still happening: multiple songs can play simultaneously, with the volume controls only controlling the most recent song. Any suggestions would be greatly appreciated.
Here is the code for the Singleton:
import Foundation
import AVFoundation
class FourPlayers {
static let audioPlayers = [one, two, three, four]
static let one = AVAudioPlayer()
static let two = AVAudioPlayer()
static let three = AVAudioPlayer()
static let four = AVAudioPlayer()
private init() {} //This prevents others from using the default '()' initializer for this class.
}
(Initially, I had just made audioPlayers static, but when that didn't work, I decided to make each individual player static as well.)
Then, in the DetailViewController:
var audioPlayers = FourPlayers.audioPlayers
Here's code for one of the four volume controls:
#IBOutlet weak var vol1: UISlider!
#IBAction func volAdjust1(sender: AnyObject) {
audioPlayers[0].volume = vol1.value
}
Playing a song looks like this (the audioFiles array is populated when the song info is passed in from the Master):
var audioFiles = []
func playAudioFiles() {
var i = 0
for _ in audioFiles {
audioPlayers[i].play()
i+=1
}
}
This is the code telling the players which file to play:
func prepareAudioFiles () {
var i = 0;
for audioFile in audioFiles {
let s = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(audioFile as? String, ofType: "mp3")!)
do {
audioPlayers[i] = try AVAudioPlayer(contentsOfURL:s)
} catch {
print("Error getting the audio file")
}
audioPlayers[i].prepareToPlay()
self.audioPlayers[i].delegate = self
}
}
It appears the the volume control is only adjusting the first player (index 0)
#IBAction func volAdjust1(sender: AnyObject) {
audioPlayers[0].volume = vol1.value
}
maybe you could add a variable to keep track of the currently playing index
var currentlyPlayingIndex = 0
#IBAction func volAdjust1(sender: AnyObject) {
audioPlayers[currentlyPlayingIndex].volume = vol1.value
}
And update it when needed
When you play the audio you could use the same variable
func playAudioFiles() {
var i = 0
for _ in audioFiles {
if i == currentlyPlayingIndex {
audioPlayers[i].play()
} else {
audioPlayers[i].stop()
}
i+=1
}
}
The problem is the following line:
var audioPlayers = FourPlayers.audioPlayers
You are actually creating a new copy of the FourPlayers.audioPlayers array into the audioPlayers variable. So you are not reusing the AVAudioPlayers but creating independent ones for each song.
See the "Assignment and Copy Behavior for Strings, Arrays, and Dictionaries" section in the Swift Programming Documentation, Classes and Structures
You could use the static variable directly in your code instead of making a copy, for example:
#IBOutlet weak var vol1: UISlider!
#IBAction func volAdjust1(sender: AnyObject) {
FourPlayers.audioPlayers[0].volume = vol1.value
}
and:
var audioFiles = []
func playAudioFiles() {
var i = 0
for _ in audioFiles {
FourPlayers.audioPlayers[i].play()
i+=1
}
}
and update the rest of your code accordingly.

AVPlayer Swift: How do I hide controls and disable landscape view?

since this is my first post, just a few words about me: Usually I design stuff (UI primarily) but I really wanted to take a leap into programming to understand you guys better. So I decided build a small app to get going.
So I've been trying to figure this out for hours now – this is my first app project ever so I apologise for my newbyness.
All I want to do is to hide the controls of AVPlayer and disable landscape view but I just can't figure out where to put showsPlaybackControls = false.
import UIKit
import AVKit
import AVFoundation
class ViewController: UIViewController {
private var firstAppear = true
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if firstAppear {
do {
try playVideo()
firstAppear = false
} catch AppError.InvalidResource(let name, let type) {
debugPrint("Could not find resource \(name).\(type)")
} catch {
debugPrint("Generic error")
}
}
}
private func playVideo() throws {
guard let path = NSBundle.mainBundle().pathForResource("video", ofType:"mp4") else {
throw AppError.InvalidResource("video", "m4v")
}
let player = AVPlayer(URL: NSURL(fileURLWithPath: path))
let playerController = AVPlayerViewController()
playerController.player = player
self.presentViewController(playerController, animated: false) {
player.play()
}
}
}
enum AppError : ErrorType {
case InvalidResource(String, String)
}
showsPlaybackControls is a property of AVPlayerViewController, so you can set it after you create it:
playerController.showsPlaybackControls = false
If you want to allow only landscape, you can subclass AVPlayerViewController, override supportedInterfaceOrientations and return only landscape, then use that class instead of AVPlayerViewController.
UPDATE
As mentioned in the comments, if you go to the documentation for AVPlayerViewController you'll find a warning that says:
Do not subclass AVPlayerViewController. Overriding this class’s methods is unsupported and results in undefined behavior.
They probably don't want you to override playback-related methods that could interfere with the behavior, and I would say that overriding only supportedInterfaceOrientations is safe.
However, if you want an alternative solution, you can create your own view controller class that overrides supportedInterfaceOrientations to return landscape only, and place the AVPlayerViewController inside that view controller.

Volume Slider in swift

I am trying to create a slider that controls volume of 4 audio files.
Here is my code for the audio:
var ButtonAudioURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("audio1", ofType: "mp3")!)
var firstplayer = AVAudioPlayer()
var ButtonAudioURL2 = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("audio2", ofType: "mp3")!)
var secondplayer = AVAudioPlayer()
var ButtonAudioURL3 = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("audio3", ofType: "mp3")!)
var thirdplayer = AVAudioPlayer()
var ButtonAudioURL4 = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("audio4", ofType: "mp3")!)
var forthplayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
firstplayer = AVAudioPlayer(contentsOfURL: ButtonAudioURL, error: nil)
secondplayer = AVAudioPlayer(contentsOfURL: ButtonAudioURL2, error: nil)
thirdplayer = AVAudioPlayer(contentsOfURL: ButtonAudioURL3, error: nil)
forthplayer = AVAudioPlayer(contentsOfURL: ButtonAudioURL4, error: nil)
I have the audio to play when each button is touched. But I just need to get a slider to control the volume of each audio file from each button.
How should i do this? I have looked at MPVolumeView class reference on developer.apple, but I am still confused and in the developer.apple reference for MPVolumeView, it talks about AirPlay, can I disable that?
I just need a slider to control the volume.
You're close!
Drag in a slider into your storyboard, and create an IB outlet and IB action for it.
In the IB action code, use firstPlayer.volume = sliderOutlet.value
Since both range from 0 to 1 as default, they are on the same scale and so can be used like this without scaling. You may need to declare the AVAudioPlayers more globally and have a way of seeing which button was pressed (e.g. a variable that stores the name of the button pressed). That way, you change the correct volume using 4 if statements and don't end up changing the volume for a player that has not been initialized yet.
Let me know if this works!
If you use Swift 2 Try with this:
#IBOutlet weak var volumen: UISlider!
var reproductor = AVAudioPlayer()
after:
#IBAction func sliderValueChanged(sender: UISlider) {
let selectedValue = Float(sender.value)
reproductor.volume = selectedValue
}
and try your slider volume during the playing sound. That is all. I've helped.
I am using Xcode 6. I am building a similar example and I dragged from the slider in the storyboard to my viewController and created an Outlet
#IBOutlet weak var volumeControl: UISlider!
after that I created an action
#IBAction func adjustVolume(sender: AnyObject) {
if audioPlayer != nil {
audioPlayer?.volume = volumeControl.value
}
}
and it is working for me. You can create 4 sliders and assign them to each instance of AudioPlayer you created.
In this link you can find an example of Playing an audio on iOS8 using AVAudioPlayer http://www.techotopia.com/index.php/Playing_Audio_on_iOS_8_using_AVAudioPlayer

How do I get music to persist as I navigate from view to view?

So, I have mastered the art of playing sounds/music using one view in iOS, but I am now trying to make a more robust app for an musical artist. Thus far, it involves segues from a "menu" ViewController to his Bio, Show Times, and "Listening Room" etc. Thus far, I created a an "AudioManager" class:
import UIKit
import AVFoundation
import MediaPlayer
class AudioManager: NSObject {
let defaltSong = ["Hell&BackCaf/01 Black Sheep", "caf"]
weak var delegate : PlayerDelegate?
var musicPlayer1 = AVAudioPlayer()
var trackNumber = 0
var musicAudioPath = NSBundle.mainBundle().pathForResource("Hell&BackCaf/01 Black Sheep", ofType: "caf")
var musicAudioPathURL = NSURL()
var error:NSError? = nil
var songList = [["Hell&BackCaf/01 Black Sheep", "caf"], ["Hell&BackCaf/02 Hell & Back", "caf"], ["Hell&BackCaf/03 Save Me", "caf"], ["Hell&BackCaf/04 Broken feat. Hillary Dodson", "caf"], ["Hell&BackCaf/05 Do Or Die", "caf"], ["Hell&BackCaf/06 Divided", "caf"]]
func ButtonPlay(song: NSString, type: NSString) {
error = nil
musicAudioPath = NSBundle.mainBundle().pathForResource(song as String, ofType: type as String)
musicAudioPathURL = NSURL(fileURLWithPath: self.musicAudioPath!)!
musicPlayer1 = AVAudioPlayer(contentsOfURL: musicAudioPathURL, error: &error)
musicPlayer1.prepareToPlay()
musicPlayer1.play()
}
func loadFirstSong() {
error = nil
musicAudioPath = NSBundle.mainBundle().pathForResource("Hell&BackCaf/01 Black Sheep", ofType: "caf")
musicAudioPathURL = NSURL(fileURLWithPath: self.musicAudioPath!)!
musicPlayer1 = AVAudioPlayer(contentsOfURL: musicAudioPathURL, error: &error)
if error == nil {
musicPlayer1.prepareToPlay()
} else {
println(error)
}
}
func advanceTrack(){
if trackNumber < songList.count - 1 {
self.trackNumber++
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
} else {
trackNumber = 0
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
}
}
func previousTrack(){
if trackNumber > 0 {
trackNumber--
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
} else {
trackNumber = songList.count - 1
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
}
}
func audioPlayerDidFinishPlaying(AVAudioPlayer!, successfully: Bool) {
self.delegate?.soundFinished(self)
println("song over")
}
}
I then used it in my MusicRoomViewController:
import UIKit
import AVFoundation
class MusicRoomViewController: UIViewController, AVAudioPlayerDelegate {
let audioManager = AudioManager()
#IBAction func pressedBackButton(sender: UIButton) {
audioManager.previousTrack()
}
#IBAction func pressedPlayButton(sender: UIButton) {
audioManager.musicPlayer1.play()
}
#IBAction func pressedForwardButton(sender: UIButton) {
audioManager.advanceTrack()
}
override func viewDidLoad() {
super.viewDidLoad()
audioManager.loadFirstSong()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewWillDisappear(animated: Bool) {
if self.isMovingFromParentViewController()
{
audioManager.musicPlayer1.play()
}
}
}
But now when I navigate to any other 'page' of the app, the music stops. After researching, I know that I am going about this all wrong, but I can't seem to figure out what I need to do to keep the audio playing until the app is closed or the user presses stop (which does not exist yet, I know). Any thoughts??
You're going to have to make your AudioManager class a singleton. What is happening, is the AudioManager instance you're creating is being deallocated once you navigate away from the view you created it in. A singleton object is created once it is first accessed, and will persist in memory until the application's lifecycle ends or it is explicitly deallocated. For more information on Swift singletons, check out this page. It's a useful pattern to learn.
Make the following modifications to AudioManager (taken from the above website):
private let _AudioManagerSharedInstance = AudioManager()
class AudioManager {
static let sharedInstance = AudioManager()
}
Access AudioManager by using AudioManager.sharedInstance, for example you can call AudioManager.sharedInstance.previousTrack().
You could create a singleton, or instantiate the AudioManager in your AppDelegate class and refer to that like so:
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
appDelegate.audioManger...

Resources