【Android音视频学习之路(二)】AudioRecord API详解及应用

1,986 阅读6分钟

【Android音视频学习之路(一)】如何在 Android 平台绘制一张图片

AudioRecord API

AudioRecord是Android系统提供的用于实现录音的功能类,主要功能就是让各种java应用来管理音视频资源,通过此类可以录制相关硬件所收集的声音。

通过事前设定好的声音存储格式,然后读取AudioRecord对象的声音数据来完成操作。

开始录音的时候,AudioRecord需要初始化一个相关联的声音buffer, 这个buffer 主要是用来保存新的声音数据。这个buffer的大小,我们可以在对象构造期间去指定。它表明一个 AudioRecord对象还没有被读取(同步)声音数据前能录多长的音(即一次可以录制的声音容量)。声音数 据从音频硬件中被读出,数据大小不超过整个录音数据的大小(可以分多次读出),即每次读取初始化 buffer容量的数据。

graph TD
构造AudioRecord对象,最小录音缓存存buffer大小通过getMinBufferSize方法得到 --> 初始化一个buffer,该buffer大于等于AudioRecord对象用于写声音数据的buffer大小 -->  开始录音 --> 创建数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流 --> 关闭数据流 --> 停止录音

使用 AudioRecord 实现录音,并生成wav

创建一个AudioRecord对象

1.首先要声明一些全局的变量参数:

 private var audioRecord: AudioRecord? = null
 private var recordBufSize: Int = 0
 private val frequency = 44100
 private val channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO
 private val encodingBitRate = AudioFormat.ENCODING_PCM_16BIT
 private var isRecording = false
 private var audioFilePath = ""
 private var recordingThread: Job? = null //录制音频的协程
函数原型:

public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)

作用:

返回成功创建AudioRecord对象所需要的最小缓冲区大小

参数:

sampleRateInHz: 默认采样率,单位Hz,这里设置为44100,44100Hz是当前唯一能保证在所有设备上工作的采样率;

channelConfig: 描述音频声道设置,这里设置为AudioFormat.CHANNEL_CONFIGURATION_MONO,CHANNEL_CONFIGURATION_MONO保证能在所有设备上工作;

audioFormat: 音频数据的采样精度,这里设置为AudioFormat.ENCODING_16BIT;

返回值:

返回成功创建AudioRecord对象所需要的最小缓冲区大小。 注意:这个大小并不保证在负荷下的流畅录制,应根据预期的频率来选择更高的值,AudioRecord实例在推送新数据时使用此值

如果硬件不支持录制参数,或输入了一个无效的参数,则返回ERROR_BAD_VALUE(-2),如果硬件查询到输出属性没有实现,或最小缓冲区用byte表示,则返回ERROR(-1)

函数原型:

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes) throws IllegalArgumentException

参数:

audioSource: 录制源,这里设置MediaRecorder.AudioSource.MIC,其他请见MediaRecorder.AudioSource录制源定义,比如MediaRecorder.AudioSource.FM_TUNER等;

sampleRateInHz: 默认采样率,单位Hz,这里设置为44100,44100Hz是当前唯一能保证在所有设备上工作的采样率;

channelConfig: 描述音频声道设置,这里设置为AudioFormat.CHANNEL_CONFIGURATION_MONO,CHANNEL_CONFIGURATION_MONO保证能在所有设备上工作;

audioFormat: 音频数据保证支持此格式,这里设置为AudioFormat.ENCODING_16BIT;

bufferSizeInBytes: 在录制过程中,音频数据写入缓冲区的总数(字节),即getMinVufferSize()获取到的值。

异常:

当参数设置不正确或不支持的参数时,将会抛出IllegalArgumentException

2.获取buffer的大小并创建AudioRecord:

  recordBufSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, encodingBitRate)
  audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration,
                encodingBitRate,recordBufSize)

3.初始化一个buffer

  val buffer = ByteArray(recordBufSize)

4.开始录音

audioRecord.startRecording()
isRecording = true

5.创建一个数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流

  audioFilePath = Environment.getExternalStorageDirectory().absolutePath + File.separator + "test.wav"
  val audioFile = File(audioFilePath)
  if (audioFile.exists()) audioFile.delete()

  val fileOutputStream = FileOutputStream(audioFile)
  fileOutputStream.use {
      while (isRecording) {
          audioRecord.read(buffer, 0, recordBufSize)
          it.write(buffer)
      }
  }

6.关闭数据流

修改标志位:isRecording 为false,上面的while循环就自动停止了,数据流也就停止流动了,Stream 也就被关闭了。

isRecording = false;

7.停止录音

停止录音之后,注意要释放资源。

 isRecording = false
 audioRecord?.stop()
 audioRecord?.release()
 audioRecord = null
 recordingThread?.cancel()

注:权限需求:WRITE_EXTERNAL_STORAGE、RECORD_AUDIO

到现在基本的录音的流程就介绍完了。但是这时候,有人就提出问题来了:

