流利说团队:Building a Better Recorder in Android

1,398 阅读4分钟
原文链接: mp.weixin.qq.com

Recorder 可以说是流利说 app 中存在最长时间的类了, 用户通过这个类边录音边打分最后生成打分报告和对应的音频文件。本文分享了随着迭代的不断进行,流利说在 Recorder 使用上的心得。

Android Recorder 的选择

Android 系统 Java 层提供两个 Recorder Api, MediaRecorder 与 AudioRecorder,前者能够生成编码后的录音文件,而后者则是 PCM Audio RAW Data,显然后者是我们所需的,我们能通过对 PCM 的操作,来完成需要做的任何操作。


AudioFocus 的处理

一般录音开始的时候,如果手机正在播放音频,需要将音频暂停。所以建议在使用 Recorder 的时候把 AudioFocus 处理了,这样无论是与内置播放器还是其他 App 的播放器互斥的行为很容易就能实现。


AudioRecorder 基本使用方法

先介绍下 AudioRecorder 的基本使用 Pattern

1. 在 Recorder Thread 中创建 Recorder 与相关的 Encoder 和 Scorer。

2. loop 读取 Recorder 里面的 PCM data,不断地将 PCM 喂入 Encoder 和 Scorer 中。

3. 外部停止录音后,将 run 这个 flag 置为 false 跳出循环,并且 close 相关 Encoder 和 Scorer 并且保存他们的结果。

4. release 相关资源。

流利说在很长一段时间里都是通过如上的方法进行类似的实现,但是随着迭代的进行,该实现遇到了不少问题。以下是我们遇到的问题以及解决问题的思路。


遇到的问题

在 loop 中 Encoder 和 Scorer 的 process block 时间太长,会导致来不及读取 AudioRecorder buffer,最终导致录制的音频丢失数据,打分错误。


采用 ProcessThread

在 RecorderThread 上,新增加 ProcessThread, 让 Encoder 与 Scorer 在 ProcessThread 中执行,就此 RecoderThread 不会因为 Encoder 和 Scorer 而 block 导致数据丢失。


遇到的问题

随着功能的迭代,Recorder 需要支持越来越多特性,比如需要支持越来越多的音频格式,Recorder 就需要挂载不同的 Encoder。打分支持在线与离线,由此 Scorer 的调用也会略有不同。打分粒度的扩充,从原先的句子,到单词,到音标,注入的业务逻辑也越来越复杂。


引入 AudioProcessor

从以上对 AudioRecorder 的使用上来看,我们能够总结出调用的范式基本如下: 

init => read&process => close => release 

以上可以得出,Recorder 中 Encoder 和 Scorer 调用逻辑基本相同,只是具体的实现有不同。就此引入 AudioProcessor 抽象统一 Encoder 和 Scorer,调用方将 process audio data 的逻辑移出 Recorder 放在 AudioProcessor 中。

并且提取出 AudioProcessor 后,将 AudioProcessor 作为依赖注入 Recorder 中,调用者能够更加灵活的处理音频数据。挂载新的 Encoder 或者新的 Scorer,无需更改 Recorder 层面的代码,只需调整调用端即可,大大减少了维护的成本。

如上代码,LingoRecorder 在录音的时候,挂载了多个 AudioProcessor,能够同时生成多种音频目标文件,方便选取更加适合的文件格式,并且还可以根据不同 api 选取更加合适的 Encoder,做兼容处理也更加简单。还可以同时挂载 A/B 两个版本的打分器,更加直观比较两种实现的不同。


遇到的问题

因为依赖 AudioRecorder,导致 Recorder 的测试也很难进行,开发者运行起来,直接录音来测试,显然是耗时耗力,且无法覆盖测试的。


引入 WavFileRecorder

测试依赖 AndroidRecorder 的实现会导致测试非常麻烦,需要在 Android 机上真正运行才能测试,并且无法确保录音文件的统一,无法正确的覆盖测试的内容。就此引入 WavFileRecorder,对 WavFileRecorder 指定对应的文件即可模拟用户的录音过程。


遇到的问题

Recorder 中需要不断地和 Native 层进行对接,因此如果传入的数据未做严格的校验,导致 native 层报出的指针异常,是很难 handle 得很优雅的。


引入 ServiceWrapper

Recorder 调用到的 Encoder 与 Scorer 属于 ndk 层。所以如果录音出错了,在某些情况可能会出现 native crash 的情况,而对于用户来说,这样体验是非常糟糕的。但是在这种稳定性的问题无法避免的情况下,如果出现 native crash,让用户感知到录音出错,提示其重试录音会是一个相对较好的体验。因此我们可以引入 IAudioProcessorService 将 AudioProcessor 放在 Service 中执行,并且该 Service 处于另外一个进程。就此倘若出现 native crash,只有相关 AudioProcessor 进程会因为异常而被杀掉,主进程并不会受到影响,只需处理相关异常,提示用户重试即可。


总结

1. 在另一线程处理 pcm 数据,以避免来不及读取 AudioRecorder 导致数据丢失。

2. 抽象出 AudioProcessor 来注入 Recorder 中以支持录音和处理的分离。

3. 提供 WavFileRecorder 以支持以文件来替代录音器生成录音数据。

4. 提供 aidl 接口更简单的将 AudioProcessor 在另一个进程的 Service 中运行。


我们将如上实践封装出了 LingoRecorder(https://github.com/lingochamp/LingoRecorder)这个项目,感兴趣的可以参考下。