Playing multiple audio files using AVAudioPlayer - ios

I created the 3 UIButtons on the Main StoryBoard.
When I press "Button1" or "Button2" each plays an audio file ( "player 1" , "player 2".) and while the audio is playing the UIButton is set to selected.
I would like "Button3" to implement multiple functions in a row.
That means when I press "Button 3", "player 3" should be played first, after that "player 1" and after that "player 2".
However, when I press "Button3", "player 1" and "player 2" are played at same time as there is no delay for the previous to finish.
I found out setting the delegate of the AVAudioPlayer or using AVQueuePlayer would solve the problem, but I find it difficult to make changes.
fileprivate var player1:AVAudioPlayer?
fileprivate var player2:AVAudioPlayer?
fileprivate var player3:AVAudioPlayer?
#IBAction func pushButton1(sender: UIButton) {
audioPlayer1(url: url1)
}
#IBAction func pushButton2(sender: UIButton) {
audioPlayer2(url: url2)
}
#IBAction func pushButton3(_ sender: UIButton) {
audioPlayer3(url: url1, url2: url2, url3: url3)
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if (player === player1) {
yourButton1.isSelected = false
} else if (player === player2) {
yourButton2.isSelected = false
} else if (player === player3) {
//"player 1" and "player 2" are played at same time
yourButton3.isSelected = false
player1!.play()
yourButton1.isSelected = true
player2!.play()
yourButton2.isSelected = false
}
}
func audioPlayer1(url: URL) {
do {
try player1 = AVAudioPlayer(contentsOf:url)
player1!.play()
yourButton1.isSelected = true
player1!.delegate = self
} catch {
print(error)
}
}
func audioPlayer2(url: URL) {
do {
try player2 = AVAudioPlayer(contentsOf:url)
player2!.play()
yourButton2.isSelected = true
player2!.delegate = self
} catch {
print(error)
}
}
func audioPlayer3(url: URL, url2: URL, url3: URL) {
do {
try player3 = AVAudioPlayer(contentsOf:url3)
try player1 = AVAudioPlayer(contentsOf: url1)
try player2 = AVAudioPlayer(contentsOf: url2)
player3!.play()
yourButton3.isSelected = true
player3!.delegate = self
player1!.delegate = self
} catch {
print(error)
}
}
}
Changed code
var queue = AVQueuePlayer()
var items = [AVPlayerItem]()
override func viewDidLoad() {
super.viewDidLoad()
player1?.delegate = self
player2?.delegate = self
player3?.delegate = self
let asset1 = AVPlayerItem(url: url1)
let asset2 = AVPlayerItem(url: url2)
let asset3 = AVPlayerItem(url: url3)
let asset4 = AVPlayerItem(url: url4)
items = [asset1, asset2, asset3, asset4]
queue = AVQueuePlayer(items: items)
for item in queue.items() {
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: item)
}
}
#IBAction func pushButton3(_ sender: UIButton) {
//audioPlayer3(url: url1, url2: url2)
sender.isSelected = true
queue.play()
}
func playerItemDidReachEnd(notification: NSNotification) {
if notification.object as? AVPlayerItem == items[0] {
yourButton3.isSelected = false
yourButton1.isSelected = true
}
if notification.object as? AVPlayerItem == items[1] {
yourButton1.isSelected = false
yourButton2.isSelected = true
}
if notification.object as? AVPlayerItem == items[2] {
yourButton2.isSelected = false
yourButton4.isSelected = true
//yourButton1.isSelected = true
}
if notification.object as? AVPlayerItem == items[3] {
yourButton4.isSelected = false
print("item 3")
}

EDIT: This is the method for AVAudioPlayer
So basically:
1: Add only one player, remove the rest of the players.
2: Create an NSMutableArray with your objects that you want to play.
3: Depending on what button gets pressed, you can re-arrange the NSMutableArray objects to set the correct order as you wish. (Simply recreate the array if you want to make it easy) And start playing the firstObject (url) in the array adding it accordingly to your player.
4: When the player finished playing, you can start playing the next object adding it to your player from the NSMutableArray the same way as done above.
Following above steps will avoid having multiple players playing simultaniously, together with other benefits (like avoid loading multiple URLs at the same time etc)
You may need some variable to check the currentPlayingObject to determine which one is next, maybe an int, and check that you don't try to load a new item from the array if it is the last item playing, to avoid crash on accessing objectAtIndex that does not exist.

Related

Problems resuming recording in the background using AVAudioRecorder in Swift

To put it in context, I'm developing an app that is recording a lot of time while in background or in the app. I'm facing two problems using AVAudioRecorder to record in my app:
My recording resumes fine when, for example, I play an audio in WhatsApp or a video in Instagram. But when I play a song in Apple Music or a play a voice note, it doesn't resume the recording again.
If I'm in a Phone Call and I start the recording, my app crashes and outputs the following:
(Error de OSStatus 561017449.)
I think also need to handle when the input/output device changes so my app doesn't crash.
The code that I implemented for the class where I'm recording (and also playing the sound I use recorder so I can try it and see if it works) is this:
class RecordViewController: UIViewController, AVAudioPlayerDelegate , AVAudioRecorderDelegate {
#IBOutlet weak var principalLabel: UILabel!
#IBOutlet weak var loginLabel: UILabel!
#IBOutlet weak var recordBTN: UIButton!
#IBOutlet weak var playBTN: UIButton!
var audioRecorder : AVAudioRecorder!
var audioPlayer : AVAudioPlayer!
var isAudioRecordingGranted: Bool!
var isRecording = false
var isPlaying = false
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
check_record_permission()
checkActivateMicrophone()
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: audioRecorder)
let tapRegister: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.goToLogin))
loginLabel.addGestureRecognizer(tapRegister)
loginLabel.isUserInteractionEnabled = true
}
#objc func goToLogin() {
self.performSegue(withIdentifier: "goToLogin", sender: self)
}
#objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeInt = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeInt) else {
return
}
switch type {
case .began:
if isRecording {
print("OOOOOOO")
audioRecorder.pause()
}
break
case .ended:
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
audioRecorder.record()
print("AAAAAAA")
} else {
print("III")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.audioRecorder.record()
}
}
default: ()
}
}
func check_record_permission()
{
switch AVAudioSession.sharedInstance().recordPermission {
case AVAudioSessionRecordPermission.granted:
isAudioRecordingGranted = true
break
case AVAudioSessionRecordPermission.denied:
isAudioRecordingGranted = false
break
case AVAudioSessionRecordPermission.undetermined:
AVAudioSession.sharedInstance().requestRecordPermission({ (allowed) in
if allowed {
self.isAudioRecordingGranted = true
} else {
self.isAudioRecordingGranted = false
}
})
break
default:
break
}
}
func checkActivateMicrophone() {
if !isAudioRecordingGranted {
playBTN.isEnabled = false
playBTN.alpha = 0.5
recordBTN.isEnabled = false
recordBTN.alpha = 0.5
principalLabel.text = "Activa el micrófono desde ajustes"
} else {
playBTN.isEnabled = true
playBTN.alpha = 1
recordBTN.isEnabled = true
recordBTN.alpha = 1
principalLabel.text = "¡Prueba las grabaciones!"
}
}
func getDocumentsDirectory() -> URL
{
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
func getFileUrl() -> URL
{
let filename = "myRecording.m4a"
let filePath = getDocumentsDirectory().appendingPathComponent(filename)
return filePath
}
func setup_recorder()
{
if isAudioRecordingGranted
{
let session = AVAudioSession.sharedInstance()
do
{
try session.setCategory(AVAudioSession.Category.playAndRecord, options: .defaultToSpeaker)
try session.setActive(true)
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 44100,
AVNumberOfChannelsKey: 2,
AVEncoderAudioQualityKey:AVAudioQuality.high.rawValue
]
audioRecorder = try AVAudioRecorder(url: getFileUrl(), settings: settings)
audioRecorder.delegate = self
audioRecorder.isMeteringEnabled = true
audioRecorder.prepareToRecord()
}
catch let error {
print(error.localizedDescription)
}
}
else
{
print("AAAAA")
}
}
#IBAction func recordAct(_ sender: Any) {
if(isRecording) {
finishAudioRecording(success: true)
recordBTN.setTitle("Record", for: .normal)
playBTN.isEnabled = true
isRecording = false
}
else
{
setup_recorder()
audioRecorder.record()//aaaaaa
recordBTN.setTitle("Stop", for: .normal)
playBTN.isEnabled = false
isRecording = true
}
}
func finishAudioRecording(success: Bool)
{
if success
{
audioRecorder.stop()
audioRecorder = nil
print("recorded successfully.")
}
else
{
print("Recording failed.")
}
}
func prepare_play()
{
do
{
audioPlayer = try AVAudioPlayer(contentsOf: getFileUrl())
audioPlayer.delegate = self
audioPlayer.prepareToPlay()
}
catch{
print("Error")
}
}
#IBAction func playAct(_ sender: Any) {
if(isPlaying)
{
audioPlayer.stop()
recordBTN.isEnabled = true
playBTN.setTitle("Play", for: .normal)
isPlaying = false
}
else
{
if FileManager.default.fileExists(atPath: getFileUrl().path)
{
recordBTN.isEnabled = false
playBTN.setTitle("pause", for: .normal)
prepare_play()
audioPlayer.play()
isPlaying = true
}
else
{
print("Audio file is missing.")
}
}
}
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool)
{
if !flag
{
print("UUU")
finishAudioRecording(success: false)
}
playBTN.isEnabled = true
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool)
{
recordBTN.isEnabled = true
}
}
I implemented the "Background Mode" of Audio, AirPlay and Picture in Picture