1. 我按照流程,把音频数据都输出到文件里面了,停止录音后,打开此文件,发现不能播放,到底是为什么呢?

答:按照流程走完了,数据是进去了,但是现在的文件里面的内容仅仅是最原始的音频数据,术语称为 raw(中文解释是“原材料”或“未经处理的东西”),这时候,你让播放器去打开,它既不知道保存的格 式是什么,又不知道如何进行解码操作。当然播放不了。 2. 那如何才能在播放器中播放我录制的内容呢?

答: 在文件的数据开头加入WAVE HEAD数据即可,也就是文件头。只有加上文件头部的数据,播放器 才能正确的知道里面的内容到底是什么,进而能够正常的解析并播放里面的内容。具体的头文件的描 述,在Play a WAV file on an AudioTrack里面可以进行了解。

添加WAVE文件头的代码如下:

/**
 * @param bufferSize 缓存的音频大小
 * @param sampleRate  采样率
 * @param channel 声道数
 */
class PcmToWavUtil(
    private val bufferSize: Int,
    private val sampleRate: Long,
    private val channel: Int
) {

    /**
     * pcm文件转wav文件
     *
     * @param inFilename 源文件路径
     * @param outFilename 目标文件路径
     */
    fun pcmToWav(inFilename: String,outFilename: String) {
        val inputStream =  FileInputStream(inFilename)
        val outputStream = FileOutputStream(outFilename)
        val totalAudioLen = inputStream.channel.size()
        val totalDataLen = totalAudioLen + 36
        val longSampleRate: Long = sampleRate
        val channels = if (channel == AudioFormat.CHANNEL_IN_MONO) 1 else 2

        val byteRate: Long = 16 * sampleRate * channels / 8
        val data: ByteArray = ByteArray(bufferSize)

        writeWaveFileHeader(outputStream, totalAudioLen, totalDataLen ,longSampleRate, channels, byteRate)
        inputStream.use { input ->
            outputStream.use { output ->
                while (input.read(data) != -1) {
                    output.write(data)
                }
            }
        }
    }

    /**
     *  加入wav文件头
     */
    @Throws(IOException::class)
    private fun writeWaveFileHeader(
        outputStream: FileOutputStream,
        totalAudioLen: Long,
        totalDataLen: Long,
        longSampleRate: Long,
        channels: Int,
        byteRate: Long
    ) {
        val header: ByteArray = ByteArray(44)
        // RIFF/WAVE header
        header[0] = 'R'.toByte()
        header[1] = 'I'.toByte()
        header[2] = 'F'.toByte()
        header[3] = 'F'.toByte()
        header[4] = ((totalDataLen and 0xff).toByte())
        header[5] = (totalDataLen shr 8 and 0xff).toByte()
        header[6] = (totalDataLen shr 16 and 0xff).toByte()
        header[7] = (totalDataLen shr 24 and 0xff).toByte()
        //WAVE
        header[8] = 'W'.toByte()
        header[9] = 'A'.toByte()
        header[10] = 'V'.toByte()
        header[11] = 'E'.toByte()
        // 'fmt ' chunk
        header[12] = 'f'.toByte()
        header[13] = 'm'.toByte()
        header[14] = 't'.toByte()
        header[15] = ' '.toByte()
        // 4 bytes: size of 'fmt ' chunk
        header[16] = 16
        header[17] = 0
        header[18] = 0
        header[19] = 0
        // format = 1
        header[20] = 1
        header[21] = 0
        header[22] = channels.toByte()
        header[23] = 0
        header[24] = ((longSampleRate and 0xff).toByte())
        header[25] = ((longSampleRate shr 8 and 0xff).toByte())
        header[26] = ((longSampleRate shr 16 and 0xff).toByte())
        header[27] = ((longSampleRate shr 24 and 0xff).toByte())
        header[28] = (byteRate and 0xff).toByte()
        header[29] = (byteRate shr 8 and 0xff).toByte()
        header[30] = (byteRate shr 16 and 0xff).toByte()
        header[31] = (byteRate shr 24 and 0xff).toByte()
        // block align
        header[32] = (2 * 16 / 8).toByte()
        header[33] = 0
        // bits per sample
        header[34] = 16
        header[35] = 0
        //data
        header[36] = 'd'.toByte()
        header[37] = 'a'.toByte()
        header[38] = 't'.toByte()
        header[39] = 'a'.toByte()
        header[40] = (totalAudioLen and 0xff).toByte()
        header[41] = (totalAudioLen shr 8 and 0xff).toByte()
        header[42] = (totalAudioLen shr 16 and 0xff).toByte()
        header[43] = (totalAudioLen shr 24 and 0xff).toByte()
        outputStream.write(header, 0, 44)

    }

}

源码地址

后言

Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加 上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文 件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。如果想简 单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder,而如果需要对音频做进一步的算法 处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互 的。直播中实时采集音频自然是要用 AudioRecord 了。