酷播亮新聞
最棒的知識補給站

一招解決安卓手機K歌延遲的問題

一招解決安卓手機K歌延遲的問題

這篇文章可以為你提供一個解決錄音和播放同步問題的思路,而且解決了聲音從手機傳輸到耳機上有延時的問題。

初識音頻

在開始之前,我先簡單介紹一下音頻相關的基礎知識,方便下文理解。

我們知道聲明是一種波,經過離散處理後,在程序中我們可以理解為一個無限接近該波形的一個數組,數組下標就是時間軸,對應的值是聲音的幅度軸。

一招解決安卓手機K歌延遲的問題

音頻最基本的特性有:

  1. 採樣頻率(Sample Rate)

    :每秒採集聲音的數量,它用赫茲(Hz)來表示。

  2. 採樣精度(Bit Depth)

    :它表示每次採樣的精度,位數越多,能記錄的範圍就越大。

  3. 聲音通道(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 數據,但是耳機接收到信號真正發出聲音還要一段時間。

處理錄音延時問題

我們回到用戶真實的使用場景中,來看看問題是如何發生的?

一招解決安卓手機K歌延遲的問題

播放源是真實的數據源,比如位於 1ms 的伴奏數據塊從寫入 AudioTrack 開始到耳機播放可能已經是 100ms 後的事情了,而用戶這個時候才開始錄入自己的聲音,這裡還可能會有從設備開始採集聲音到緩衝區的一個延時,如果是使用藍牙耳機的話,那延時的問題就會更加突出了。

我們來感受一下延時的情況,在咖啡館錄的音,雜音比較多,但是不難聽出來錄音是比原來的聲音要延遲了。

看下聲波圖:

一招解決安卓手機K歌延遲的問題

解決方案:

當錄音和播放開始之後,它們就會在同一時域中平行演繹,根據延時的特點,我們不難得出:

錄音時長 = 延遲時長 + 播放時長 + 額外時長(播放完之後的自由錄音)

只要我們能知道延遲的時長,在讀取錄音數據的時候,我們只要截取掉 AudioRecord 前面的延遲數據就可以讓問題得到解決了。那怎麼才能知道應該截掉多少個 byte 的數據呢?在這裡我想到了一個巧妙的解決方法,給大家分享一下思路。

從上面的節拍器的聲波圖我們可以看到,波峰對應的就是噠的那一聲,錄音音軌和節拍器音軌上的波峰差就是我們想知道的延遲時長。根據這個特點,我們可以設計出獲取這個延遲時長的一個思路:

  1. 讓用戶帶上耳機,根據固定節奏的節拍器(要有一定時間間隔)聲音進行錄音,簡單的啦..啦..啦..就好。
  2. 根據獲取到的錄音數據和原始的節拍器聲音進行比較, 我取的是 8 個波峰區間數據進行比較,如果延遲誤差都在一個小範圍內,那就認為是正確的。

一招解決安卓手機K歌延遲的問題

具體的演算法大概如下:

//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;

}

結果展示

波形圖:

一招解決安卓手機K歌延遲的問題

聲音結果:

調整之後情況就改善多了,聽覺上基本感受不到延遲了。但是這樣會給用戶帶來一些不方便,換耳機的時候需要重新調整。個人的認知實在有限,雖然這可能是個有效的方法,但肯定不是最佳的做法,同時好奇像唱吧這種軟體是如何處理的?歡迎大牛們交流一下想法~

參考資料

  1. 無線音頻的延時問題:http://www.memchina.cn/News/9733.html
  2. MediaSyncEvent TestCase

作者介紹

葉大俠,會玩吉他的產品程序員,不著名開源工具 JApiDocs 作者,C 大調音樂網 創始人。自由職業中,在折騰和音樂相關的產品。

OSC開源社區頭條號,每日推送最新優質的技術類文章,包括外文翻譯,軟體更新,技術博客等。歡迎關注osc開源社區頭條號,也可以點擊「了解更多」閱讀原文。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 OSC開源社區 的精彩文章:
如有侵權請來信告知:酷播亮新聞 » 一招解決安卓手機K歌延遲的問題