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 市場下載了: 聲音筆記+,雖然還比較粗糙,期待你的支持~

相關文章
相關標籤/搜索