7、音频录制:MediaRecorder使用、AudioRecord使用、音频数据采集与处理
音频录制,是音视频开发里最基础也最容易被忽视的一环。很多人觉得不就是按个录音键嘛,有什么难的?但真到了项目里,你会发现坑一个接一个。我最早做录音功能时,也踩过不少雷,今天就把这些经验掰开揉碎讲给你听。
Android 提供了两套音频录制方案:MediaRecorder 和 AudioRecord。前者是高层封装,简单易用;后者是底层 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 伺候。我整理了一张状态流转图,帮你理清脉络:
避坑指南:每次录制结束后,记得调用 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