前段時間寫了一個錄音模塊,需求是:『錄音的時候實時語音轉文字,實時計算音量大小,實時進行 MP3 轉碼保存爲文件』安全
首先進行需求分析,肯定技術方案:微信
整個業務流程如上,只不過咱們爲了效率和解耦,將每一個處理邏輯獨立開來使用多線程進行併發處理。多線程
具體流程見下圖:併發
下面的僞代碼所有使用 kotlin
展現,不熟悉 kotlin
不要緊,只須要關注具體的業務邏輯。異步
首先建立一個線程給 AudioRecord 進行錄音採集:ide
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 文件都沒有問題。ui
開開心心的打包,測試,上線。編碼
而後你懂的,有些用戶錄製出現雜音、電流音、聲音斷斷續續。??
機智的同窗可能經過標題已經猜到了問題的緣由,但我當時沒有手機進行問題復現,爲了解決這個問題但是花了很大的功夫才定位到問題所在。
由於咱們在錄音採集時將數據讀取到 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 一份畫面幀的數據保存爲圖片。
但是之前沒有寫博客記錄這種小問題,致使遇到相似的問題儘可能不記得了。因此此次記錄下來??。
歡迎關注微信公衆號:大腦好餓,更多幹貨等你來嘗