Get AVAudioPlayer to play multiple sounds at a time

17,277

Solution 1

The reason the audio stops is because you only have one AVAudioPlayer set up, so when you ask the class to play another sound you are currently replacing the old instance with a new instance of AVAudioPlayer. You are overwriting it basically.

You can either create two instances of the GSAudio class, and then call playSound on each of them, or make the class a generic audio manager that uses a dictionary of audioPlayers.

I much prefer the latter option, as it allows for cleaner code and is also more efficient. You can check to see if you have already made a player for the sound before, rather than making a new player for example.

Anyways, I re-made your class for you so that it will play multiple sounds at once. It can also play the same sound over itself (it doesn't replace the previous instance of the sound) Hope it helps!

The class is a singleton, so to access the class use:

GSAudio.sharedInstance

for example, to play a sound you would call:

GSAudio.sharedInstance.playSound("AudioFileName")

and to play a number of sounds at once:

GSAudio.sharedInstance.playSounds("AudioFileName1", "AudioFileName2")

or you could load up the sounds in an array somewhere and call the playSounds function that accepts an array:

let sounds = ["AudioFileName1", "AudioFileName2"]
GSAudio.sharedInstance.playSounds(sounds)

I also added a playSounds function that allows you to delay each sound being played in a cascade kind of format. So:

 let soundFileNames = ["SoundFileName1", "SoundFileName2", "SoundFileName3"]
 GSAudio.sharedInstance.playSounds(soundFileNames, withDelay: 1.0)

would play sound2 a second after sound1, then sound3 would play a second after sound2 etc.

Here is the class:

class GSAudio: NSObject, AVAudioPlayerDelegate {

    static let sharedInstance = GSAudio()

    private override init() {}

    var players = [NSURL:AVAudioPlayer]()
    var duplicatePlayers = [AVAudioPlayer]()

    func playSound (soundFileName: String){

        let soundFileNameURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(soundFileName, ofType: "aif", inDirectory:"Sounds")!)

        if let player = players[soundFileNameURL] { //player for sound has been found

            if player.playing == false { //player is not in use, so use that one
                player.prepareToPlay()
                player.play()

            } else { // player is in use, create a new, duplicate, player and use that instead

                let duplicatePlayer = try! AVAudioPlayer(contentsOfURL: soundFileNameURL)
                //use 'try!' because we know the URL worked before.

                duplicatePlayer.delegate = self
                //assign delegate for duplicatePlayer so delegate can remove the duplicate once it's stopped playing

                duplicatePlayers.append(duplicatePlayer)
                //add duplicate to array so it doesn't get removed from memory before finishing

                duplicatePlayer.prepareToPlay()
                duplicatePlayer.play()

            }
        } else { //player has not been found, create a new player with the URL if possible
            do{
                let player = try AVAudioPlayer(contentsOfURL: soundFileNameURL)
                players[soundFileNameURL] = player
                player.prepareToPlay()
                player.play()
            } catch {
                print("Could not play sound file!")
            }
        }
    }


    func playSounds(soundFileNames: [String]){

        for soundFileName in soundFileNames {
            playSound(soundFileName)
        }
    }

    func playSounds(soundFileNames: String...){
        for soundFileName in soundFileNames {
            playSound(soundFileName)
        }
    }

    func playSounds(soundFileNames: [String], withDelay: Double) { //withDelay is in seconds
        for (index, soundFileName) in soundFileNames.enumerate() {
            let delay = withDelay*Double(index)
            let _ = NSTimer.scheduledTimerWithTimeInterval(delay, target: self, selector: #selector(playSoundNotification(_:)), userInfo: ["fileName":soundFileName], repeats: false)
        }
    }

     func playSoundNotification(notification: NSNotification) {
        if let soundFileName = notification.userInfo?["fileName"] as? String {
             playSound(soundFileName)
         }
     }

     func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
        duplicatePlayers.removeAtIndex(duplicatePlayers.indexOf(player)!)
        //Remove the duplicate player once it is done
    }

}

Solution 2

Here's a Swift 4 version of @Oliver Wilkinson code with some safechecks and improved code formatting:

import Foundation
import AVFoundation

class GSAudio: NSObject, AVAudioPlayerDelegate {

    static let sharedInstance = GSAudio()

    private override init() { }

    var players: [URL: AVAudioPlayer] = [:]
    var duplicatePlayers: [AVAudioPlayer] = []

