banner
jzman

jzman

Coding、思考、自觉。
github

AudioRecord收集音頻數據及合成

本文介紹些 Android音視頻開發中的AudioRecord的使用,案例將會在前面MediaCodec錄製MP4的基礎上進行,使用AudioRecord將音頻數據合成到MP4中,Android音視頻同系列文章如下:

本文的主要內容如下:

  1. AudioRecord 介紹
  2. AudioRecord 生命週期
  3. AudioRecord 音頻數據讀取
  4. 直接緩衝區和字節序(選)
  5. AudioRecord 使用

AudioRecord 介紹#

AudioRecord 是 Android 中用來錄製硬體設備的音頻工具,通過 pulling的方式獲取音頻數據,一般用來獲得原始音頻 PCM格式的數據,可以實現邊錄邊播,多用於音頻數據的即時處理。

創建AudioRecord的參數及說明如下:

// 創建AudioRecord
public AudioRecord (int audioSource, 
                int sampleRateInHz, 
                int channelConfig, 
                int audioFormat, 
                int bufferSizeInBytes)
  • audioSource:表示音頻源,音頻源定義在MediaRecorder.AudioSource中,如常見的音頻源主麥克風MediaRecorder.AudioSource.MIC等。
  • sampleRateInHz:表示以赫茲為單位的採樣率,其含義是每個通道每秒的採樣數,常見採樣率中只有 44100Hz 的採樣率可以保證在所有設備上正常使用,可以通過getSampleRate獲取實際採樣率,這個採樣率不是音頻內容播放的採樣率,比如可以在採樣率為 48000Hz 的設備上播放採樣率為 8000Hz 的聲音,對應平台會自動處理採樣率轉換,因此不會以 6 倍的速度播放。
  • channelConfig:表示聲道數,聲道定義在AudioFormat中,常見的聲道中只有單聲道AudioFormat.CHANNEL_IN_MONO能保證在所有設備上正常使用,其他的比如AudioFormat.CHANNEL_IN_STEREO表示雙聲道,也就是立體聲。
  • audioFormat:表示AudioRecord返回的音頻數據的格式,對於線性 PCM來說,反應每個樣本大小(8、16、32 位)及表現形式(整型、浮點型),音頻格式定義在AudioFormat中,常見的音頻數據格式中只有AudioFormat.ENCODING_PCM_16BIT可以保證在所有的設備上正常使用,像AudioFormat.ENCODING_PCM_8BIT不能保證在所有設備上正常使用。
  • bufferSizeInBytes:表示寫入音頻數據的緩衝區的大小,該值不能小於getMinBufferSize的大小,即不能小於AudioRecord所需的最小緩衝區的大小,否則將導致AudioRecord初始化失敗,該緩衝區大小並不能保證在負載情況下順利錄製,必要時可選擇更大值。

AudioRecord 生命週期#

AudioRecord的生命週期狀態包括 STATE_UNINITIALIZEDSTATE_INITIALIZEDRECORDSTATE_RECORDINGRECORDSTATE_STOPPED,分別對應未初始化、已初始化、錄製中、停止錄製,如下圖所示:

Mermaid Loading...

簡單說明一下:

  1. 未創建之前或者release之後AudioRecord都進入STATE_UNINITIALIZED狀態。
  2. 創建AudioRecord時進入STATE_INITIALIZED狀態。
  3. 調用startRecording進入RECORDSTATE_RECORDING狀態。
  4. 調用stop進入RECORDSTATE_STOPPED狀態。

那麼如何獲取AudioRecord的狀態呢,可以通過getStategetRecordingState獲取其狀態,為保證正確使用可在使用AudioRecord對象操作之前進行其狀態的判斷。

AudioRecord 音頻數據讀取#

AudioRecord 提供的三種讀取音頻數據的方式,如下:

// 1. 讀取音頻數據,音頻格式為AudioFormat#ENCODING_PCM_8BIT
int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)
// 2. 讀取音頻數據,音頻格式為AudioFormat#ENCODING_PCM_16BIT
int read(@NonNull short[] audioData, int offsetInShorts, int sizeInShorts)
// 3. 讀取音頻數據,見後面章節
int read(@NonNull ByteBuffer audioBuffer, int sizeInBytes)

讀取音頻數據的返回值大於等於 0,讀取音頻數據常見異常如下:

  1. ERROR_INVALID_OPERATION:表示AudioRecord 未初始化。
  2. ERROR_BAD_VALUE:表示參數無效。
  3. ERROR_DEAD_OBJECT:表示已經傳輸了一些音頻數據的情況下不返回錯誤碼,將在下次 read返回處返回錯誤碼。

上面三個 read 函數都是從硬體音頻設備讀取音頻數據,前兩個主要的區別就是音頻格式不同,分別是 8 位、16 位,對應的量化級別則是 2^8 和 2^16 量化級別。

第三個read函數在讀取音頻數據時,會將其記錄在直接緩衝區 (DirectBuffer) 中,如果此緩衝區不是 DirectBuffer 則一直返回 0,也就是使用第三個read函數時傳入的參數audioBuffer必須是一個 DirectBuffer,否則不能正確讀取到音頻數據,此時,該Bufferposition將保持不變,緩衝區中的數據的音頻格式則取決於AudioRecord中指定的格式,且字節存放的方式為本機字節序。

直接緩衝區和字節序#

