线程安全引起的录音杂音电流音问题
0 条评论前段时间写了一个录音模块,需求是:『录音的时候实时语音转文字,实时计算音量大小,实时进行 MP3 转码保存为文件』
首先进行需求分析,确定技术方案:
- 使用 AudioRecord 进行录音,实时获取原始音频数据
- 将音频数据传递给第三方语音转文字 SDK 进行处理
- 对音频数据进行处理,计算出音量大小
- 对音频数据进行 MP3 编码
- 将编码后的数据写入 MP3 文件
整个业务流程如上,只不过我们为了效率和解耦,将每个处理逻辑独立开来使用多线程进行并发处理。
具体流程见下图:
代码撸起来
下面的伪代码全部使用 kotlin
展示,不熟悉 kotlin
没关系,只需要关注具体的业务逻辑。
音频数据采集
首先创建一个线程给 AudioRecord 进行录音采集:
1 | val emitter: FlowableProcessor<ShortArray> |
我们在子线程中读取到音频数据,并且通过 RxJava 将数据向下传递。(用什么传递不重要,重要的是将数据传递给下一层去进行处理)
对数据进行处理
外部接收 RxJava 的事件,对音频数据进行处理 (再次提醒,不需要在意细节,主要关注业务流程) :
1 | // 使用 observeOnIo() 操作将线程切换到 IO 线程 |
整个业务流程就是这样,我自己使用的手机和公司所有的测试机,试听录制出来的 MP3 文件都没有问题。
开开心心的打包,测试,上线。
然后你懂的,有些用户录制出现杂音、电流音、声音断断续续。😂😂
机智的同学可能通过标题已经猜到了问题的原因,但我当时没有手机进行问题复现,为了解决这个问题可是花了很大的功夫才定位到问题所在。
解决杂音问题
因为我们在录音采集时将数据读取到 buffer
对象中,然后将 buffer
对象通过 RxJava 向下传递,因为 RxJava 的下游都开启了异步线程去处理事件,那么在录音采集的死循环中不等当前的数据进行 MP3 编码完毕就对 buffer
对象写入新采集到的音频数据,这个时候 MP3 编码出来的音频数据就被污染了。
1 | val emitter: FlowableProcessor<ShortArray> |
要解决这个问题很简单:
1 | // 将 buffer 数据 copy 一份进行传递,这样就不会修改下游的数据了 |
但是使用 copy 的方式会频繁的创建、销毁 ShortArray 对象,能不能优化一下呢?
我们可以使用对象池来管理 ShortArray,这样就不会频繁的进行创建、销毁操作。在 Android 的 support.v4 包中有一个 Pools
类实现了简单的对象池功能:
1 | val bufferPool = Pools.SynchronizedPool<ShortArray>(10) |
总结
很简单的一个多线程并发问题,但是当我们自己不能复现的时候,还是带来了很大的麻烦。
这种问题在编写 emitter.onNext(buffer)
这行代码的时候就应该要考虑到线程安全问题,并且我之前做直播截屏的时候也遇到过类似的问题,截取直播流的画面帧保存为图片,因为截屏的操作不会很频繁,当时是直接 copy 一份画面帧的数据保存为图片。
可是以前没有写博客记录这种小问题,导致遇到类似的问题尽量不记得了。所以这次记录下来😂😂。