3、Android AudioRecord与音频采集:AudioRecord基础用法、音频数据格式与缓冲区、音频采集实战

音频采集,是音视频传输的起点。你想想看,如果源头的声音数据都拿不好,后面编码、推流全是白搭。这一章,我们就来啃下 Android 上最核心的音频采集 API——AudioRecord

说实话,我早年刚接触音视频开发时,也踩过不少 AudioRecord 的坑。比如缓冲区设太小导致丢帧,或者采样率不匹配导致声音变调。嗯,今天咱们就把这些经验一次性说透。

3.1 AudioRecord 是什么?

AudioRecord 是 Android 提供的原生音频采集类。它直接从硬件麦克风读取 PCM 裸数据,不经过任何编码。说白了,它给你的就是最原始的声音样本。

为什么不用 MediaRecorder?因为 MediaRecorder 直接输出编码后的文件(如 MP3、AAC),你拿不到原始数据。而做 RTMP 推流,我们需要自己控制编码和封装,所以必须用 AudioRecord。

核心要点: AudioRecord 输出的是 PCM 数据,这是后续编码(AAC/Opus)的原材料。

3.2 AudioRecord 基础用法

使用 AudioRecord 其实就三步:初始化、启动采集、读取数据。但每一步都有讲究。

3.2.1 初始化参数

构造函数长这样:

AudioRecord(
    int audioSource,      // 音频源,一般用 MediaRecorder.AudioSource.MIC
    int sampleRateInHz,   // 采样率,如 44100、48000
    int channelConfig,    // 声道配置,如 CHANNEL_IN_MONO
    int audioFormat,      // 音频格式,如 ENCODING_PCM_16BIT
    int bufferSizeInBytes // 缓冲区大小
)

这里我重点说一下几个参数的选择:

  • audioSource:大部分场景用 MIC 就行。如果需要消除回声,可以用 VOICE_COMMUNICATION,它会启用内置的 AEC(声学回声消除)。
  • sampleRateInHz:44100Hz 是 CD 音质,48000Hz 是专业级。我个人习惯用 44100,兼容性最好。但如果你要做高清语音,48000 更合适。
  • channelConfig:手机麦克风通常是单声道,所以用 CHANNEL_IN_MONO。双声道采集需要两个麦克风,大部分手机不支持。
  • audioFormatENCODING_PCM_16BIT 是主流,每个采样点占 2 字节。16 位精度足够,再高意义不大。
我的经验: 采样率和声道数一旦确定,后续编码器参数必须与之匹配。比如你采集 44100Hz 单声道,编码 AAC 时也要设成 44100Hz 单声道,否则声音会变调。

3.2.2 缓冲区大小

缓冲区大小不能随便设。Android 提供了一个工具方法:

int minBufferSize = AudioRecord.getMinBufferSize(
    44100,
    AudioFormat.CHANNEL_IN_MONO,
    AudioFormat.ENCODING_PCM_16BIT
);

这个 getMinBufferSize 返回的是系统建议的最小缓冲区。你实际使用时,可以取这个值的 2 倍或 4 倍。为什么?

我曾经在一个低端设备上遇到过问题:缓冲区设得太小,导致读取线程频繁阻塞,最终丢帧。后来我把缓冲区放大到 4 倍,问题就解决了。说白了,缓冲区大一点,抗抖动能力更强,但延迟也会增加。你需要根据场景权衡。

注意: 如果传入的缓冲区小于 getMinBufferSize,AudioRecord 初始化会直接抛异常。所以千万别自作聪明设一个很小的值。

3.3 音频数据格式:PCM 详解

AudioRecord 输出的 PCM 数据,本质上就是一堆字节。但你要理解它的结构,才能正确处理。

以 16 位单声道为例:

  • 每个采样点用 2 个字节表示(小端序)
  • 每秒有 44100 个采样点
  • 所以每秒数据量 = 44100 × 2 = 88200 字节

如果是双声道 16 位:

  • 每个采样点用 4 个字节(左声道 2 字节 + 右声道 2 字节)
  • 每秒数据量 = 44100 × 4 = 176400 字节

