AVAudioEngine输入节点在重新开始录制时安装Tap会崩溃

21

我正在我的应用程序中实现语音识别。当我首次呈现带有语音识别逻辑的视图控制器时,一切正常。但是,当我尝试再次呈现视图控制器时,我遇到了以下崩溃:

ERROR:    [0x190bf000] >avae> AVAudioNode.mm:568: CreateRecordingTap: required condition is false: IsFormatSampleRateAndChannelCountValid(format)
*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'required condition is false: IsFormatSampleRateAndChannelCountValid(format)'

以下是用于开始和停止录制的代码:

@available(iOS 10.0, *)
extension DictationViewController {

fileprivate func startRecording() throws {
    guard let recognizer = speechRecognizer else {
        debugLog(className, message: "Not supported for the device's locale")
        return
    }

    guard recognizer.isAvailable else {
        debugLog(className, message: "Recognizer is not available right now")
        return
    }

    mostRecentlyProcessedSegmentDuration = 0
    guard let node = audioEngine.inputNode else {
        debugLog(className, message: "Could not get an input node")
        return
    }

    let recordingFormat = node.outputFormat(forBus: 0)
    node.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] (buffer, _) in
        self?.request.append(buffer)
    }

    audioEngine.prepare()
    try audioEngine.start()

    recognitionTask = recognizer.recognitionTask(with: request, resultHandler: {/***/})
}

fileprivate func stopRecording() {
    audioEngine.stop()
    audioEngine.inputNode?.removeTap(onBus: 0)
    request.endAudio()
    recognitionTask?.cancel()
}

}

在请求授权后,startRecording()会在viewDidLoad中调用。当视图控制器被解除时,stopRecording()会被调用。

请帮忙。我正在努力寻找解决这个崩溃的方法。


你找到解决方法了吗?我遇到了类似的崩溃问题,但只有在运行iOS 8.1时才会出现? - Luke Bartolomeo
如果这是Mac应用程序,那么您需要具备音频输入功能。 - onmyway133
7个回答

23

首先,有一个小问题。当敲击设备的麦克风时,您需要使用输入总线的格式:

let recordingFormat = node.inputFormat(forBus: 0)

其次,在进行一些挖掘后,似乎这种崩溃最常源自于您应用程序共享的AVAudioSession类别设置。如果您打算执行实时麦克风音频处理,请确保已像以下这样配置您的音频会话:

private func configureAudioSession() {
    do {
        try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: .mixWithOthers)
        try AVAudioSession.sharedInstance().setActive(true)
    } catch { }
}

第一部分是错误的。我们应该使用inputNode的“outputFormat”。 - joshmori
@joshmori 能否详细说明一下? - WongWray
1
在总线上安装音频监听器,以记录、监控和观察“节点输出”。来自 https://developer.apple.com/documentation/avfoundation/avaudionode/1387122-installtap节点的输出 => 使用该节点的 outputFormat。 - joshmori
有趣的是,你提供的示例在使用麦克风时采用了 outputFormat。我认为由于麦克风只处理语音输入,所以应该使用 inputFormat。我在网上找到了几个示例,一些使用 inputFormat,一些使用 outputFormat。也许有人可以解释这里的差异? - WongWray

17

解决此问题有两种可能的方法。

  1. 检查 inputFormat.channelCount。这可能是因为麦克风正在被另一个应用程序或其他地方使用而引发错误。
if(inputNode.inputFormat(forBus: 0).channelCount == 0){
    NSLog("Not enough available inputs!")
    return
}
  1. 尝试重置 audioEngine
audioEngine.reset()

10
我在进行电话通话时尝试使用语音识别时,出现了“所需条件无效:IsFormatSampleRateAndChannelCountValid(format)”的崩溃,这导致采样率为零。 我的解决方案是创建如下的audioInputIsBusy()函数,并在try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)之前调用它。 这可以防止崩溃,并显示“语音识别不可用”的消息,然后使用audioEngine = AVAudioEngine()重置audioEngine。
func audioInputIsBusy(recordingFormat: AVAudioFormat) -> Bool {
    guard recordingFormat.sampleRate == 0 || recordingFormat.channelCount == 0 else {
        return false
    }

    return true
}

