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

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

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

  1. 写给小白的音频认识基础
  2. Android上一种效果奇好的混音方法介绍

场景描述

音乐中只有一种声音有时候很单薄的,咱们常常但愿把不一样的声音加在一块儿,可是在录制的时候咱们须要严格同步起来,把两种声音的时差控制在听觉容许的范围内,才可能得到咱们想要的结果。另一点,在录制的时候,为了避免把播放的声音和人声或者器乐声混到一块,一般都须要录制者带着耳机边听边录。android

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

一线但愿:MediaSyncEvent?

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

确定先从 SDK 入手,发现 AudioRecord 里面有个方法 startRecording(MediaSyncEvent syncEvent) , 再看了一遍文档, 仿佛在黑暗中看到了一丝光亮。session

The MediaSyncEvent class defines events that can be used to synchronize playback or capture * actions between different players and recorders.app

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

研究完testSynchronizedRecord咱们回来看看MediaSyncEvent它到底是用来干吗的?工具

MediaSycEvent 能够经过 MediaSyncEvent.createEvent() 进行构造,它支持两种事件类型。单元测试

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

    /**
     * The corresponding action is triggered only when the presentation is completed
     * (meaning the media has been presented to the user) on the specified session.
     * A synchronization of this type requires a source audio session ID to be set via
     * {@link #setAudioSessionId(int) method.
     */
    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 来帮忙

此路不通以后,咱们须要另辟蹊径。在运动员比赛前,咱们须要先让你们在同一线上等待,直到看到信号发出再一块儿出发。在这里,咱们也须要让 AudioTrackAudioRecord 先在同一块儿跑线上等着,而后一块儿出发,各奔东西。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();
    }
}

复制代码

上面经过CyclicBarrierAudioTrackwriteAudioRecordread 在同一块儿跑线上,彷佛事情已经解决了,然而并无。虽然你开始往耳机write数据,可是耳机接收到信号真正发出声音还要一段时间。

处理录音延时问题

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

录音延时

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

咱们来感觉一下延时的状况,在咖啡馆录的音,杂音比较多,可是不难听出来录音是比原来的声音要延迟了。

看下声波图:

延迟声波图

解决方案:

当录音和播放开始以后,它们就会在同一时域中平行演绎,根据延时的特色,咱们不可贵出:

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

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

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

  1. 让用户带上耳机,根据固定节奏的节拍器(要有必定时间间隔)声音进行录音,简单的啦..啦..啦..就好。
  2. 根据获取到的录音数据和原始的节拍器声音进行比较, 我取的是 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;
}
复制代码

结果展现

波形图:

声音结果:

通过调整以后状况就改善多了,听觉上基本感觉不到延迟了。可是这样会给用户带来一些不方便,换耳机的时候须要从新调整。我的的认知实在有限,虽然这多是个有效的方法,但确定不是最佳的作法,同时好奇像唱吧这种软件是如何处理的?欢迎大牛们交流一下想法~

参考资料

  1. 无线音频的延时问题:http://www.memchina.cn/News/9733.html

  2. MediaSyncEvent TestCase:

技术交流群:70948803,大部分时间群里都是安静的,只交流技术相关,不多发言,不欢迎广告喷子。

不玩音乐的看到这里能够关闭了。

色彩浓重的广告时间:

若是你有玩音乐,我作了一个音乐学习和记录的辅助工具。终于能够在 App 市场下载了: 声音笔记+,虽然还比较粗糙,期待你的支持~

相关文章
相关标签/搜索