How to add new items to AVQueuePlayer currently playing with old items

Adding New Items To AVQueuePlayer Doesn't Play
I have scenarios where I'm streaming HLS audios from server. Initially I make an Array of AVPlayerItem. And initialise the AVQueuePlayer, And on button clicks it plays initial items very fine. But down the road when half of my items finishes playing I am trying to add more items at the end of list which doesn't play.
class ViewController: UIViewController, AVAudioPlayerDelegate{
let baseUrl = "********"
var player: AVQueuePlayer?
var items: [AVPlayerItem] = [];
let controller = AVPlayerViewController()
var counter = 0;
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(animationDidFinish(_:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player?.items())
}
override func viewDidAppear(_ animated: Bool) {
queueMaker()
}
#IBAction func btnTapped(_ sender: UIButton) {
let player = AVQueuePlayer(items: self.items)
let playerLayer = AVPlayerLayer(player: player)
self.view.layer.addSublayer(playerLayer)
player.play()
}
#objc func animationDidFinish(_ notification: NSNotification) {
print("HLS : AVQueuePlayer Item Finished Playing")
counter += 1
if ((items.count) - counter) < 4 {
print("HLS : Adding More Items")
addMoreItemsToQueue()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func queueMaker() {
let listOfAyahs: [String] = ["1/1.m3u8","2/2.m3u8","3/3.m3u8","4/4.m3u8","5/5.m3u8","6/6.m3u8","7/7.m3u8"]
for item in listOfAyahs {
let stringUrl = "\(self.baseUrl)\(item)"
let url = URL(string: stringUrl)
let item = AVPlayerItem(url: url!)
items.append(item)
}
}
func addMoreItemsToQueue() {
let listOfAyahs: [String] = ["8/8.m3u8","9/9.m3u8","10/10.m3u8","11/11.m3u8","12/12.m3u8","13/13.m3u8","14/14.m3u8"]
for item in listOfAyahs {
let stringUrl = "\(self.baseUrl)\(item)"
let url = URL(string: stringUrl)
let item = AVPlayerItem(url: url!)
items.append(item)
}
}
}
You are initialising a AVQuePlayer with some items which is the base copy for the AVQuePlayer and later you are appending new AVPlayerItems to your items array, about which the AVQuePlayer is not aware of. So, update the following to function and it should work.
#IBAction func btnTapped(_ sender: UIButton) {
// UPDATE THE PLAYER's SCOPE TO GLOBAL
player = AVQueuePlayer(items: self.items)
let playerLayer = AVPlayerLayer(player: player)
self.view.layer.addSublayer(playerLayer)
player?.play()
}
And add replace insert items to the queue like this
func addMoreItemsToQueue() {
let listOfAyahs: [String] = ["8/8.m3u8","9/9.m3u8","10/10.m3u8","11/11.m3u8","12/12.m3u8","13/13.m3u8","14/14.m3u8"]
var lastItem = self.player?.items().last
for item in listOfAyahs {
let stringUrl = "\(self.baseUrl)\(item)"
let url = URL(string: stringUrl)
let item = AVPlayerItem(url: url!)
self.player?.insert(item, after: lastItem)
lastItem = item
}
}
Try and share the results.

