這篇文章可以為你提供一個解決錄音和播放的同步的思路,而且解決了聲音從手機傳輸到耳機上的延時的問題。
你需要有一些關於音訊的基本認識,如果你還不是很瞭解,建議先閱讀前面兩篇文章。
場景描述
音樂中只有一種聲音有時候很單薄的,我們經常希望把不同的聲音加在一起,但是在錄製的時候我們需要嚴格同步起來,把兩種聲音的時差控制在聽覺允許的範圍內,才可能獲得我們想要的結果。另外一點,在錄製的時候,為了不把播放的聲音和人聲或者器樂聲混到一塊,通常都需要錄製者帶著耳機邊聽邊錄。
為了實現最終兩個或者多個聲音能非常好的契合到一起,除了要解決錄音和播放的同步,還需要考慮到聲音從手機傳輸到耳機上的延時。這個場景除了會出現在一些比較專業的音樂軟體上,常用的 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()
進行構造,它支援兩種事件型別。
/**
* 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 來幫忙
此路不通之後,我們需要另闢蹊徑。在運動員比賽前,我們需要先讓大家在同一線上等待,直到看到訊號發出再一起出發。在這裡,我們也需要讓 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 後的事情了,而使用者這個時候才開始錄入自己的聲音,這裡還可能會有從裝置開始採集聲音到緩衝區的一個延時,如果是使用藍芽耳機的話,那延時的問題就會更加突出了。
我們來感受一下延時的情況,在咖啡館錄的音,雜音比較多,但是不難聽出來錄音是比原來的聲音要延遲了。
看下聲波圖:
解決方案:
當錄音和播放開始之後,它們就會在同一時域中平行演繹,根據延時的特點,我們不難得出:
錄音時長 = 延遲時長 + 播放時長 + 額外時長(播放完之後的自由錄音)
只要我們能知道延遲的時長,在讀取錄音資料的時候,我們只要擷取掉 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://www.memchina.cn/News/9733.html
技術交流群:70948803,大部分時間群裡都是安靜的,只交流技術相關,很少發言,不歡迎廣告噴子。
不玩音樂的看到這裡可以關閉了。
色彩濃重的廣告時間:
如果你有玩音樂,我做了一個音樂學習和記錄的輔助工具。終於可以在 App 市場下載了: 聲音筆記+,雖然還比較粗糙,期待你的支援~