    func playSound(soundFileName: String) {

        guard let bundle = Bundle.main.path(forResource: soundFileName, ofType: "aac") else { return }
        let soundFileNameURL = URL(fileURLWithPath: bundle)

        if let player = players[soundFileNameURL] { //player for sound has been found

            if !player.isPlaying { //player is not in use, so use that one
                player.prepareToPlay()
                player.play()
            } else { // player is in use, create a new, duplicate, player and use that instead

                do {
                    let duplicatePlayer = try AVAudioPlayer(contentsOf: soundFileNameURL)

                    duplicatePlayer.delegate = self
                    //assign delegate for duplicatePlayer so delegate can remove the duplicate once it's stopped playing

                    duplicatePlayers.append(duplicatePlayer)
                    //add duplicate to array so it doesn't get removed from memory before finishing

                    duplicatePlayer.prepareToPlay()
                    duplicatePlayer.play()
                } catch let error {
                    print(error.localizedDescription)
                }

            }
        } else { //player has not been found, create a new player with the URL if possible
            do {
                let player = try AVAudioPlayer(contentsOf: soundFileNameURL)
                players[soundFileNameURL] = player
                player.prepareToPlay()
                player.play()
            } catch let error {
                print(error.localizedDescription)
            }
        }
    }


    func playSounds(soundFileNames: [String]) {
        for soundFileName in soundFileNames {
            playSound(soundFileName: soundFileName)
        }
    }

    func playSounds(soundFileNames: String...) {
        for soundFileName in soundFileNames {
            playSound(soundFileName: soundFileName)
        }
    }

    func playSounds(soundFileNames: [String], withDelay: Double) { //withDelay is in seconds
        for (index, soundFileName) in soundFileNames.enumerated() {
            let delay = withDelay * Double(index)
            let _ = Timer.scheduledTimer(timeInterval: delay, target: self, selector: #selector(playSoundNotification(_:)), userInfo: ["fileName": soundFileName], repeats: false)
        }
    }

    @objc func playSoundNotification(_ notification: NSNotification) {
        if let soundFileName = notification.userInfo?["fileName"] as? String {
            playSound(soundFileName: soundFileName)
        }
    }

    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        if let index = duplicatePlayers.index(of: player) {
            duplicatePlayers.remove(at: index)
        }
    }

}

Solution 3

I have created a helper library that simplifies playing sounds in Swift. It creates multiple instances of AVAudioPlayer to allow playing the same sound multiple times concurrently. You can download it from Github or import with Cocoapods.

Here is the link: SwiftySound

The usage is as simple as it can be:

Sound.play(file: "sound.mp3")

Solution 4

All answers are posting pages of code; it doesn't need to be that complicated.

// Create a new player for the sound; it doesn't matter which sound file this is
                let soundPlayer = try AVAudioPlayer( contentsOf: url )
                soundPlayer.numberOfLoops = 0
                soundPlayer.volume = 1
                soundPlayer.play()
                soundPlayers.append( soundPlayer )

// In an timer based loop or other callback such as display link, prune out players that are done, thus deallocating them
        checkSfx: for player in soundPlayers {
            if player.isPlaying { continue } else {
                if let index = soundPlayers.index(of: player) {
                    soundPlayers.remove(at: index)
                    break checkSfx
                }
            }
        }

Solution 5

Swift 5+

Compiling some of the previous answers, improving code style and reusability

I usually avoid loose strings throughout my projects and use, instead, custom protocols for objects that will hold those string properties.

I prefer this to the enum approach simply because enumerations tend to couple your project together quite quickly. Everytime you add a new case you must edit the same file with the enumeration, breaking somewhat the Open-Closed principle from SOLID and increasing chances for error.

In this particular case, you could have a protocol that defines sounds:

protocol Sound {
    func getFileName() -> String
    func getFileExtension() -> String
    func getVolume() -> Float
    func isLoop() -> Bool
}

extension Sound {
    func getVolume() -> Float { 1 }
    func isLoop() -> Bool { false }
}

And when you need a new sound you can simply create a new structure or class that implements this protocol (It will even be suggested on autocomplete if your IDE, just like Xcode, supports it, giving you similar benefits to those of the enumeration... and it works way better in medium to large multi framework projects).

(Usually I leave volume and other configurations with default implementations as they are less frequently customized).

For instance, you could have a coin drop sound:

struct CoinDropSound: Sound {
    func getFileName() -> String { "coin_drop" }
    func getFileExtension() -> String { "wav" }
}

Then, you could use a singleton SoundManager that would take care of managing playing audio files

import AVFAudio

final class SoundManager: NSObject, AVAudioPlayerDelegate {
    static let shared = SoundManager()