How do I play many audio files one by one

I created the 4 UIButtons on the Main StoryBoard.
I would like "Button4" to implement the other buttons function in a row. That means when I press button 4, player 1 should be played first, after that player 2 and after that player 3.
However, when I press "Button4", "Button2" and "Button3" are played at same time.
fileprivate var player1:AVAudioPlayer?
fileprivate var player2:AVAudioPlayer?
fileprivate var player3:AVAudioPlayer?
let url1 = Bundle.main.bundleURL.appendingPathComponent("music1.mp3")
let url2 = Bundle.main.bundleURL.appendingPathComponent("music2.mp3")
let url3 = Bundle.main.bundleURL.appendingPathComponent("music3.mp3")
#IBAction func pushButton1(sender: UIButton) {
Player(url: url1)
}
#IBAction func pushButton2(sender: UIButton) {
Player1(url: url2)
}
#IBAction func pushButton3(_ sender: UIButton) {
Player2(url: url1, url2: url2, url3: url3)
}
//"yourButton2" and "yourButton3" is played at same time in this code at player2
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if (player === player1) {
yourButton.isSelected = false
} else if (player === player2) {
yourButton2.isSelected = false
} else if (player === player3) {
yourButton.isSelected = false
player2!.play()
yourButton2.isSelected = true
player2!.play()
yourButton3.isSelected = true
player1!.play()
}
}
func Player(url: URL) {
do {
try player1 = AVAudioPlayer(contentsOf:url)
player1!.play()
yourButton.isSelected = true
player1!.delegate = self
} catch {
print(error)
}
}
func Player1(url: URL) {
do {
try player2 = AVAudioPlayer(contentsOf:url)
player2!.play()
yourButton2.isSelected = true
player2!.delegate = self
} catch {
print(error)
}
}
func Player2(url: URL, url2: URL, url3: URL) {
do {
try player3 = AVAudioPlayer(contentsOf:url)
try player2 = AVAudioPlayer(contentsOf: url2)
try player1 = AVAudioPlayer(contentsOf: url3)
player3!.play()
yourButton.isSelected = true
player3!.delegate = self
player2!.delegate = self
player1!.delegate = self
} catch {
print(error)
}
}
Rather than using an AVAudioPlayer, how about an AVQueuePlayer?
Here's a quick example:
var files = ["file1", "file2", "file3"]
var player: AVQueuePlayer = {
var pathArray = [String]()
files.forEach { resource in
if let path = Bundle.main.path(forResource: resource, ofType: "mp3") {
pathArray.append(path)
}
}
var urlArray = [URL]()
pathArray.forEach { path in
urlArray.append(URL(fileURLWithPath: path))
}
var playerItems = [AVPlayerItem]()
urlArray.forEach { url in
playerItems.append(AVPlayerItem(url: url))
}
let player = AVQueuePlayer(items: playerItems)
player.actionAtItemEnd = AVPlayerActionAtItemEnd.advance
return player
}()
and in your button's action:
#IBAction func buttonTapped(_ sender: UIButton) {
files = ["file2", "file3", "file1"]
player.play()
}
Admittedly, this is pretty quick and dirty, but something kind of like this because we're passing the files array into the player. It shouldn't be too difficult to find more optimization for this code.
EDIT: realized I wasn't passing in an array of AVPlayerItems, so updated.
Edit....
Each of the buttons, 1, 2, 3 etc...will work but "push button 2" is a bit of a mix...
Anyway, reexplain?

