如何使用连接到 AVAudioEngine 混音器的节点同时播放缓冲区中的多个声音

Posted

技术标签:

【中文标题】如何使用连接到 AVAudioEngine 混音器的节点同时播放缓冲区中的多个声音【英文标题】:How to play multiple sounds from buffer simultaneously using nodes connected to AVAudioEngine's mixer 【发布时间】:2019-08-04 01:25:19 【问题描述】:

我正在为 ios 制作一个基本的音乐应用程序,按下音符会播放相应的声音。我正在尝试将多个声音存储在缓冲区中,以便以最小的延迟同时播放。但是,我在任何时候都只能播放一种声音。

我最初使用多个 AVAudioPlayer 对象设置我的声音,为每个播放器分配一个声音。虽然它确实同时播放了多个声音,但它似乎无法同时启动两个声音(似乎它会在第一个声音开始后稍微延迟第二个声音)。此外,如果我以非常快的速度按下音符,引擎似乎跟不上,在我按下后面的音符后,后面的声音会很好地开始。

我正在尝试解决这个问题,从我所做的研究来看,使用 AVAudioEngine 播放声音似乎是最好的方法,我可以在缓冲区数组中设置声音,然后它们从这些缓冲区中播放。

class ViewController: UIViewController

    // Main Audio Engine and it's corresponding mixer
    var audioEngine: AVAudioEngine = AVAudioEngine()
    var mainMixer = AVAudioMixerNode()

    // One AVAudioPlayerNode per note
    var audioFilePlayer: [AVAudioPlayerNode] = Array(repeating: AVAudioPlayerNode(), count: 7)

    // Array of filepaths
    let noteFilePath: [String] = [
    Bundle.main.path(forResource: "note1", ofType: "wav")!, 
    Bundle.main.path(forResource: "note2", ofType: "wav")!, 
    Bundle.main.path(forResource: "note3", ofType: "wav")!]

    // Array to store the note URLs
    var noteFileURL = [URL]()

    // One audio file per note
    var noteAudioFile = [AVAudioFile]()

    // One audio buffer per note
    var noteAudioFileBuffer = [AVAudioPCMBuffer]()

    override func viewDidLoad()
    
        super.viewDidLoad()
        do
        

            // For each note, read the note URL into an AVAudioFile,
            // setup the AVAudioPCMBuffer using data read from the file,
            // and read the AVAudioFile into the corresponding buffer
            for i in 0...2
            
                noteFileURL.append(URL(fileURLWithPath: noteFilePath[i]))

                // Read the corresponding url into the audio file
                try noteAudioFile.append(AVAudioFile(forReading: noteFileURL[i]))

                // Read data from the audio file, and store it in the correct buffer
                let noteAudioFormat = noteAudioFile[i].processingFormat

                let noteAudioFrameCount = UInt32(noteAudioFile[i].length)

                noteAudioFileBuffer.append(AVAudioPCMBuffer(pcmFormat: noteAudioFormat, frameCapacity: noteAudioFrameCount)!)

                // Read the audio file into the buffer
                try noteAudioFile[i].read(into: noteAudioFileBuffer[i])
            

           mainMixer = audioEngine.mainMixerNode

            // For each note, attach the corresponding node to the audioEngine, and connect the node to the audioEngine's mixer.
            for i in 0...2
            
                audioEngine.attach(audioFilePlayer[i])

                audioEngine.connect(audioFilePlayer[i], to: mainMixer, fromBus: 0, toBus: i, format: noteAudioFileBuffer[i].format)
            

            // Start the audio engine
            try audioEngine.start()

            // Setup the audio session to play sound in the app, and activate the audio session
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.soloAmbient)
            try AVAudioSession.sharedInstance().setMode(AVAudioSession.Mode.default)
            try AVAudioSession.sharedInstance().setActive(true)            
        
        catch let error
        
            print(error.localizedDescription)
        
    

    func playSound(senderTag: Int)
    
        let sound: Int = senderTag - 1

        // Set up the corresponding audio player to play its sound.
        audioFilePlayer[sound].scheduleBuffer(noteAudioFileBuffer[sound], at: nil, options: .interrupts, completionHandler: nil)
        audioFilePlayer[sound].play()

    

每个声音都应该在不打断其他声音的情况下播放,只有在再次播放声音时才打断自己的声音。然而,尽管设置了多个缓冲区和播放器,并在 audioEngine 的混音器上将每个缓冲区和播放器分配给自己的总线,播放一个声音仍然会阻止任何其他声音的播放。

此外,虽然省略 .interrupts 确实会阻止声音停止其他声音,但这些声音在当前播放的声音完成之前不会播放。这意味着如果我播放note1,然后note2,然后note3,note1会播放,而note2只有在note1完成后才会播放,而note3只会在note2完成后播放。

编辑:我能够在 playSound 函数中使用以下代码使 audioFilePlayer 再次重置到开头,而无需使用中断。

if audioFilePlayer[sound].isPlaying == true
        
            audioFilePlayer[sound].stop()
        
        audioFilePlayer[sound].scheduleBuffer(noteAudioFileBuffer[sound], at: nil, completionHandler: nil)
        audioFilePlayer[sound].play()

这仍然让我弄清楚如何同时播放这些声音,因为播放另一个声音仍会停止当前播放的声音。

编辑 2:我找到了解决问题的方法。我的答案如下。

【问题讨论】:

【参考方案1】:

事实证明,使用 .interrupt 选项不是问题(事实上,这实际上是我体验中重新启动正在播放的声音的最佳方式,因为在重新启动期间没有明显的停顿,不像 stop() 函数)。阻止同时播放多个声音的实际问题是这行特定的代码。

// One AVAudioPlayerNode per note
    var audioFilePlayer: [AVAudioPlayerNode] = Array(repeating: AVAudioPlayerNode(), count: 7)

这里发生的情况是,数组的每个项目都被分配了完全相同的 AVAudioPlayerNode 值,因此它们都有效地共享相同的 AVAudioPlayerNode。结果,AVAudioPlayerNode 函数影响了数组中的所有项目,而不仅仅是指定的项目。为了解决这个问题并给每个项目一个不同的 AVAudioPlayerNode 值,我最终更改了上面的行,以便它以 AVAudioPlayerNode 类型的空数组开始。

// One AVAudioPlayerNode per note
    var audioFilePlayer = [AVAudioPlayerNode]()

然后,我在 viewDidLoad() 函数的第二个 for 循环的开头添加了一个新行,将一个新的 AVAudioPlayerNode 添加到该数组。

// For each note, attach the corresponding node to the audioEngine, and connect the node to the audioEngine's mixer.
for i in 0...6

    audioFilePlayer.append(AVAudioPlayerNode())
    // audioEngine code

这为数组中的每个项目提供了不同的 AVAudioPlayerNode 值。播放声音或重新启动声音不再打断当前正在播放的其他声音。我现在可以同时播放任何音符,并且在按下音符和播放之间没有任何明显的延迟。

【讨论】:

请记住,在 Swift 中,类实例具有 reference 语义(结构和枚举具有值语义)! 更多音频文件时 UI 会冻结。为我在 0...31

以上是关于如何使用连接到 AVAudioEngine 混音器的节点同时播放缓冲区中的多个声音的主要内容,如果未能解决你的问题,请参考以下文章

使用 AVAudioEngine 进行电平测量

AVAudioEngine 是不是支持递归路由?

AVAudioEngine 调度样本准确的参数变化

如何在苹果的 AudioUnit 3D 混音器上设置总线?

混音 |无法连接到 Web3 提供程序

如何在 mac os x 上从通过 USB 连接的多轨混音器获取输入