使AVAudioPlayer同时播放多个声音

26

我正在尝试在AVAudioPlayer实例上播放多个音频文件,但是当一个声音播放时,另一个声音就停止了。 我无法同时播放多个声音。 这是我的代码:

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 ()
    }
}

有谁可以告诉我如何同时播放多个音频文件吗?非常感谢任何帮助。

非常感谢, Kai


1
你有没有试过我的课程? - Olivier Wilkinson
1
@OlivierWilkinson,我确实尝试了你的类,如果你想同时开始两个声音,它很好用,但是当第二个声音开始播放时,我不希望它突然停止已经播放的声音。感谢你的帮助。 - Kai
1
我不确定我理解这个问题。 - Olivier Wilkinson
1
声音同时播放,它们不会相互停止。如果您想单独调用声音,则可以调用playSound()而不是playSounds()。即使使用playSound(),它也不会停止先前的声音。 - Olivier Wilkinson
1
没问题,我会在接下来的一个小时内编辑我的答案以包含那种情况 :) - Olivier Wilkinson
显示剩余5条评论
5个回答

44
音频停止的原因是你只有一个AVAudioPlayer设置,所以当你要求该类播放另一个声音时,你正在用AVAudioPlayer的新实例替换旧实例。你基本上在覆盖它。
你可以创建两个GSAudio类的实例,然后在每个实例上调用playSound,或者将该类作为通用音频管理器,使用audioPlayers字典。
我更喜欢后者选项,因为它可以产生更清晰的代码,并且也更有效率。例如,你可以检查是否已经为该声音制作了播放器,而不是制作一个新的播放器。
无论如何,我重新为你制作了该类,使得它可以同时播放多个声音。它还可以在自身上播放相同的声音(它不会替换前一个声音的实例)。希望有所帮助!
该类是单例模式,因此要访问该类,请使用:
GSAudio.sharedInstance

例如,要播放声音,您需要调用以下函数:
GSAudio.sharedInstance.playSound("AudioFileName")

同时播放多个声音:

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

或者你可以在某个地方将声音加载到数组中,并调用接受数组的playSounds函数:

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

我还添加了一个playSounds函数,它允许你以级联的方式延迟播放每个声音。因此:
 let soundFileNames = ["SoundFileName1", "SoundFileName2", "SoundFileName3"]
 GSAudio.sharedInstance.playSounds(soundFileNames, withDelay: 1.0)

希望第二个声音(sound2)在第一个声音(sound1)的后面延迟一秒钟播放,接着第三个声音(sound3)在第二个声音(sound2)的后面延迟一秒钟播放等等。

以下是该类:

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
    }

}

1
谢谢您的编辑,但是当我尝试使用这个类时,出现了“命令由于信号而失败:分段错误:11”的错误。请问您知道如何解决吗?非常感谢。 - Kai
1
是的,我刚刚也看到了,我不经意间复制了一个与另一个函数冲突的函数。两秒钟就好。 - Olivier Wilkinson
2
没问题!我将编辑答案,只包括最终的类(目前有点长!哈哈) - Olivier Wilkinson
2
这对于只计划弹出几个声音的简单应用程序非常有效...但是,如果您想要像快速机枪射击这样的东西,您的应用程序会因为不必要地反复加载相同的声音而变得很慢。需要提前在内存中缓存声音,并启动/停止播放器。 - DiggyJohn
2
你的解释让我很容易理解,所以我添加了第二个音频播放器 var audioPlayer = AVAudioPlayer() var secondAudioPlayer = AVAudioPlayer() 并通过它播放了我的第二个声音 self.secondAudioPlayer = try AVAudioPlayer(contentsOf: self.AlarmEnd) self.secondAudioPlayer.play() ,它起作用了。谢谢! - Kurt L.
显示剩余8条评论

23
这里是一个Swift 4版本的@Oliver Wilkinson代码,增加了一些安全检查和改进的代码格式:
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)
        }
    }

}

1
太棒了,运行得非常好。感谢您更新到Swift 4。 - mondousage
播放功能很好,但是我该如何停止所有使用延迟调用的播放器?似乎只有在players字典中当前正在播放的播放器[URL: AVAudioPlayer]才会被停止,因为其他(未来的)播放器由于延迟尚未实例化。(例如,在创建播放器之前停止播放器不会发生任何事情)。我是否漏掉了什么? - Marmaduk
当我尝试使用这个类时,我遇到了错误。有什么想法吗?App/SceneDelegate.swift:140:31: 'GSAudio'初始化程序由于“private”保护级别而无法访问 - bluefloyd8
尝试在类声明前添加“public”关键字。 - Makalele

10

我创建了一个帮助库,可以简化Swift中播放声音的过程。它创建了多个AVAudioPlayer实例,以允许同时多次播放相同的声音。您可以从Github上下载它,或使用Cocoapods导入。

这是链接: SwiftySound

使用方法非常简单:

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

你是我的英雄,朋友。 - Anters Bear

8
所有的答案都是发布代码页面;它并不需要那么复杂。
// 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
                }
            }
        }

由于某种原因,在一个非常快的循环中,其他答案都不起作用,但是你的答案起了作用。谢谢。 - Ashkan Ghodrat
soundPlayers - 是一个数组吗? - Yerbol
是的,soundPlayers 是一个数组。 - Bobjt

5

Swift 5+

编译之前的答案,改进代码风格和可重用性

我通常避免在我的项目中使用松散的字符串,并使用自定义协议来保存那些字符串属性的对象。

我更喜欢这种方法而不是使用enum,因为枚举很容易将你的项目紧密地耦合在一起。每当您添加一个新的case时,您必须编辑同一个文件中的枚举,从SOLID的开放-封闭原则中有所违背,并增加了出错的机会。

在这种特殊情况下,您可以拥有一个定义声音的协议:

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

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

当您需要一个新的声音时,您可以简单地创建一个实现此协议的新结构或类(如果您的IDE支持它,就像Xcode一样,甚至会在自动完成时建议它,为您提供与枚举类似的好处...而且在中等到大型多框架项目中运作得更好)。

(通常我会保留默认实现的音量和其他配置,因为它们很少被定制。)

例如,您可以拥有一个硬币掉落的声音:

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

然后,您可以使用一个单例的 SoundManager 来管理播放音频文件。
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 }
    }
}

这里我创建了一个帮助器getAudioPlayer,以便能够从代码执行中提前返回并利用guard let
更经常使用guard let,并且更喜欢较少嵌套的代码,通常可以大大提高可读性。
要在项目中的任何地方使用此SoundManager,只需访问其共享实例并传递符合Sound的对象。
例如,给出先前的CoinDropSound
SoundManager.shared.play(sound: CoinDropSound())

您可以省略sound参数,因为它可能会提高可读性。

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

然后:
SoundManager.shared.play(CoinDropSound())

如果我想暂停/寻找特定的播放器,我该怎么做? - Pokaboom
@Pokaboom 你需要一种引用特定玩家的方法。您可以在某个地方存储要播放声音的玩家(保留对其的引用),或者添加新的“id” / “key”属性,以便搜索该玩家并应用所需操作(暂停/停止/播放)。 - Lucas Werner Kuipers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接