AVAudioEngine 的 inputNode 在播放 AVAudioPlayerNode 时格式会改变

7
我将从一个简单的“playground”视图控制器类开始,这个类展示了我的问题:
```

我将从一个简单的“playground”视图控制器类开始,这个类展示了我的问题:

```
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找到的音频文件,另一个是切换在引擎的上安装/删除tap。我的目标是同时使直播麦克风捕捉和音频文件播放工作,但完全互相排斥。也就是说,我想无论当前的麦克风tap状态如何都能触发播放,反之亦然。如果在触发音频文件播放之前安装tap,则一切按预期完全正常运行。但是,如果首先播放音频文件,然后尝试安装tap,则会出现以下崩溃:
[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAEGraphNode.mm:810:CreateRecordingTap: (IsFormatSampleRateAndChannelCountValid(format))]

由于遇到问题,我通过在installTap调用上面的日志语句来检查麦克风格式的数据。果然,当我在播放之前安装tap时,我得到了预期的采样率44100.0和1个通道计数。但是当我先播放音频文件,然后再安装麦克风tap时,我的日志显示采样率为0,通道计数为2,这就引发了上述错误。

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

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个回答

12

经过一番搜索,我找到了问题所在。问题在于音频引擎的 inputNode 单例。从文档中可以看出:

当首次访问输入节点(inputNode)时,音频引擎会根据需要创建单例。要接收输入,请从输入音频节点的输出连接另一个音频节点,或在其上创建记录挂接。

同时也提到了我遇到的格式问题:

  

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

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

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

如果您想让AVAudioEngine在内部配置自身以进行输入,则似乎必须在启动(start())之前访问其inputNode。即使停止(stop())和重置(reset())引擎也无法导致访问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()
    }
}

2
感谢您发布这个跟进! - Rogare
我需要将采样率改为16khz(麦克风:44100,我想要:16000)。怎么做?有任何提示吗?已经尝试了主混合节点,但如果我设置不同的格式,应用程序会崩溃。 - MD TAREQ HASSAN

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