本文介紹些 Android
音視頻開發中的AudioRecord
的使用,案例將會在前面MediaCodec
錄製MP4
的基礎上進行,使用AudioRecord
將音頻數據合成到MP4
中,Android
音視頻同系列文章如下:
本文的主要內容如下:
- AudioRecord 介紹
- AudioRecord 生命週期
- AudioRecord 音頻數據讀取
- 直接緩衝區和字節序(選)
- 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_UNINITIALIZED
、STATE_INITIALIZED
、RECORDSTATE_RECORDING
和RECORDSTATE_STOPPED
,分別對應未初始化、已初始化、錄製中、停止錄製,如下圖所示:
簡單說明一下:
- 未創建之前或者
release
之後AudioRecord
都進入STATE_UNINITIALIZED
狀態。 - 創建
AudioRecord
時進入STATE_INITIALIZED
狀態。 - 調用
startRecording
進入RECORDSTATE_RECORDING
狀態。 - 調用
stop
進入RECORDSTATE_STOPPED
狀態。
那麼如何獲取AudioRecord
的狀態呢,可以通過getState
和getRecordingState
獲取其狀態,為保證正確使用可在使用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,讀取音頻數據常見異常如下:
- ERROR_INVALID_OPERATION:表示
AudioRecord
未初始化。 - ERROR_BAD_VALUE:表示參數無效。
- ERROR_DEAD_OBJECT:表示已經傳輸了一些音頻數據的情況下不返回錯誤碼,將在下次
read
返回處返回錯誤碼。
上面三個 read
函數都是從硬體音頻設備讀取音頻數據,前兩個主要的區別就是音頻格式不同,分別是 8 位、16 位,對應的量化級別則是 2^8 和 2^16 量化級別。
第三個read
函數在讀取音頻數據時,會將其記錄在直接緩衝區 (DirectBuffer
) 中,如果此緩衝區不是 DirectBuffer
則一直返回 0,也就是使用第三個read
函數時傳入的參數audioBuffer
必須是一個 DirectBuffer
,否則不能正確讀取到音頻數據,此時,該Buffer
的position
將保持不變,緩衝區中的數據的音頻格式則取決於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
文件中,其關鍵步驟如下:
- 開啟一個線程使用
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
錄製音頻數據,當讀取到音頻數據可將音頻數據寫入文件即可。
- 讀取到音頻數據要想合成到
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
使用導致音頻編碼失敗,還有就是流結束的處理。
- 文件的合成使用
MediaMuxer
,MediaMuxer
在啟動之前必須確保添加好視軌和音軌
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
的使用基本如上。