上面提到了兩個概念直接緩衝區和字節序,這裡簡單說明一下:

直接緩衝區#

DirectBuffer 是 NIO 裡面的東西,這裡簡單看下普通緩衝區和直接緩衝區的一些區別。

  • 普通緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

可知普通緩衝區從堆上分配一個字節緩衝區,該緩衝區受 JVM 的管理,意味著在合適的時候是可以被 GC 回收的,GC 回收伴隨著內存的整理,某種程度上對性能是有影響的。

  • 直接緩衝區
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
public static ByteBuffer allocateDirect(int capacity) {
    // Android-changed: Android's DirectByteBuffers carry a MemoryRef.
    // return new DirectByteBuffer(capacity);
    DirectByteBuffer.MemoryRef memoryRef = new DirectByteBuffer.MemoryRef(capacity);
    return new DirectByteBuffer(capacity, memoryRef);
}

上面是 Android 中的DirectBuffer 的實現,可見是從內存中分配的,這種方式獲得的緩衝區的獲取成本是釋放成本都是巨大的,但是可以駐留在垃圾回收堆的外部,一般分配給大型、壽命長的緩衝區,最後分配此緩衝區能夠帶來顯著的性能提升才進行分配,是否是DirectBuffer 可以通過 isDirect來確定。

字節序#

字節序指的是字節在內存中的存放方式,字節序主要分為兩類:BIG-ENDIAN 和 LITTLE-ENDIAN,通俗的稱之為網絡字節序和本機字節序,具體如下:

  • 本機字節序,即 LITTLE-ENDIAN (小字節序、低字節序),即低位字節排放在內存的低地址端,高位字節排放在內存的高地址端,與之對應的還有網絡字節序。
  • 網絡字節序,一般指的是 TCP/IP 協議中使用的字節序,因為 TCP/IP 各層協議將字節序定義為 BIG-ENDIAN,所以網絡字節序一般指的是 BIG-ENDIAN。

AudioRecord 的使用#

記得在前面的文章 Camera2、MediaCodec 錄製 mp4 中只是錄製了視頻,側重於MediaCodec的使用,這裡將在視頻錄製的基礎上使用AudioRecord添加音頻的錄製,並將其合成到MP4文件中,其關鍵步驟如下:

  1. 開啟一個線程使用AudioRecord讀取硬體的音頻數據,開線程可以避免卡頓,文末案例中也有代碼示例,見 AudioEncode2,參考如下:
/**
 * 音頻讀取Runnable
 */
class RecordRunnable : Runnable{
    override fun run() {
        val byteArray = ByteArray(bufferSize)
        // 錄製狀態 -1表示默認狀態,1表述錄製狀態,0表示停止錄製
        while (recording == 1){
            val result = mAudioRecord.read(byteArray, 0, bufferSize)
            if (result > 0){
                val resultArray = ByteArray(result)
                System.arraycopy(byteArray, 0, resultArray, 0, result)
                quene.offer(resultArray)
            }
        }
        // 自定義流結束的數據
        if (recording == 0){
            val stopArray = byteArrayOf((-100).toByte())
            quene.offer(stopArray)
        }
    }
}

這裡提一下,如果只是使用AudioRecord錄製音頻數據,當讀取到音頻數據可將音頻數據寫入文件即可。

  1. 讀取到音頻數據要想合成到MP4中需要先進行音頻數據的編碼,音頻數據編碼器配置如下:
// 音頻數據編碼器配置
private fun initAudioCodec() {
    L.i(TAG, "init Codec start")
    try {
        val mediaFormat =
            MediaFormat.createAudioFormat(
                MediaFormat.MIMETYPE_AUDIO_AAC,
                RecordConfig.SAMPLE_RATE,
                2
            )
        mAudioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
        mediaFormat.setInteger(
            MediaFormat.KEY_AAC_PROFILE,
            MediaCodecInfo.CodecProfileLevel.AACObjectLC
        )
        mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 8192)
        mAudioCodec.setCallback(this)
        mAudioCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    } catch (e: Exception) {
        L.i(TAG, "init error:${e.message}")
    }
    L.i(TAG, "init Codec end")
}

關於編碼也就是MediaCodec的使用可以參考前面下面兩篇文章:

這裡使用MediaCodec的異步處理模式進行音頻數據的編碼,這裡將不貼代碼了,注意一點就是填充和釋放Buffer的時候一定要判斷條件,如果InputBuffer一直不釋放則會導致無可用的InputBuffer使用導致音頻編碼失敗,還有就是流結束的處理。

  1. 文件的合成使用MediaMuxerMediaMuxer在啟動之前必須確保添加好視軌和音軌
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
    L.i(TAG, "onOutputFormatChanged format:${format}")
    // 添加音軌
    addAudioTrack(format)
    // 如果音軌和視軌都添加的情況下才啟動MediaMuxer
    if (RecordConfig.videoTrackIndex != -1) {
        mAudioMuxer.start()
        RecordConfig.isMuxerStart = true
        L.i(TAG, "onOutputFormatChanged isMuxerStart:${RecordConfig.isMuxerStart}")
    }
}
// 添加音軌
private fun addAudioTrack(format: MediaFormat) {
    L.i(TAG, "addAudioTrack format:${format}")
    RecordConfig.audioTrackIndex = mAudioMuxer.addTrack(format)
    RecordConfig.isAddAudioTrack = true
}
// ...

AudioRecord的使用基本如上。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。