4、音频采集入门:AudioRecord使用、PCM数据格式、音频缓冲区管理、采样率与声道设置

音频采集,说白了就是让手机麦克风把声音变成数字信号。很多初学者一上来就怼着摄像头折腾视频,结果发现录出来的视频没声音,或者声音断断续续、有杂音。嗯,这多半是音频采集没处理好。

我个人习惯把音频采集比作「水龙头接水」——麦克风是水源,AudioRecord 是水管,缓冲区就是水桶。水桶太小会溢出来,太大又浪费。咱们今天就把这套流程彻底讲透。

核心知识点一览:

  • AudioRecord 的初始化与生命周期
  • PCM 原始音频数据长什么样
  • 缓冲区大小怎么算、怎么调
  • 采样率和声道到底怎么选
音频采集核心流程 麦克风(硬件) AudioRecord 缓冲区 PCM 关键配置参数 采样率(44100Hz) 声道(单声道) 位深(16bit) 麦克风 → AudioRecord → 缓冲区 → PCM 原始数据 → 后续编码/推流 缓冲区大小 = 最小缓冲区 × 2(推荐)

4.1 AudioRecord 的初始化与生命周期

AudioRecord 是 Android 官方提供的音频采集 API。它不负责编码,只负责拿原始 PCM 数据。我刚开始用的时候,总以为它跟 MediaRecorder 一样能直接出文件,结果折腾半天发现只有一堆 byte 数组——嗯,这就是 PCM。

初始化 AudioRecord 需要五个参数:

int sampleRate = 44100;          // 采样率,单位 Hz
int channelConfig = AudioFormat.CHANNEL_IN_MONO;  // 单声道
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位位深
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.MIC,  // 麦克风音源
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize * 2  // 缓冲区取最小值的2倍,更稳定
);

个人经验:bufferSize 我建议取 getMinBufferSize() 返回值的 2 倍。取最小值的话,系统压力大时容易丢数据,录出来的声音会「卡顿」。我在一个直播项目里吃过这个亏,后来改成 2 倍就稳了。

AudioRecord 的生命周期很简单:

  • 初始化:new 出来,检查状态是否为 STATE_INITIALIZED
  • 开始采集:调用 startRecording()
  • 读取数据:循环调用 read() 方法,把数据塞进缓冲区
  • 停止采集:调用 stop()
  • 释放资源:调用 release()

注意:startRecording() 和 stop() 必须在子线程调用,不能在主线程里搞。我曾经在面试时问过候选人这个问题,好几个人答不上来——AudioRecord 的 read() 是阻塞的,放主线程会 ANR。

4.2 PCM 数据格式——最原始的音频

PCM 就是脉冲编码调制,说白了就是麦克风把声波的振幅每隔一小段时间「拍个照」,记录成数字。你想想看,一张照片是一帧画面,PCM 的一个采样点就是一帧声音。

咱们常用的配置是 16 位位深、单声道。16 位意味着每个采样点占 2 个字节,取值范围是 -32768 到 32767。0 代表静音,正数代表声波正半周,负数代表负半周。

举个例子:采样率 44100Hz、单声道、16 位,每秒产生的数据量是:

44100 采样点/秒 × 2 字节/采样点 × 1 声道 = 88200 字节/秒 ≈ 86 KB/s

这个数据量其实不小。录一分钟就是 5MB 左右,纯原始数据,没压缩。所以后面咱们要编码成 AAC 或者 MP3,不然存本地都费劲。

避坑指南:我曾经在解析 PCM 数据时,直接把 byte 数组当成 short 数组来用,忘了 Android 是小端序。结果波形全是反的,调试了半天才发现是字节序的问题。如果你要手动处理 PCM,记得用 ByteBuffer 并设置 ByteOrder.LITTLE_ENDIAN。

4.3 音频缓冲区管理——别让水桶溢出来

缓冲区管理是音频采集里最容易出问题的地方。缓冲区太小,read() 调用频繁,CPU 开销大;缓冲区太大,延迟高,直播场景下口型对不上。

我一般遵循这个原则:

  • 非实时场景(比如本地录音):缓冲区取 getMinBufferSize() 的 4 倍,稳定优先
  • 实时场景(比如直播推流):缓冲区取 getMinBufferSize() 的 2 倍,延迟和稳定性折中
  • 低延迟场景(比如 K 歌):缓冲区取 getMinBufferSize() 的 1 倍,但要做好丢包处理

读取数据的典型循环:

byte[] buffer = new byte[bufferSize];
while (isRecording) {
    int bytesRead = audioRecord.read(buffer, 0, buffer.length);
    if (bytesRead > 0) {
        // 把 buffer 前 bytesRead 个字节送去编码或推流
        // 注意:bytesRead 可能小于 buffer.length,别把脏数据送出去
    } else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
        // 操作错误,建议重启 AudioRecord
    }
}

小技巧:read() 返回 -1 表示读取失败,返回 0 表示没有数据。我习惯在每次 read() 之后检查返回值,如果连续 3 次返回 0 或负数,就主动重启 AudioRecord。这个逻辑救过我一次线上事故——用户手机麦克风被占用时,程序不会直接崩溃,而是自动恢复。

4.4 采样率与声道设置——选对了事半功倍

采样率和声道是音频采集的两个核心参数。选错了,轻则音质差,重则设备不支持直接报错。

采样率:

  • 44100Hz:CD 音质,兼容性最好,几乎所有 Android 设备都支持。我建议无脑选这个。
  • 48000Hz:DVD 音质,部分设备支持,常用于专业录音。
  • 8000Hz / 16000Hz:电话音质,语音通话场景用,省带宽。

声道:

  • CHANNEL_IN_MONO(单声道):一个麦克风采集,数据量小,处理简单。直播推流首选。
  • CHANNEL_IN_STEREO(双声道):两个麦克风,有立体感,但数据量翻倍。除非你做音乐录制,否则不推荐。
场景 推荐采样率 推荐声道 位深
直播推流 44100Hz 单声道 16bit
语音通话 16000Hz 单声道 16bit
音乐录制 48000Hz 双声道 16bit 或 24bit
本地录音笔记 44100Hz 单声道 16bit

重要提醒:不是所有设备都支持 48000Hz 或双声道。初始化之前,最好用 getMinBufferSize() 测试一下,如果返回负数或抛出异常,就降级到 44100Hz 单声道。我在适配某款低端平板时就遇到过这个问题,直接崩溃了,后来加了降级逻辑才搞定。

最后说一句,音频采集是整个音视频 pipeline 的起点。起点稳了,后面编码、推流才顺。我见过太多人一上来就搞编码推流,结果音频数据本身就有问题,折腾半天找不到原因。先把 AudioRecord 玩明白,后面的事就简单了。


公众号:蓝海资料掘金营,微信deep3321