7、音频录制:MediaRecorder使用、AudioRecord使用、音频数据采集与处理

音频录制,是音视频开发里最基础也最容易被忽视的一环。很多人觉得不就是按个录音键嘛,有什么难的?但真到了项目里,你会发现坑一个接一个。我最早做录音功能时,也踩过不少雷,今天就把这些经验掰开揉碎讲给你听。

Android 提供了两套音频录制方案:MediaRecorderAudioRecord。前者是高层封装,简单易用;后者是底层 API,灵活可控。说白了,一个适合快速实现,一个适合深度定制。

核心要点:MediaRecorder 输出的是编码后的文件(如 MP3、AAC),而 AudioRecord 输出的是裸 PCM 数据。你需要根据场景选对工具。

7.1 MediaRecorder:开箱即用的录音方案

如果你只是想快速实现一个录音功能,比如语音备忘录、录音笔记,MediaRecorder 是最省事的。它内部封装了音频采集、编码、文件写入的全流程。

我习惯把它比作「傻瓜相机」—— 你只需要设置好参数,调用 start() 和 stop() 就行。但傻瓜相机也有它的脾气,比如状态机管理就很容易翻车。

基本使用流程

MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
recorder.setAudioSamplingRate(44100);
recorder.setAudioBitRate(128000);
recorder.setOutputFile("/sdcard/recording.mp4");

try {
    recorder.prepare();
    recorder.start();
} catch (IOException e) {
    e.printStackTrace();
}

嗯,这里要注意:setAudioSource() 必须在 setOutputFormat() 之前调用。这是 MediaRecorder 的状态机要求,顺序错了会直接抛异常。我曾经在重构代码时不小心调换了这两行的顺序,排查了半天才发现。

状态机与生命周期

MediaRecorder 内部维护了一个严格的状态机。你想想看,如果状态不对就调用方法,它会直接 IllegalStateException 伺候。我整理了一张状态流转图,帮你理清脉络:

MediaRecorder 状态机流转图 Initial Initialized Prepared Recording Released setAudioSource() prepare() start() stop() release()

避坑指南:每次录制结束后,记得调用 reset() 让 MediaRecorder 回到 Initial 状态,否则无法开始下一次录制。我曾经在循环录制场景中忘了 reset,结果第二次 start() 直接崩溃。

7.2 AudioRecord:更底层的音频采集

当你不满足于 MediaRecorder 的黑盒操作时,就该 AudioRecord 登场了。它直接操作音频缓冲区,让你拿到最原始的 PCM 数据。说白了,MediaRecorder 给你的是「成品」,AudioRecord 给你的是「原料」。

我做过一个实时音频特效处理 App,就是用 AudioRecord 采集数据,然后做 FFT 变换、添加混响。这种场景下 MediaRecorder 完全无能为力。

初始化与参数配置

int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.MIC,
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize
);

这里有个关键点:bufferSize 必须用 getMinBufferSize() 获取,不要自己拍脑袋写一个固定值。不同设备的底层缓冲区大小不一样,写死了很容易出现音频卡顿或崩溃。

数据读取与处理

byte[] audioData = new byte[bufferSize];
audioRecord.startRecording();

while (isRecording) {
    int bytesRead = audioRecord.read(audioData, 0, bufferSize);
    if (bytesRead > 0) {
        // 在这里处理 PCM 数据
        processAudioData(audioData, bytesRead);
    }
}

audioRecord.stop();
audioRecord.release();

你想想看,read() 方法返回的是实际读取的字节数,不一定等于 bufferSize。所以处理数据时一定要用 bytesRead,而不是 bufferSize。我见过有人直接用 bufferSize 做循环,结果数据尾部全是垃圾值。

重要提醒:AudioRecord 的 read() 是阻塞调用。如果你在 UI 线程里调用,界面会卡死。务必放到子线程中执行。

7.3 音频数据采集与处理实战

拿到 PCM 数据后,真正的挑战才开始。PCM 是裸数据,不能直接播放,也不能直接保存。你需要做两件事:格式转换编码压缩

PCM 转 WAV

WAV 格式就是在 PCM 数据前面加一个 44 字节的文件头。我手写过一个工具类,专门做这个转换:

private void writeWavHeader(OutputStream out, int sampleRate, int channels, int bitsPerSample, int dataSize) throws IOException {
    int byteRate = sampleRate * channels * bitsPerSample / 8;
    int blockAlign = channels * bitsPerSample / 8;

    out.write("RIFF".getBytes());
    out.write(intToByteArray(36 + dataSize), 0, 4);
    out.write("WAVE".getBytes());
    out.write("fmt ".getBytes());
    out.write(intToByteArray(16), 0, 4);  // Subchunk1Size
    out.write(shortToByteArray((short) 1), 0, 2);  // AudioFormat (PCM)
    out.write(shortToByteArray((short) channels), 0, 2);
    out.write(intToByteArray(sampleRate), 0, 4);
    out.write(intToByteArray(byteRate), 0, 4);
    out.write(shortToByteArray((short) blockAlign), 0, 2);
    out.write(shortToByteArray((short) bitsPerSample), 0, 2);
    out.write("data".getBytes());
    out.write(intToByteArray(dataSize), 0, 4);
}

嗯,这个头部的每个字段都有严格定义。我最早写的时候把 Subchunk1Size 写成了 18,结果生成的 WAV 文件在 Windows 上能播,在 Mac 上就报错。后来查规范才发现必须是 16。

实时音量检测

很多录音 App 需要显示音量波形。其实原理很简单:对 PCM 数据取绝对值,然后求平均值:

public double calculateVolume(byte[] audioData, int bytesRead) {
    long sum = 0;
    for (int i = 0; i < bytesRead; i += 2) {
        short sample = (short) ((audioData[i + 1] << 8) | (audioData[i] & 0xFF));
        sum += Math.abs(sample);
    }
    return sum / (double) (bytesRead / 2);
}

这里要注意字节序问题。Android 的 PCM 数据默认是小端序(Little Endian),高位字节在后。如果你按大端序解析,音量值会完全不对。

7.4 两种方案的对比与选型

对比维度 MediaRecorder AudioRecord
使用难度 简单,几行代码搞定 中等,需手动管理缓冲区
输出格式 编码后的文件(MP3/AAC) 裸 PCM 数据
灵活性 低,无法干预编码过程 高,可做实时处理
适用场景 语音备忘录、录音笔记 实时特效、语音识别、自定义编码
性能开销 低(硬件编码) 中(需自行处理数据)

我个人习惯是:能用 MediaRecorder 就用 MediaRecorder。除非你需要对音频数据做实时处理,或者要输出非标准格式,否则没必要自己造轮子。毕竟 Android 团队已经帮你优化好了编码器,何必重复发明轮子呢?

经验之谈:如果你用 AudioRecord 做实时处理,建议把读取和处理放在两个线程里,中间用队列缓冲。这样即使处理逻辑变慢,也不会阻塞音频采集,避免出现「爆音」。

音频录制这块,说白了就是「采集 → 处理 → 输出」三个环节。MediaRecorder 帮你把后两步打包了,AudioRecord 则让你全程掌控。选哪个,取决于你的需求有多「野」。我见过有人用 AudioRecord 做声纹识别,也有人用 MediaRecorder 做简单的录音笔 App,都没问题。

最后提醒一句:别忘了在 AndroidManifest 里申请 RECORD_AUDIO 权限。而且 Android 6.0 以上需要动态申请。这个坑我踩过不止一次,每次都是「明明代码没问题,怎么就是录不了音?」—— 结果一看,权限没给。


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