iOS Crash治理 -- format.sampleRate == hwFormat.sampleRate治理

1,050 阅读3分钟

一. 背景

由于项目中使用了语音识别功能,该功能也带来了AVAudioEngine的录音崩溃问题,该崩溃偶现,出现概率相对较低,具体崩溃堆栈如下:

二. 分析和治理

我们从崩溃的堆栈定位到崩溃代码:

因为是崩溃在installTapOnBus函数,这里指的是语音的输入节点,创建监听器,监听音频输入流,并通过block回调进行处理。

- (void)installTapOnBus:(AVAudioNodeBus)bus bufferSize:(AVAudioFrameCount)bufferSize format:(AVAudioFormat * __nullable)format block:(AVAudioNodeTapBlock)tapBlock;

从崩溃的原因reason required condition is false: format.sampleRate == hwFormat.sampleRate,可以看出这个崩溃是因为输入流的采样率和输出采样率不一致导致的。

A.方案一

既然是采样率不一样,那这边直接从输入的采样率inputFormat取值赋值给输出的采样率,保证两者一致。

- (AVAudioFormat *)audioFormat:(AVAudioInputNode *)inputNode {
    AVAudioFormat *inputFormat = [inputNode outputFormatForBus:0];
    AVAudioFormat *format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 sampleRate:inputFormat.sampleRate channels:inputFormat.channelCount interleaved:YES];
    return format;
}

使用了该方法来治理,上线后依然会出现采样率不一致的崩溃。

B. 方案二

由于第一种方案依然没有根治这个崩溃,该崩溃还是偶现出现。

因此也参考了一些其他的博客: cloud.tencent.com/developer/a…

这篇文章里面是作者是自己创建一个混合输出的节点mixerNode,然后将mixerNode添加到音视频管理引擎上,接着获取输入节点inputNode,将输入节点inputNode的输入格式设置为连接输入节点和混合节点的格式设置,最后在混合节点这里设置监听器,监听输入流。

let inputFormat = audioEngine.inputNode.inputFormat(forBus: 0)
let outputFormat = audioEngine.mainMixerNode.outputFormat(forBus: 0)
//修改format为inputNode的format,防止录音崩溃
audioEngine.connect(audioEngine.inputNode, to: audioEngine.mainMixerNode, fromBus: 0, toBus: 0, format: inputFormat)
audioEngine.mainMixerNode.installTap(onBus: 0, bufferSize: 4096, format: outputFormat) { 
 [weak self] pcmBuffer, when in
...
}

但这里跟我们这边业务场景有输入,问题导致的原因不一样。

因为这个问题是偶现的,因此猜想是不是有一些异常情况,导致的输入节点的输入采样率和输出采样率不匹配。

比如这篇文章提到的cloud.tencent.com/developer/a…

因此修改的方案如下:

    AVAudioFormat *recordingFormat = [inputNode outputFormatForBus:0];
    AVAudioFormat *inputFormat = [inputNode inputFormatForBus:0];
    [inputNode removeTapOnBus:0];
    /// 判断输入采样率和输出采样率是否一致
    if (inputFormat.sampleRate != recordingFormat.sampleRate) {
        return;
    }
    /// 过滤输入采样率或者频道数为空的情况
    if (inputFormat.sampleRate == 0 ||
        inputFormat.channelCount == 0) {
        return;
    }
    /// 过滤输出采样率或者频道数为空的情况
    if (recordingFormat.sampleRate == 0 ||
        recordingFormat.channelCount == 0) {
        return;
    }
    [inputNode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
        [self.recognitionRequest appendAudioPCMBuffer:buffer];
    }];

然后加上相关的降级代码和日志和错误上报的埋点代码。

经上线验证后,发现这个问题彻底解决。

三. 总结

经过上面的分析和来回治理,这个崩溃:reason required condition is false: format.sampleRate == hwFormat.sampleRate,最终的解决办法是通过过滤一些异常情况,比如判断输入采样率和输出采样率真的不一致、输入采样率或者频道数为0的情况、输出采样率或者频道数为0,遇到这种情况,就直接return放弃此次的采样。

    AVAudioFormat *recordingFormat = [inputNode outputFormatForBus:0];
    AVAudioFormat *inputFormat = [inputNode inputFormatForBus:0];
    [inputNode removeTapOnBus:0];
    /// 判断输入采样率和输出采样率是否一致
    if (inputFormat.sampleRate != recordingFormat.sampleRate) {
        return;
    }
    /// 过滤输入采样率或者频道数为空的情况
    if (inputFormat.sampleRate == 0 ||
        inputFormat.channelCount == 0) {
        return;
    }
    /// 过滤输出采样率或者频道数为空的情况
    if (recordingFormat.sampleRate == 0 ||
        recordingFormat.channelCount == 0) {
        return;
    }
    [inputNode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
        [self.recognitionRequest appendAudioPCMBuffer:buffer];
    }];

四. 推荐

这是我的另外两篇崩溃治理,有兴趣的也可以看下:

货拉拉iOS疑难Crash治理-系统键盘语音

货拉拉iOS疑难Crash治理-TTS problem iOS 17