土鳖小屋

 找回密码
 欢迎注册

QQ登录

只需一步,快速开始

03广告位出售中
广告出售中配资
搜狗联盟招募 收益最高
亿恩免费建网站 服务器特价
【网维体】微信营销平台
【多线高防】无视一切攻击!广告
百度站长平台 精确提升SEO
21广告位高权重链接招租
大型装修门户助您抢占商机天翼云主机 四川资源池开启
国内/香港主机/试用/更有免费
微信小程序微信营销项目诚招代理
122广告位出售中
广告位 
    查看: 33|回复: 0
    收起左侧

    Android 实现边听伴奏边K歌探究

    [复制链接]
    弗罗多 发表于 2018-4-5 17:24 | 显示全部楼层 |阅读模式
    阿里云学生主机9.9元
    悦淘淘

    马上注册,结交更多好友,享用更多功能,让你轻松玩转土鳖小屋

    您需要 登录 才可以下载或查看,没有帐号?欢迎注册

    x

    这篇文章可以为你提供一个解决录音和播放的同步的思路,而且解决了声音从手机传输到耳机上的延时的问题。

    你需要有一些关于音频的基本认识,如果你还不是很了解,建议先阅读前面两篇文章。

      1. 写给小白的音频认识基础

      2. Android上一种效果奇好的混音方法介绍

    场景描述

    音乐中只有一种声音有时候很单薄的,我们经常希望把不同的声音加在一起,但是在录制的时候我们需要严格同步起来,把两种声音的时差控制在听觉允许的范围内,才可能获得我们想要的结果。另外一点,在录制的时候,为了不把播放的声音和人声或者器乐声混到一块,通常都需要录制者带着耳机边听边录。

    为了实现最终两个或者多个声音能非常好的契合到一起,除了要解决录音和播放的同步,还需要考虑到声音从手机传输到耳机上的延时。这个场景除了会出现在一些比较专业的音乐软件上,常用的 K 歌软件也不可避免会遇到这个问题。

    一线希望:MediaSyncEvent?

    先抛出结论:并不能解决问题~

    肯定先从 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.

    然而对于它的使用资料实在太少,stackoverflow 上有个提问是 0 回答:这里。翻了 Google 很久,最终在官方的 CTS (Compatibility Test Suite) 中找到了它的身影:在 AudioRecordTest 的testSynchronizedRecord方法中。这里顺便提一下,这些单元测试是非常好实打实的官方学习资料,如果苦于找不到答案的时候,不妨来这里找找看。

    研究完testSynchronizedRecord我们回来看看MediaSyncEvent它究竟是用来干嘛的?

    MediaSycEvent 可以通过 MediaSyncEvent.createEvent() 进行构造,它支持两种事件类型。

    1. /**
    2.      * No sync event specified. When used with a synchronized playback or capture method, the
    3.      * behavior is equivalent to calling the corresponding non synchronized method.
    4.      */
    5.     public static final int SYNC_EVENT_NONE = AudioSystem.SYNC_EVENT_NONE;

    6.     /**
    7.      * The corresponding action is triggered only when the presentation is completed
    8.      * (meaning the media has been presented to the user) on the specified session.
    9.      * A synchronization of this type requires a source audio session ID to be set via
    10.      * {@link #setAudioSessionId(int) method.
    11.      */
    12.     public static final int SYNC_EVENT_PRESENTATION_COMPLETE = AudioSystem.SYNC_EVENT_PRESENTATION_COMPLETE;
    复制代码

    其实就只有一种,SYNC_EVENT_NONE 就相当于没有同步事件,常规的 AudioRecord.startRecording() 方法就是用的这个参数。从AudioRecordTest.testSynchronizedRecord 的测试用例中可以得知SYNC_EVENT_PRESENTATION_COMPLETE的作用其实是等AudioTrack播放完的瞬间才触发AudioRecord的录音,这明显和我们的需求是不通的,没想明白在哪些场景会有这个需求,Google 要专门提供这个一个参数,如果有想法的朋友可以给我留言。

    CyclicBarrier 来帮忙

    此路不通之后,我们需要另辟蹊径。在运动员比赛前,我们需要先让大家在同一线上等待,直到看到信号发出再一起出发。在这里,我们也需要让 AudioTrack 和 AudioRecord 先在同一起跑线上等着,然后一起出发,各奔东西。Java 世界里面的CyclicBarrier就很合适做这件事情。

    1. // play 和 record 两个同步线程
    2. CyclicBarrier recordBarrier = new CyclicBarrier(2);

    3. AudioTrack audioTrack;
    4. AudioRecord audioRecord;

    5. // UI Thread
    6. public void start(){
    7.     recordBarrier.reset();
    8.     audioTrack.play();
    9.     audioRecord.startRecording();
    10.     new RecordThread().start();
    11.     new PlayThread().start();
    12. }

    13. class RecordThread extends Thread{
    14.     public void run(){
    15.         //等play线程开始写的时候read
    16.         recordBarrier.await();
    17.         audioRecord.read();
    18.     }
    19. }

    20. class PlayThread extends Thread{
    21.     public void run(){
    22.         //等reacord线程开始读的时候write
    23.         recordBarrier.await();
    24.         audioTrack.write();
    25.     }
    26. }
    复制代码

    上面通过CyclicBarrier让 AudioTrack的 write 和 AudioRecord 的 read 在同一起跑线上,似乎事情已经解决了,然而并没有。虽然你开始往耳机write数据,但是耳机接收到信号真正发出声音还要一段时间。

    处理录音延时问题

    我们回到用户真实的使用场景中,来看看问题是如何发生的?




                                   
    登录/注册后可看大图


    播放源是真实的数据源,比如位于 1ms 的伴奏数据块从写入AudioTrack开始到耳机播放可能已经是 100ms 后的事情了,而用户这个时候才开始录入自己的声音,这里还可能会有从设备开始采集声音到缓冲区的一个延时,如果是使用蓝牙耳机的话,那延时的问题就会更加突出了。

    我们来感受一下延时的情况,在咖啡馆录的音,杂音比较多,但是不难听出来录音是比原来的声音要延迟了。

    看下声波图:




                                   
    登录/注册后可看大图


    解决方案:

    当录音和播放开始之后,它们就会在同一时域中平行演绎,根据延时的特点,我们不难得出:

    录音时长 = 延迟时长 + 播放时长 + 额外时长(播放完之后的自由录音)

    只要我们能知道延迟的时长,在读取录音数据的时候,我们只要截取掉 AudioRecord 前面的延迟数据就可以让问题得到解决了。那怎么才能知道应该截掉多少个 byte 的数据呢?在这里我想到了一个巧妙的解决方法,给大家分享一下思路。

    从上面的节拍器的声波图我们可以看到,波峰对应的就是哒的那一声,录音音轨和节拍器音轨上的波峰差就是我们想知道的延迟时长。根据这个特点,我们可以设计出获取这个延迟时长的一个思路:

      1. 让用户带上耳机,根据固定节奏的节拍器(要有一定时间间隔)声音进行录音,简单的啦..啦..啦..就好。

      2. 根据获取到的录音数据和原始的节拍器声音进行比较, 我取的是 8 个波峰区间数据进行比较,如果延迟误差都在一个小范围内,那就认为是正确的。




                                   
    登录/注册后可看大图


    具体的算法大概如下:

    1. //ANALYZE_BEAT_LEN = 8
    2. int[] maxPositions = new int[ANALYZE_BEAT_LEN];
    3. for(i = 0; i != maxPositions.length; i++){
    4.     byte[] segBytes = getSegBytes(); //获取一拍时长的数据
    5.     maxPositions[i] = getMaxSamplePos(segBytes);// 获取拍中波峰所在的大致位置
    6. }

    7. //按小到大排序
    8. Arrays.sort(maxPositions);

    9. //取中间一半的值,如果平均值误差在 10 毫秒内,就认为是正确的
    10. int sampleTotalValue = 0;
    11. int sampleLen = ANALYZE_BEAT_LEN / 2;
    12. int[] sampleValues = new int[sampleLen];

    13. for(int beginIndex = sampleLen / 2, i=0; i != sampleLen; i++){
    14.     sampleValues[i] = maxPositions[ i + beginIndex];
    15.     sampleTotalValue += sampleValues[i];
    16. }

    17. int averSampleValue = sampleTotalValue / sampleLen;

    18. boolean isValid = true;
    19. for(int sampleValue : sampleValues){
    20.     //errorRangeByteLen : 10 毫秒的 byte 长度
    21.     if(Math.abs(averSampleValue - sampleValue) > errorRangeByteLen){
    22.         isValid = false;
    23.     }
    24. }

    25. if(isValid){
    26.     stopPlay = true;
    27.     // 结果
    28.     int result = averSampleValue;
    29. }
    复制代码
    了,听觉上基本感受不到延迟了。但是这样会给用户带来一些不方便,换耳机的时候需要重新调整。个人的认知实在有限,虽然这可能是个有效的方法,但肯定不是最佳的做法,同时好奇像唱吧这种软件是如何处理的?欢迎大牛们交流一下想法~
    参考资料
      1. 无线音频的延时问题:http://www.memchina.cn/News/9733.html
    技术交流群:70948803,大部分时间群里都是安静的,只交流技术相关,很少发言,不欢迎广告喷子。
    不玩音乐的看到这里可以关闭了。
    色彩浓重的广告时间:
    如果你有玩音乐,我做了一个音乐学习和记录的辅助工具。终于可以在 App 市场下载了: 声音笔记+,虽然还比较粗糙,期待你的支持~



    作者:叶大侠
    链接:https://juejin.im/post/5aae0caff265da23793befd1


    您需要登录后才可以回帖 登录 | 欢迎注册

    本版积分规则

    QQ|手机版|小黑屋|土鳖小屋 ( 豫ICP备14000521号-2  

    GMT+8, 2018-5-24 20:10

    Powered by Discuz! X3.2

    © 2001-2013 Comsenz Inc.

    快速回复 返回顶部 返回列表