Playing multiple audio files in a row

I set the 4 UIButtons on the Main StoryBoard.
I would like "Button4" to implement the method of the other three Buttons in a row. (At first, plays an audio file and set selected "Button1" then "Button2" then "Button3")
However "Button2" and "Button3" are played at same time.
fileprivate var player1:AVAudioPlayer?
fileprivate var player2:AVAudioPlayer?
fileprivate var player3:AVAudioPlayer?
let url1 = Bundle.main.bundleURL.appendingPathComponent("music1.mp3")
let url2 = Bundle.main.bundleURL.appendingPathComponent("music2.mp3")
let url3 = Bundle.main.bundleURL.appendingPathComponent("music3.mp3")
#IBAction func pushButton1(sender: UIButton) {
Player(url: url1)
}
#IBAction func pushButton2(sender: UIButton) {
Player1(url: url2)
}
#IBAction func pushButton3(_ sender: UIButton) {
Player2(url: url1, url2: url2, url3: url3)
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if (player === player1) {
yourButton.isSelected = false
} else if (player === player2) {
yourButton2.isSelected = false
} else if (player === player3) {
yourButton.isSelected = false
player2!.play()
yourButton2.isSelected = true
player2!.play()
yourButton3.isSelected = true
player1!.play()
}
}
func Player(url: URL) {
do {
try player1 = AVAudioPlayer(contentsOf:url)
player1!.play()
yourButton.isSelected = true
player1!.delegate = self
} catch {
print(error)
}
}
func Player1(url: URL) {
do {
try player2 = AVAudioPlayer(contentsOf:url)
player2!.play()
yourButton2.isSelected = true
player2!.delegate = self
} catch {
print(error)
}
}
func Player2(url: URL, url2: URL, url3: URL) {
do {
try player3 = AVAudioPlayer(contentsOf:url)
try player2 = AVAudioPlayer(contentsOf: url2)
try player1 = AVAudioPlayer(contentsOf: url3)
player3!.play()
yourButton.isSelected = true
player3!.delegate = self
player2!.delegate = self
player1!.delegate = self
} catch {
print(error)
}
}
I believe that when you press button 4, player 1 should be played first, after that player 2 and after that player 3. For this task, the below code should work.
let url1 = Bundle.main.bundleURL.appendingPathComponent("music1.mp3")
let url2 = Bundle.main.bundleURL.appendingPathComponent("music2.mp3")
let url3 = Bundle.main.bundleURL.appendingPathComponent("music3.mp3")
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if (player === player1) {
yourButton.isSelected = false
yourButton2.isSelected = true
do {
//initialise player 2
player2 = try AVAudioPlayer(contentsOf: url2)
player2!.delegate = self
}catch {
}
player2!.play()
} else if (player === player2) {
yourButton3.isSelected = true
yourButton2.isSelected = false
do {
//initialise player 3
player3 = try AVAudioPlayer(contentsOf: url3)
player3!.delegate = self
}catch {
}
player3!.play()
} else if (player === player3) {
yourButton3.isSelected = false
}
}