你想想看,如果缓冲区大小是 4096 字节,那么一次 read() 最多能读到 4096 字节,也就是约 46 毫秒的音频数据(单声道 44100Hz)。这个时间窗口很重要,后面做实时推流时会用到。

计算公式: 一次读取的时长(秒) = 缓冲区大小(字节) / (采样率 × 声道数 × 位深/8)

3.4 音频采集实战:完整代码

下面是一个完整的 AudioRecord 采集示例。我把它封装成了一个工具类,方便复用。

public class AudioCapturer {
    private AudioRecord audioRecord;
    private boolean isRecording = false;
    private int bufferSize;
    private int sampleRate = 44100;

    public void startCapture() {
        bufferSize = AudioRecord.getMinBufferSize(
            sampleRate,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT
        ) * 2;  // 我习惯用 2 倍缓冲区

        audioRecord = new AudioRecord(
            MediaRecorder.AudioSource.MIC,
            sampleRate,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSize
        );

        if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            throw new RuntimeException("AudioRecord 初始化失败");
        }

        audioRecord.startRecording();
        isRecording = true;

        new Thread(new Runnable() {
            @Override
            public void run() {
                byte[] audioData = new byte[bufferSize];
                while (isRecording) {
                    int bytesRead = audioRecord.read(audioData, 0, bufferSize);
                    if (bytesRead > 0) {
                        // 这里拿到 PCM 数据,可以送去编码或推流
                        onAudioData(audioData, bytesRead);
                    }
                }
            }
        }).start();
    }

    public void stopCapture() {
        isRecording = false;
        if (audioRecord != null) {
            audioRecord.stop();
            audioRecord.release();
            audioRecord = null;
        }
    }

    private void onAudioData(byte[] data, int size) {
        // 回调给上层处理
    }
}

这段代码有几个关键点:

  • 读取操作放在子线程,不能阻塞主线程
  • read() 是阻塞的,直到有数据或超时
  • 每次读取后要判断 bytesRead,可能小于缓冲区大小
避坑指南: 我曾经遇到过 read() 返回 -1 的情况,原因是麦克风权限没给。所以记得在 AndroidManifest 中添加 RECORD_AUDIO 权限,并且在运行时动态申请。

3.5 知识体系图

下面这张图展示了 AudioRecord 音频采集的完整流程和关键参数:

AudioRecord 音频采集知识体系 初始化参数 缓冲区大小 PCM 数据格式 读取线程 audioSource: MIC sampleRate: 44100 channel: MONO format: PCM_16BIT getMinBufferSize() 建议值 × 2 或 × 4 抗抖动 vs 低延迟 平衡选择 16位 = 2字节/采样 小端序存储 每秒 88200 字节 (44100×2) 子线程运行 阻塞式 read() 判断 bytesRead 权限检查 输出:PCM 裸数据 → 编码器/推流 图:AudioRecord 音频采集流程与关键参数

3.6 常见问题与避坑

最后,我总结几个实际开发中容易踩的坑:

  1. 权限问题:Android 6.0 以上需要动态申请 RECORD_AUDIO 权限。我见过不少新手在真机上跑不起来,就是因为忘了这个。
  2. 设备兼容性:有些低端设备不支持 48000Hz 采样率。建议先用 getMinBufferSize 测试一下,如果返回负数,说明不支持。
  3. 内存泄漏:AudioRecord 使用完后一定要 release(),否则会一直占用麦克风资源。最好在 onPause()onDestroy() 中释放。
  4. 数据对齐:PCM 数据是连续的字节流,没有帧边界。如果你要分片处理,必须自己维护缓冲区,确保数据完整。
重要提醒: 不要在 read() 返回后直接修改 audioData 数组的内容,除非你已经拷贝了一份。因为下一次 read() 会覆盖原数据。

好了,这一章的内容就到这里。AudioRecord 虽然基础,但它是整个音视频传输的基石。把这一章吃透,后面编码和推流才能走得稳。


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