前段時間寫了一個錄音模組,需求是:『錄音的時候實時語音轉文字,實時計算音量大小,實時進行 MP3 轉碼儲存為檔案』
首先進行需求分析,確定技術方案:
- 使用 AudioRecord 進行錄音,實時獲取原始音訊資料
- 將音訊資料傳遞給第三方語音轉文字 SDK 進行處理
- 對音訊資料進行處理,計算出音量大小
- 對音訊資料進行 MP3 編碼
- 將編碼後的資料寫入 MP3 檔案
整個業務流程如上,只不過我們為了效率和解耦,將每個處理邏輯獨立開來使用多執行緒進行併發處理。
具體流程見下圖:
程式碼擼起來
下面的虛擬碼全部使用 kotlin
展示,不熟悉 kotlin
沒關係,只需要關注具體的業務邏輯。
音訊資料採集
首先建立一個執行緒給 AudioRecord 進行錄音採集:
val emitter: FlowableProcessor<ShortArray>
val isRecording = AtomicBoolean()
override fun run() {
var buffer = ShortArray(bufferSize)
while (isRecording.get()) {
val readSize = audioRecord.read(buffer, 0, bufferSize)
if (readSize > 0) {
//將資料使用 RxJava 傳送出去
emitter.onNext(buffer)
}
}
}
複製程式碼
我們在子執行緒中讀取到音訊資料,並且通過 RxJava 將資料向下傳遞。(用什麼傳遞不重要,重要的是將資料傳遞給下一層去進行處理)
對資料進行處理
外部接收 RxJava 的事件,對音訊資料進行處理 (再次提醒,不需要在意細節,主要關注業務流程) :
// 使用 observeOnIo() 操作將執行緒切換到 IO 執行緒
recorder?.start()?.observeOnIo()?.subscribe{ it:ShortArray ->
//此時的程式碼和錄音採集的程式碼分別執行在不同的執行緒上了
//計算音量大小
val volume = calVolume(it)
}
recorder?.start()?.observeOnIo()?.subscribe{ it:ShortArray ->
//此時的程式碼和錄音採集的程式碼分別執行在不同的執行緒上了
//將 ShortArray 轉換為 ByteArray
var pcmBuffer: ByteArray = ...
it.toByteArray(pcmBuffer)
//語音轉文字
...
}
recorder?.start()?.observeOnIo()?.subscribe{ it:ShortArray ->
//此時的程式碼和錄音採集的程式碼分別執行在不同的執行緒上了
//進行 MP3 編碼
val encode = mp3Encode(it)
if (encode != null && encode > 0) {
// 將編碼後的資料寫入檔案
mp3Stream.write(mp3Buffer, 0, encode)
}
}
複製程式碼
整個業務流程就是這樣,我自己使用的手機和公司所有的測試機,試聽錄製出來的 MP3 檔案都沒有問題。
開開心心的打包,測試,上線。
然後你懂的,有些使用者錄製出現雜音、電流音、聲音斷斷續續。??
機智的同學可能通過標題已經猜到了問題的原因,但我當時沒有手機進行問題復現,為了解決這個問題可是花了很大的功夫才定位到問題所在。
解決雜音問題
因為我們在錄音採集時將資料讀取到 buffer
物件中,然後將 buffer
物件通過 RxJava 向下傳遞,因為 RxJava 的下游都開啟了非同步執行緒去處理事件,那麼在錄音採集的死迴圈中不等當前的資料進行 MP3 編碼完畢就對 buffer
物件寫入新採集到的音訊資料,這個時候 MP3 編碼出來的音訊資料就被汙染了。
val emitter: FlowableProcessor<ShortArray>
val isRecording = AtomicBoolean()
override fun run() {
var buffer = ShortArray(bufferSize)
while (isRecording.get()) {
// 讀取音訊資料到 buffer 中
val readSize = audioRecord.read(buffer, 0, bufferSize)
if (readSize > 0) {
//將 buffer 傳送出去,因為下游是非同步處理,所以執行完畢直接開始下次迴圈
emitter.onNext(buffer)
}
}
}
複製程式碼
要解決這個問題很簡單:
// 將 buffer 資料 copy 一份進行傳遞,這樣就不會修改下游的資料了
emitter.onNext(buffer.copyOf())
複製程式碼
但是使用 copy 的方式會頻繁的建立、銷燬 ShortArray 物件,能不能優化一下呢?
我們可以使用物件池來管理 ShortArray,這樣就不會頻繁的進行建立、銷燬操作。在 Android 的 support.v4 包中有一個 Pools
類實現了簡單的物件池功能:
val bufferPool = Pools.SynchronizedPool<ShortArray>(10)
fun acquireBuffer(bufferSize: Int): ShortArray {
var buffer = bufferPool.acquire()
if (buffer == null || buffer.size != bufferSize) {
buffer = ShortArray(bufferSize)
}
return buffer
}
fun releaseBuffer(shortArray: ShortArray) {
try {
bufferPool.release(shortArray)
} catch (e: Exception) {
Timber.e(e)
}
}
override fun run() {
while (isRecording.get()) {
//通過物件池獲取 buffer 物件
val buffer = acquireBuffer(bufferSize)
// 讀取音訊資料到 buffer 中
val readSize = audioRecord.read(buffer, 0, bufferSize)
if (readSize > 0) {
//將 buffer 傳送出去,下游處理完畢後呼叫 releaseBuffer 對 buffer 物件進行釋放
emitter.onNext(buffer)
}
}
}
複製程式碼
總結
很簡單的一個多執行緒併發問題,但是當我們自己不能復現的時候,還是帶來了很大的麻煩。
這種問題在編寫 emitter.onNext(buffer)
這行程式碼的時候就應該要考慮到執行緒安全問題,並且我之前做直播截圖的時候也遇到過類似的問題,擷取直播流的畫面幀儲存為圖片,因為截圖的操作不會很頻繁,當時是直接 copy 一份畫面幀的資料儲存為圖片。
可是以前沒有寫部落格記錄這種小問題,導致遇到類似的問題儘量不記得了。所以這次記錄下來??。