    private var audioPlayers: [URL: AVAudioPlayer] = [:]
    private var duplicateAudioPlayers: [AVAudioPlayer] = []

    private override init() {}

    func play(sound: Sound) {
        let fileName = sound.getFileName()
        let fileExtension = sound.getFileExtension()

        guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension),
              let player = getAudioPlayer(for: url) else { return }

        player.volume = sound.getVolume()
        player.numberOfLoops = numberOfLoops
        player.prepareToPlay()
        player.play()
    }

    private func getAudioPlayer(for url: URL) -> AVAudioPlayer? {
        guard let player = audioPlayers[url] else {
            let player = try? AVAudioPlayer(contentsOf: url)
            audioPlayers[url] = player
            return  player
        }
        guard player.isPlaying else { return player }
        guard let duplicatePlayer = try? AVAudioPlayer(contentsOf: url) else { return nil }
        duplicatePlayer.delegate = self
        duplicateAudioPlayers.append(duplicatePlayer)
        return duplicatePlayer
    }

    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        duplicateAudioPlayers.removeAll { $0 == player }
    }
}

Here I created a helper getAudioPlayer to be able to return early from code execution and make use of the guard let.

Using guard let more often and preferring less nested code can, most of the time, highly improve readability.

To use this SoundManager from anywhere in your project, simply access its shared instance and pass an object that conforms to Sound.

For example, given the previous CoinDropSound:

SoundManager.shared.play(sound: CoinDropSound())

You could maybe omit the sound parameter as it may improve readability

class SoundManager {
    // ...
    func play(_ sound: Sound) {
        // ...
    }
    // ...
}

And then:

SoundManager.shared.play(CoinDropSound())
Share:
17,277

Related videos on Youtube

Kai
Author by

Kai

Updated on April 20, 2022

