線程安全引發的錄音雜音電流音問題

前段時間寫了一個錄音模塊,需求是:『錄音的時候實時語音轉文字,實時計算音量大小,實時進行 MP3 轉碼保存爲文件』安全

首先進行需求分析,肯定技術方案:微信

  1. 使用 AudioRecord 進行錄音,實時獲取原始音頻數據
  2. 將音頻數據傳遞給第三方語音轉文字 SDK 進行處理
  3. 對音頻數據進行處理,計算出音量大小
  4. 對音頻數據進行 MP3 編碼
  5. 將編碼後的數據寫入 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 一份畫面幀的數據保存爲圖片。

但是之前沒有寫博客記錄這種小問題,致使遇到相似的問題儘可能不記得了。因此此次記錄下來??。

歡迎關注微信公衆號:大腦好餓,更多幹貨等你來嘗

微信公衆號

相關文章
相關標籤/搜索