How can I make my iOS app continuously loop through a playlists of sounds?

I'm trying to make an app that plays a series of chimes related to the content of an array, whenever all the chimes are played the app should start playing them again from the beginning of the array.
This is my code:
var counter = 0
var song = ["1","2","3","4"]
func music() {
while counter < song.count {
var audioPath = NSBundle.mainBundle().pathForResource("\(song[counter])", ofType: "wav")!
var error : NSError? = nil
player = AVAudioPlayer(contentsOfURL: NSURL(string: audioPath), error: &error)
if error == nil {
player.delegate = self
player.prepareToPlay()
player.play()
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool) {
if flag {
counter++
}
}
if ((counter + 1) == song.count) {
counter = 0
}
}
}
How can I achieve this?
You don't need to have a loop here. The logic is the following: you play a music file, when this file is over you increment the counter and play the next file
var counter = 0
var song = ["1","2","3","4"]
func music()
{
var audioPath = NSBundle.mainBundle().pathForResource("\(song[counter])", ofType: "wav")!
var error : NSError? = nil
player = AVAudioPlayer(contentsOfURL: NSURL(string: audioPath), error: &error)
if error == nil {
player.delegate = self
player.prepareToPlay()
player.play()
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool)
{
player.delegate = nil
if flag {
counter++
}
if ((counter + 1) == song.count) {
counter = 0
}
music()
}
You can do this easily using an AVQueuePlayer…
let song = ["1","2","3","4"]
func music() {
let items = song.compactMap {
Bundle.main.url(forResource: "\($0)", withExtension: "wav")
}.map {
AVPlayerItem(url: $0)
}
let player = AVQueuePlayer(items: items)
player.delegate = self
player.play()
}

Resources