安卓手机上 K 歌,声音延迟怎么解决?
2018-07-16 11:50
381 查看
这篇文章可以为你提供一个解决录音和播放同步问题的思路,而且解决了声音从手机传输到耳机上有延时的问题。
我们知道声明是一种波,经过离散处理后,在程序中我们可以理解为一个无限接近该波形的一个数组,数组下标就是时间轴,对应的值是声音的幅度轴。
音频最基本的特性有:
采样频率(Sample Rate):每秒采集声音的数量,它用赫兹(Hz)来表示。
采样精度(Bit Depth):它表示每次采样的精度,位数越多,能记录的范围就越大。
声音通道(Channel):简单理解就是各个通道有一个独立的声音,它们会同时发出来。
这不是本文的重点,所以不再展开了,这里只是简单说明一下声音跟开发的关系。点击这篇文章可以帮助你更好地了解:音频基础认识。
先从 Android SDK 入手,发现 AudioRecord 里面有个方法 startRecording(MediaSyncEvent syncEvent) , 再看了一遍文档, 仿佛在黑暗中看到了一丝光亮。
The MediaSyncEvent class defines events that can be used to synchronize playback or capture * actions between different players and recorders.
这句话的大概意思就是 MediaSyncEvent 定义了用来处理播放器、录音或者视频录制的同步事件。
然而对于它的使用资料实在太少,stackoverflow 上有个提问是 0 回答:这里。翻了 Google 很久,最终在官方的 CTS (Compatibility Test Suite) 中找到了它的身影:在 AudioRecordTest 的 testSynchronizedRecord 方法中。
这里顺便提一下,这些单元测试是非常好实打实的官方学习资料,如果苦于找不到答案的时候,不妨来这里找找看。
由于 testSynchronizedRecord 的代码太长,大家可以点进上面链接对照看,我们通过它来看看 MediaSyncEvent 究竟可以做什么?
从 MediaSyncEvent 的文档或源码看,它里面主要定义了两个事件类型变量:一个是 SYNC_EVENT_NONE,另外一个是 SYNC_EVENT_PRESENTATION_COMPLETE。
SYNC_EVENT_NONE 就相当于没有同步事件,常规的 AudioRecord.startRecording() 方法就是用的这个参数。从 AudioRecordTest.testSynchronizedRecord 的测试用例中可以得知 SYNC_EVENT_PRESENTATION_COMPLETE 的作用其实是等 AudioTrack (Andriod SDK 中用来播放音频字节流的类)播放完的瞬间才触发 AudioRecord 的录音,这明显和我们的需求是不通的,没想明白在哪些场景会有这个需求,Google 要专门提供这个一个参数,如果有想法的朋友可以给我留言。
上面通过 CyclicBarrier 让 AudioTrack的 write 和 AudioRecord 的 read 在同一起跑线上,似乎事情已经解决了,然而并没有。虽然你开始往耳机 write 数据,但是耳机接收到信号真正发出声音还要一段时间。
播放源是真实的数据源,比如位于 1ms 的伴奏数据块从写入 AudioTrack 开始到耳机播放可能已经是 100ms 后的事情了,而用户这个时候才开始录入自己的声音,这里还可能会有从设备开始采集声音到缓冲区的一个延时,如果是使用蓝牙耳机的话,那延时的问题就会更加突出了。
我们来感受一下延时的情况,在咖啡馆录的音,杂音比较多,但是不难听出来录音是比原来的声音要延迟了。
http://ohb4y25jk.bkt.clouddn.com/audio-sync-test-1.mp3
看下声波图:
解决方案:
当录音和播放开始之后,它们就会在同一时域中平行演绎,根据延时的特点,我们不难得出:
录音时长 = 延迟时长 + 播放时长 + 额外时长(播放完之后的自由录音)
只要我们能知道延迟的时长,在读取录音数据的时候,我们只要截取掉 AudioRecord 前面的延迟数据就可以让问题得到解决了。那怎么才能知道应该截掉多少个 byte 的数据呢?在这里我想到了一个巧妙的解决方法,给大家分享一下思路。
从上面的节拍器的声波图我们可以看到,波峰对应的就是哒的那一声,录音音轨和节拍器音轨上的波峰差就是我们想知道的延迟时长。根据这个特点,我们可以设计出获取这个延迟时长的一个思路:
让用户带上耳机,根据固定节奏的节拍器(要有一定时间间隔)声音进行录音,简单的啦..啦..啦..就好。
根据获取到的录音数据和原始的节拍器声音进行比较, 我取的是 8 个波峰区间数据进行比较,如果延迟误差都在一个小范围内,那就认为是正确的。
具体的算法大概如下:
结果展示
波形图:
声音结果:
http://ohb4y25jk.bkt.clouddn.com/audio-sync-test-2.mp3
调整之后情况就改善多了,听觉上基本感受不到延迟了。但是这样会给用户带来一些不方便,换耳机的时候需要重新调整。个人的认知实在有限,虽然这可能是个有效的方法,但肯定不是最佳的做法,同时好奇像唱吧这种软件是如何处理的?欢迎大牛们交流一下想法~
MediaSyncEvent TestCase
初识音频
在开始之前,我先简单介绍一下音频相关的基础知识,方便下文理解。我们知道声明是一种波,经过离散处理后,在程序中我们可以理解为一个无限接近该波形的一个数组,数组下标就是时间轴,对应的值是声音的幅度轴。
音频最基本的特性有:
采样频率(Sample Rate):每秒采集声音的数量,它用赫兹(Hz)来表示。
采样精度(Bit Depth):它表示每次采样的精度,位数越多,能记录的范围就越大。
声音通道(Channel):简单理解就是各个通道有一个独立的声音,它们会同时发出来。
这不是本文的重点,所以不再展开了,这里只是简单说明一下声音跟开发的关系。点击这篇文章可以帮助你更好地了解:音频基础认识。
场景描述
我们在 Android 手机上进行 K 歌的时候,是要一边跟着伴奏的声音,一边进行录音的,最后把两个声音合并成一个声音。在实际处理的时候,你会发现录音的音轨和伴奏的音轨是会有个时间差,表现为录出来的声音跑在伴奏的后面了。如果是通过有线耳机或手机扬声器一边听伴奏一边录音,这个延迟会稍微没那么严重,但是人耳也能感受到滞后了;如果是用延时比较大的蓝牙耳机来一边听伴奏一边录,那么延迟问题就会很凸显了。本文的测试音频录音时用的是蓝牙耳机。一线希望:MediaSyncEvent?
先抛出结论:并不能解决问题~先从 Android SDK 入手,发现 AudioRecord 里面有个方法 startRecording(MediaSyncEvent syncEvent) , 再看了一遍文档, 仿佛在黑暗中看到了一丝光亮。
The MediaSyncEvent class defines events that can be used to synchronize playback or capture * actions between different players and recorders.
这句话的大概意思就是 MediaSyncEvent 定义了用来处理播放器、录音或者视频录制的同步事件。
然而对于它的使用资料实在太少,stackoverflow 上有个提问是 0 回答:这里。翻了 Google 很久,最终在官方的 CTS (Compatibility Test Suite) 中找到了它的身影:在 AudioRecordTest 的 testSynchronizedRecord 方法中。
这里顺便提一下,这些单元测试是非常好实打实的官方学习资料,如果苦于找不到答案的时候,不妨来这里找找看。
由于 testSynchronizedRecord 的代码太长,大家可以点进上面链接对照看,我们通过它来看看 MediaSyncEvent 究竟可以做什么?
从 MediaSyncEvent 的文档或源码看,它里面主要定义了两个事件类型变量:一个是 SYNC_EVENT_NONE,另外一个是 SYNC_EVENT_PRESENTATION_COMPLETE。
SYNC_EVENT_NONE 就相当于没有同步事件,常规的 AudioRecord.startRecording() 方法就是用的这个参数。从 AudioRecordTest.testSynchronizedRecord 的测试用例中可以得知 SYNC_EVENT_PRESENTATION_COMPLETE 的作用其实是等 AudioTrack (Andriod SDK 中用来播放音频字节流的类)播放完的瞬间才触发 AudioRecord 的录音,这明显和我们的需求是不通的,没想明白在哪些场景会有这个需求,Google 要专门提供这个一个参数,如果有想法的朋友可以给我留言。
CyclicBarrier 来帮忙
此路不通之后,我们需要另辟蹊径。在运动员比赛前,我们需要先让大家在同一线上等待,直到看到信号发出再一起出发。在这里,我们也需要让 AudioTrack 和 AudioRecord 先在同一起跑线上等着,然后一起出发,各奔东西。Java 世界里面的 CyclicBarrier 就很合适做这件事情。// play 和 record 两个同步线程 CyclicBarrier recordBarrier = new CyclicBarrier(2); AudioTrack audioTrack; AudioRecord audioRecord; // UI Thread public void start(){ recordBarrier.reset(); audioTrack.play(); audioRecord.startRecording(); new RecordThread().start(); new PlayThread().start(); } class RecordThread extends Thread{ public void run(){ //等play线程开始写的时候read recordBarrier.await(); audioRecord.read(); } } class PlayThread extends Thread{ public void run(){ //等reacord线程开始读的时候write recordBarrier.await(); audioTrack.write(); } }
上面通过 CyclicBarrier 让 AudioTrack的 write 和 AudioRecord 的 read 在同一起跑线上,似乎事情已经解决了,然而并没有。虽然你开始往耳机 write 数据,但是耳机接收到信号真正发出声音还要一段时间。
处理录音延时问题
我们回到用户真实的使用场景中,来看看问题是如何发生的?
播放源是真实的数据源,比如位于 1ms 的伴奏数据块从写入 AudioTrack 开始到耳机播放可能已经是 100ms 后的事情了,而用户这个时候才开始录入自己的声音,这里还可能会有从设备开始采集声音到缓冲区的一个延时,如果是使用蓝牙耳机的话,那延时的问题就会更加突出了。
我们来感受一下延时的情况,在咖啡馆录的音,杂音比较多,但是不难听出来录音是比原来的声音要延迟了。
http://ohb4y25jk.bkt.clouddn.com/audio-sync-test-1.mp3
看下声波图:
解决方案:
当录音和播放开始之后,它们就会在同一时域中平行演绎,根据延时的特点,我们不难得出:
录音时长 = 延迟时长 + 播放时长 + 额外时长(播放完之后的自由录音)
只要我们能知道延迟的时长,在读取录音数据的时候,我们只要截取掉 AudioRecord 前面的延迟数据就可以让问题得到解决了。那怎么才能知道应该截掉多少个 byte 的数据呢?在这里我想到了一个巧妙的解决方法,给大家分享一下思路。
从上面的节拍器的声波图我们可以看到,波峰对应的就是哒的那一声,录音音轨和节拍器音轨上的波峰差就是我们想知道的延迟时长。根据这个特点,我们可以设计出获取这个延迟时长的一个思路:
让用户带上耳机,根据固定节奏的节拍器(要有一定时间间隔)声音进行录音,简单的啦..啦..啦..就好。
根据获取到的录音数据和原始的节拍器声音进行比较, 我取的是 8 个波峰区间数据进行比较,如果延迟误差都在一个小范围内,那就认为是正确的。
具体的算法大概如下:
//ANALYZE\_BEAT\_LEN = 8 int\[\] maxPositions = new int\[ANALYZE\_BEAT\_LEN\]; for(i = 0; i != maxPositions.length; i++){ byte\[\] segBytes = getSegBytes(); //获取一拍时长的数据 maxPositions\[i\] = getMaxSamplePos(segBytes);// 获取拍中波峰所在的大致位置 } //按小到大排序 Arrays.sort(maxPositions); //取中间一半的值,如果平均值误差在 10 毫秒内,就认为是正确的 int sampleTotalValue = 0; int sampleLen = ANALYZE\_BEAT\_LEN / 2; int\[\] sampleValues = new int\[sampleLen\]; for(int beginIndex = sampleLen / 2, i=0; i != sampleLen; i++){ sampleValues\[i\] = maxPositions\[ i + beginIndex\]; sampleTotalValue += sampleValues\[i\]; } int averSampleValue = sampleTotalValue / sampleLen; boolean isValid = true; for(int sampleValue : sampleValues){ //errorRangeByteLen : 10 毫秒的 byte 长度 if(Math.abs(averSampleValue - sampleValue) > errorRangeByteLen){ isValid = false; } } if(isValid){ stopPlay = true; // 结果 int result = averSampleValue; }
结果展示
波形图:
声音结果:
http://ohb4y25jk.bkt.clouddn.com/audio-sync-test-2.mp3
调整之后情况就改善多了,听觉上基本感受不到延迟了。但是这样会给用户带来一些不方便,换耳机的时候需要重新调整。个人的认知实在有限,虽然这可能是个有效的方法,但肯定不是最佳的做法,同时好奇像唱吧这种软件是如何处理的?欢迎大牛们交流一下想法~
参考资料
无线音频的延时问题:http://www.memchina.cn/News/9733.htmlMediaSyncEvent TestCase
作者介绍
叶大侠,会玩吉他的产品程序员,不著名开源工具 JApiDocs 作者,C 大调音乐网 创始人。自由职业中,在折腾和音乐相关的产品。相关文章推荐
- XP系统,安卓手机搜不到热点,怎么解决
- 安卓手机对fixed支持不好怎么解决。
- 安卓手机内存不足怎么办?安卓手机内存越来越小解决方法汇总
- 安卓解决华为手机虚拟导航的沉浸效果展示
- iPhone手机浏览器运行嵌入页面的iframe时候会撑全屏,页面在安卓手机上正常,解决办法
- win8没有声音怎么办?win8系统没有声音解决方法教程
- win7无声音显示“未插入扬声器或耳机” 怎么解决
- 解决安卓手机WIFI热点选项消失问题
- 安卓手机怎么卸载APP软件
- 安卓video层级怎么解决
- 安卓手机不开机的情况下 怎么备份 联系人 短信 通话记录
- 安卓手机可以连上wifi但无法上网的解决办法
- 3种解决安卓(android)手机锁屏密码忘记的方法!
- [Phonegap+Sencha Touch] 移动开发19 某些安卓手机上弹出消息框 点击后不消失的解决办法
- 电脑/华为安卓手机 观看Coursera上视频无法播放解决方法
- 安卓手机微信发不出去怎么办 微信不能发信息怎么办
- 手机里面的图片删掉了怎么找回来的超全面解决方法
- 解决安卓手机卡顿的七个技巧