提示: let recordingFormat = audioEngine.inputNode.outputFormat(forBus: 0)

意思是在使用音频引擎时,将输入节点的输出格式赋值给 recordingFormat 变量。


你好,能否提供更多的代码?你是说要重置引擎然后再次调用installTap吗? - famfamfam
@famfamfam 是的,事件顺序大致如下...中间还有其他逻辑:1. audioEngine = AVAudioEngine() 2. 若录音格式 recordingFormat 可用,则继续执行;否则结束。3. audioEngine.inputNode 安装一个 tap。4. 尝试启动 audioEngine。 - carlachip

6
您可以替换这段代码:
let recordingFormat = node.outputFormat(forBus: 0)

with the following:

let recordingFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)

这段代码解决了问题。

1
谢谢这个修复。但是 outputFormat(forBus: 0) 不工作的原因是什么? - e.ozmen
因为您正在尝试使用outputNode的格式来监听inputNode。 - Daedelus
2
但是AVAudioNode类中的installTapOnBus方法示例向开发人员展示了如何使用该方法;AVAudioEngine *engine = [[AVAudioEngine alloc] init]; AVAudioInputNode *input = [engine inputNode]; AVAudioFormat *format = [input outputFormatForBus: 0]; [input installTapOnBus: 0 bufferSize: 8192 format: format block: ^(AVAudioPCMBuffer *buf, AVAudioTime *when) { // ‘buf' 包含从输入节点捕获的音频,时间为 'when' }];他们将outputformatonbus提供给installtaponbus方法。 - Kaan Esin
1
@Daedelus OP 没有使用 outputNode 的格式。OP 正在使用 inputNode 的 outputFormat。 - R. Rincón
你绝不能假设采样率:我相信 iPhone X 及其以后的设备采样率为 48000,请使用 AVAudioEngine.inputNode.outputFormat.sampleRate 确定您设备的采样率,同时确保在录制之前 AVAudioSession.sharedInstance().sampleRate 相同,否则会崩溃。 - Maximilian
今天我发现当苹果车载播放器在汽车中时,采样率也是不同的,在三菱汽车中为24000。 - neskafesha

2

在我的情况下,我必须在安装Tap之前调用removeTap()才能使其正常工作。以上解决方案对我都不起作用。

//Remove tap first.
inputNode.removeTap(onBus: 0)

// Configure the microphone input.
let recordingFormat = inputNode.inputFormat(forBus: 0)            
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            //process buffer...
        }

0
class AudioRecordProvider {
  
    var audioEngine = AVAudioEngine()
    let mixerNode = AVAudioMixerNode()

    
    func startListening() throws {
        guard !audioEngine.isRunning else { return }
        let audioSession = AVAudioSession.sharedInstance()
        
        try audioSession.setCategory(.playAndRecord)
        
        audioSession.requestRecordPermission { [weak self] success in
            guard success, let self = self else { return }
            try? self.audioEngine.start()
        }
    }
    
    func stopListening() {
        if audioEngine.isRunning {
            audioEngine.stop()
        }
    }


    
    func configureAudioEngine() {
        self.audioEngine = AVAudioEngine()
        let inputFormat = audioEngine.inputNode.inputFormat(forBus: 0)
        
        let outputFormat = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1)
        
        audioEngine.attach(mixerNode)

        audioEngine.connect(audioEngine.inputNode, to: mixerNode, format: inputFormat)
        audioEngine.connect(mixerNode, to: audioEngine.outputNode, format: outputFormat)
            
    }
}

这个答案对我很有用。这是使用示例。 生命周期:configureAudioEngine -> startListening -> stopListening。 每次调用configureAudioEngine都会重新初始化AVAudioEngine,并且它有效。


-1

在开始每次运行之前,请尝试以下操作:

audioEngine = AVAudioEngine()


仍然会导致崩溃。 - famfamfam

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