Comments

  • Kai
    Kai about 2 years

    I'm trying to get multiple sounds files to play on an AVAudioPlayer instance, however when one sound plays, the other stops. I can't get more than one sound to play at a time. Here is my code:

    import AVFoundation
    
    class GSAudio{
    
        static var instance: GSAudio!
    
        var soundFileNameURL: NSURL = NSURL()
        var soundFileName = ""
        var soundPlay = AVAudioPlayer()
    
        func playSound (soundFile: String){
    
            GSAudio.instance = self
    
            soundFileName = soundFile
            soundFileNameURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(soundFileName, ofType: "aif", inDirectory:"Sounds")!)
            do{
                try soundPlay = AVAudioPlayer(contentsOfURL: soundFileNameURL)
            } catch {
                print("Could not play sound file!")
            }
    
            soundPlay.prepareToPlay()
            soundPlay.play ()
        }
    }
    

    Can anyone please help me by telling me how to get more than one sound file to play at a time? Any help is much appreciated.

    Many thanks, Kai

    • Olivier Wilkinson
      Olivier Wilkinson almost 8 years
      Did you ever give my class a go?
    • Kai
      Kai almost 8 years
      @OlivierWilkinson I did try your class and it is good for if you want to start both sounds at the same time, but I want it so that when a second sound starts playing, I don't want it to suddenly stop the sound that's already playing. Thanks for your help
    • Olivier Wilkinson
      Olivier Wilkinson almost 8 years
      I'm not sure I understand the problem.
    • Olivier Wilkinson
      Olivier Wilkinson almost 8 years
      The sounds play simultaneously, they don't stop each other. And if you would like to call sounds separately you can call playSound() rather than playSounds(). Even with playSound() it doesn't stop the previous sound.
    • Olivier Wilkinson
      Olivier Wilkinson almost 8 years
      Ah I see what you mean. Yeah just call playSound() then. It will still work without stopping the previous sound.
    • Olivier Wilkinson
      Olivier Wilkinson almost 8 years
      Unless you are only using one audio file
    • Kai
      Kai almost 8 years
      @OlivierWilkinson Thanks, I'll check the code works when I get the chance later today
    • Olivier Wilkinson
      Olivier Wilkinson almost 8 years
      Did you manage to get it working?
    • Kai
      Kai almost 8 years
      Thanks, I've got multiple sounds to play, but say I wanted to get the same sound to play on top of each other (for example, when I press a button, I want it to make a sound, and if they push another button straight after, it uses the same sound, even though the other sound is still playing). How would I be able to do this? Many thanks
    • Olivier Wilkinson
      Olivier Wilkinson almost 8 years
      No problem, I'll edit my answer to include that scenario in the next hour or so :)
  • Kai
    Kai almost 8 years
    Thanks for the edit, but when I try to use this class I get: Command failed due to signal: Segmentation fault: 11. Do you know how to resolve this please? Many thanks
  • Olivier Wilkinson
    Olivier Wilkinson almost 8 years
    Yeah I just saw that myself, I mindlessly copied a function that conflicted with another one. Two secs
  • Olivier Wilkinson
    Olivier Wilkinson almost 8 years
    I edited the code to avoid the segmentation fault, see above. or just delete the playSounds(soundFileNames: String..., withDelay: Double) function, the one that uses an array is fine :)
  • Kai
    Kai almost 8 years
    I think we're getting closer, but when my code reaches the following line: let soundFileNameURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(soundFileName, ofType: "aiff")!), my app crashes with fatal error: unexpectedly found nil while unwrapping an Optional value. Any ideas? Thanks
  • Olivier Wilkinson
    Olivier Wilkinson almost 8 years
    Ah sorry that was just for me, put your line in there, I didn't have a sounds directory so changed that line for my code
  • Olivier Wilkinson
    Olivier Wilkinson almost 8 years
    just changed it to the right line in my answer as well
  • Kai
    Kai almost 8 years
    It's finally worked! Thank you ever so much for your help! You've earned yourself one free internets for your work. Thank you very much once again
  • Olivier Wilkinson
    Olivier Wilkinson almost 8 years
    No problem at all! I'm going to edit the answer to only include the final class (it's a bit long at the moment! haha)
  • Anters Bear
    Anters Bear over 6 years
    You're a hero my friend
  • Ashkan Ghodrat
    Ashkan Ghodrat about 5 years
    for some reason, in a very fast loop, other answers didnt work, but yours did. thanks
  • DiggyJohn
    DiggyJohn about 5 years
    This works great for simple applications where you only plan to pop off a few sounds... but if you want something like rapid machine gun fire, for example, your application gets bogged down with constant disk access loading the same sound over and over unnecessarily. Need to cache the sounds ahead of time in memory and just start stop the players.
  • Yerbol
    Yerbol about 5 years
    soundPlayers - is an array?
  • Bobjt
    Bobjt about 5 years
    Yes soundPlayers is an array.
  • Kurt Lane
    Kurt Lane over 4 years
    Your explanation made good sense to me so I added a second audio player var audioPlayer = AVAudioPlayer() var secondAudioPlayer = AVAudioPlayer() and played my second sound through that self.secondAudioPlayer = try AVAudioPlayer(contentsOf: self.AlarmEnd) self.secondAudioPlayer.play() and it worked. Thankyou!
  • mondousage
    mondousage over 4 years
    Fantastic. Works like a charm. Thanks for updating for Swift 4.
  • Marmaduk
    Marmaduk about 3 years
    Playing works great, but how would I go about stoping all players called with delay, seems that only the player that is currently playing in the players dict [URL: AVAudioPlayer] is being stopped, since the other (future) players are yet to be instantiated due to the delay. (e.g. stopping a player before it's created doesn't do anything). Am I missing something?
  • Marmaduk
    Marmaduk about 3 years
    Playing works great, but how would I go about stoping all players called with delay, seems that only the player that is currently playing in the players dict [URL: AVAudioPlayer] is being stopped, since the other (future) players are yet to be instantiated due to the delay. (e.g. stopping a player before it's created doesn't do anything). Am I missing something?
  • Olivier Wilkinson
    Olivier Wilkinson about 3 years
    @Marmaduk the sample code above doesn't support cancelling the sounds, to do that you could return a cancel function from playSounds which invalidates all the timers that have not yet run. You could go further and stop the currently playing sound when the cancel function is called also.
  • Raj D
    Raj D about 3 years
    I made a function to stop all sounds currently playing. func stopAllSounds() { for (_, player) in players { player.stop() } players.removeAll() for player in duplicatePlayers { player.stop() } duplicatePlayers.removeAll() }
  • bluefloyd8
    bluefloyd8 over 2 years
    I am getting errors when trying to use this class. Any idea? App/SceneDelegate.swift:140:31: 'GSAudio' initializer is inaccessible due to 'private' protection level
  • Makalele
    Makalele over 2 years
    Try adding 'public' keyword before class declaration.
  • Pokaboom
    Pokaboom about 2 years
    if I wanted to pause/seek a specific player, how can I do that?
  • Lucas Werner Kuipers
    Lucas Werner Kuipers about 2 years
    @Pokaboom You would need a way to reference that specific player. You could either store the player of the sound you are about to play somewhere (keeping a reference to it) or add new "id" / "key" property that could use to search for the player and apply what you want (pause / stop / play)