播放 AVAudioPlayerNode 时 AVAudioEngine inputNode 的格式发生变化

Posted

技术标签:

【中文标题】播放 AVAudioPlayerNode 时 AVAudioEngine inputNode 的格式发生变化【英文标题】:AVAudioEngine inputNode's format changes when playing an AVAudioPlayerNode 【发布时间】:2018-06-02 20:49:39 【问题描述】:

我将从一个简单的“操场”视图控制器类开始,它演示了我的问题:

class AudioEnginePlaygroundViewController: UIViewController 
    private var audioEngine: AVAudioEngine!
    private var micTapped = false
    override func viewDidLoad() 
        super.viewDidLoad()
        configureAudiosession()
        audioEngine = AVAudioEngine()
    

    @IBAction func toggleMicTap(_ sender: Any) 
        guard let mic = audioEngine.inputNode else 
            return
        
        if micTapped 
            mic.removeTap(onBus: 0)
            micTapped = false
            return
        
        stopAudioPlayback()

        let micFormat = mic.inputFormat(forBus: 0)
        print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
        mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat)  (buffer, when) in
            print("in tap completion")
            let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
        
        micTapped = true
        startEngine()
    

    @IBAction func playAudioFile(_ sender: Any) 
        stopAudioPlayback()
        let playerNode = AVAudioPlayerNode()

        let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
        let audioFile = readableAudioFileFrom(url: audioUrl)
        audioEngine.attach(playerNode)
        audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
        startEngine()
        playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
        playerNode.play()
    

    // MARK: Internal Methods

    private func configureAudioSession() 
        do 
            try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
            try AVAudioSession.sharedInstance().setActive(true)
         catch  
    

    private func readableAudioFileFrom(url: URL) -> AVAudioFile 
        var audioFile: AVAudioFile!
        do 
            try audioFile = AVAudioFile(forReading: url)
         catch  
        return audioFile
    

    private func startEngine() 
        guard !audioEngine.isRunning else 
            return
        

        do 
            try audioEngine.start()
         catch  
    

    private func stopAudioPlayback() 
        audioEngine.stop()
        audioEngine.reset()
    

上面的 VC 有一个 AVAudioEngine 实例和两个 UIButton 动作:一个播放在硬编码 url 中找到的音频文件,另一个在引擎的 inputNode上切换点击的安装/移除>。

我的目标是让实时麦克风窃听和音频文件播放同时工作,但完全相互排斥。也就是说,无论我的麦克风水龙头的当前状态如何,我都希望能够触发播放,反之亦然。如果我在触发音频文件播放之前安装水龙头,一切都会按预期工作。但是,如果我先播放音频文件,然后尝试安装水龙头,则会出现以下崩溃:

[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAEGraphNode.mm:810:CreateRecordingTap: (IsFormatSampleRateAndChannelCountValid(format))]

这导致我通过 installTap 调用上方的日志语句检查麦克风格式的数据。果然,当我在播放前安装水龙头时,我得到了预期的 44100.0 采样率和 1 的通道数。但是当我先播放音频文件并然后安装麦克风水龙头时,我的日志显示0 的采样率和 2 的通道数,这给了我上面显示的错误。

我尝试修改 AVAudioEngine 的启动/重置流程,尝试了 AVAudioSession 的不同类别/模式组合(请参阅我的 configureAudioSession 方法),并尝试手动创建点击格式,如下所示:

let micFormat = mic.inputFormat(forBus: 0)
var trueFormat: AVAudioFormat!
if micFormat.sampleRate == 0 
    trueFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)
 else 
    trueFormat = micFormat

print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
mic.installTap(onBus: 0, bufferSize: 2048, format: trueFormat)  (buffer, when) in
    print("in tap completion")
    let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))

这给了我一个类似但不同的错误:

[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAudioIONodeImpl.mm:896:SetOutputFormat: (IsFormatSampleRateAndChannelCountValid(hwFormat))]

