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的使用基本如上。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。