我看不出麦克风的格式数据会因是否播放了 AVAudioPlayerNode 而有所不同的任何原因。

【问题讨论】:

【参考方案1】:

经过一番搜索,我发现了问题。问题在于音频引擎的 inputNode 单例。来自文档:

当第一次访问 inputNode 时,音频引擎会按需创建一个单例。要接收输入,请从输入音频节点的输出连接另一个音频节点,或者在其上创建一个录音抽头。

加上我遇到的格式问题的参考:

检查输入节点的输入格式(特别是硬件格式)的非零采样率和通道数,以查看是否启用了输入。

在我的 Playground 类中,触发音频文件播放的流程在创建“活动链”之前从不访问引擎的 inputNode

audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)

如果您希望引擎在内部配置自己以进行输入,您似乎必须在 start() 之前访问 AVAudioEngine 的 inputNode。即使停止()和重置()引擎也不会导致访问 inputNode 来重新配置引擎。 (我怀疑通过 disconnectNode 调用手动中断活动链将允许内部重新配置,但我还不确定)。

所以代码方面的修复很简单:只需在实例化后立即访问引擎的输入节点,以便为引擎配置音频输入。这是文件播放和麦克风敲击协同工作的整个课程:

import UIKit

class AudioEnginePlaygroundViewController: UIViewController 
    private var audioEngine: AVAudioEngine!
    private var mic: AVAudioInputNode!
    private var micTapped = false

    override func viewDidLoad() 
        super.viewDidLoad()
        configureAudioSession()
        audioEngine = AVAudioEngine()
        mic = audioEngine.inputNode!
    

    @IBAction func toggleMicTap(_ sender: Any) 
        if micTapped 
            mic.removeTap(onBus: 0)
            micTapped = false
            return
        

        let micFormat = mic.inputFormat(forBus: 0)
        mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat)  (buffer, when) in
            let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
        
        micTapped = true
        startEngine()
    

    @IBAction func playAudioFile(_ sender: Any) 
        stopAudioPlayback()
        let playerNode = AVAudioPlayerNode()

        let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
        let audioFile = readableAudioFileFrom(url: audioUrl)
        audioEngine.attach(playerNode)
        audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
        startEngine()
        playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
        playerNode.play()
    

    // MARK: Internal Methods

    private func configureAudioSession() 
        do 
            try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
            try AVAudioSession.sharedInstance().setActive(true)
         catch  
    

    private func readableAudioFileFrom(url: URL) -> AVAudioFile 
        var audioFile: AVAudioFile!
        do 
            try audioFile = AVAudioFile(forReading: url)
         catch  
        return audioFile
    

    private func startEngine() 
        guard !audioEngine.isRunning else 
            return
        

        do 
            try audioEngine.start()
         catch  
    

    private func stopAudioPlayback() 
        audioEngine.stop()
        audioEngine.reset()
    

【讨论】:

感谢您发布此后续内容! 我需要将采样率更改为 16khz(麦克风:44100,我想要:16000)。怎么做?任何提示。已经尝试过主混合节点,但如果我设置不同的格式,应用程序会崩溃

以上是关于播放 AVAudioPlayerNode 时 AVAudioEngine inputNode 的格式发生变化的主要内容,如果未能解决你的问题,请参考以下文章

AVAudioPlayerNode音量更改不会立即应用

AVAudioEngine 录制在 AVAudioPlayerNode 中播放的声音

播放完成后在 AVAudioPlayerNode 上调用 stop() 会导致崩溃

AVAudioPlayerNode lastRenderTime

通过 iOS (Swift) 中的 AVAudioPlayerNode 播放缓冲区时出现可听故障 *在模拟器中工作,但不在设备上

swift Swift Playground展示了如何使用AVAudioPlayerNode,AVAudioMixerNode,AVAudioEng在背景音频文件上播放多个wav文件