3、AudioRecord基础:创建、配置与数据读取
好,咱们进入第三章。这一章我打算把 AudioRecord 的底裤扒干净。
你可能会想:「AudioRecord 不就是 new 一下,然后 startRecording 吗?」
嗯,表面上看确实是这样。但实际项目中,很多人栽就栽在「创建配置」和「数据读取」这两个看似简单的环节上。我见过太多人因为 buffer 大小没设对,导致录音延迟高得离谱;也有人因为格式转换没处理好,音频数据直接变成噪音。
今天咱们就把这些坑一个一个填平。
3.1 AudioRecord 的创建与配置
先看最基础的——怎么创建一个 AudioRecord 实例。
// 最基本的创建方式
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
);
这段代码看起来简单,但每个参数都有讲究。我一个个说。
3.1.1 音频源的选择
第一个参数是 MediaRecorder.AudioSource。很多人直接用 MIC,但实际项目中,我建议你根据场景选:
| 音频源 | 适用场景 | 个人建议 |
|---|---|---|
| MIC | 通用录音 | 默认选项,但延迟不是最优 |
| VOICE_RECOGNITION | 语音识别 | 系统会做降噪处理,适合语音输入 |
| UNPROCESSED | 低延时场景 | 我个人做低延时项目时首选,绕过所有后处理 |
| VOICE_COMMUNICATION | VoIP通话 | 系统会优化回声消除,但会增加延迟 |
3.1.2 采样率与通道配置
采样率这块,我踩过坑。以前有个项目,我设了 48000Hz,结果在某些低端手机上直接创建失败。
为什么会这样?因为不是所有设备都支持所有采样率。安全做法是:
- 先用
AudioRecord.getMinBufferSize()检测是否支持 - 如果返回负数或
ERROR_BAD_VALUE,降级到 44100Hz - 再不行就 16000Hz
通道配置方面,我个人习惯用 CHANNEL_IN_MONO。为什么?
- 双通道数据量翻倍,处理压力大
- 大部分手机麦克风本身就是单声道
- 低延时场景下,单声道足够用
3.1.3 BufferSize 的玄机
这个 bufferSize 是重中之重。很多人直接写死 1024 或 4096,这是大忌。
getMinBufferSize() 获取系统推荐值。
我曾经有个项目,为了「降低延迟」,把 buffer 设得很小。结果呢?录音数据频繁丢失,声音断断续续。后来才发现,buffer 太小会导致底层驱动来不及填充数据,直接丢帧。
那是不是 buffer 越大越好?也不是。buffer 越大,延迟越高。这是个 trade-off:
| Buffer 大小 | 优点 | 缺点 |
|---|---|---|
| 偏小(接近最小值) | 延迟低 | 容易丢数据,CPU 压力大 |
| 偏大(2倍最小值) | 数据稳定 | 延迟高,不适合实时场景 |
我的做法是:用 getMinBufferSize() 的值,然后乘以 1.5 到 2 倍。既保证稳定性,又不会太延迟。
3.2 读取回调与循环读取
创建完 AudioRecord,接下来就是读数据了。两种方式:回调模式和循环读取模式。
3.2.1 回调模式(OnRecordPositionUpdateListener)
这种方式适合「被动接收」数据的场景。你设置一个监听器,系统有数据了就通知你。
audioRecord.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
@Override
public void onMarkerReached(AudioRecord recorder) {
// 到达标记位置时触发,一般用不到
}
@Override
public void onPeriodicNotification(AudioRecord recorder) {
// 周期性通知,可以在这里读数据
int bytesRead = recorder.read(buffer, 0, buffer.length);
// 处理数据...
}
});
// 设置通知周期(必须设置,否则不会回调)
audioRecord.setPositionNotificationPeriod(1024);
说实话,我个人不太喜欢回调模式。原因有二:
- 回调线程是系统内部的,你没法控制优先级
- 如果处理慢了,会阻塞底层,导致丢数据
3.2.2 循环读取模式(推荐)
这才是我的主力方式。自己开一个线程,循环读数据。
class AudioCaptureThread extends Thread {
private volatile boolean isRunning = true;
private byte[] buffer = new byte[bufferSize];
@Override
public void run() {
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_URGENT_AUDIO
);
while (isRunning) {
int bytesRead = audioRecord.read(buffer, 0, buffer.length);
if (bytesRead > 0) {
// 把数据交给处理队列
processAudioData(buffer, bytesRead);
} else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
// 录音状态异常,退出
break;
}
}
}
public void stopCapture() {
isRunning = false;
}
}
THREAD_PRIORITY_URGENT_AUDIO,这样系统会优先调度这个线程,减少读取延迟。
这里有个细节:read() 方法是阻塞的。如果没数据,它会一直等。所以不用担心 while 循环空转消耗 CPU。
3.2.3 读取方式的对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| 回调模式 | 代码简单,不用自己管理线程 | 线程不可控,容易丢数据 |
| 循环读取 | 完全可控,适合低延时 | 需要自己管理线程生命周期 |
| 直接读取(非阻塞) | 可以配合 Selector 使用 | 需要轮询,CPU 占用高 |
我个人强烈推荐循环读取模式。你想想看,低延时场景下,每一毫秒都很关键。把线程控制权握在自己手里,心里踏实。
3.3 音频数据的格式转换
好,数据读出来了。但读出来的是原始 PCM 数据,很多时候需要转换格式。
3.3.1 PCM 数据格式
AudioRecord 读出来的数据,默认是 ENCODING_PCM_16BIT。什么意思?每个采样点用 2 个字节(short)表示,范围是 -32768 到 32767。
但有些场景需要其他格式:
- WebRTC 处理:需要 16-bit 有符号 short 数组
- 音频编码(如 AAC):需要 byte 数组
- 浮点运算:需要 float 数组(归一化到 -1.0 ~ 1.0)
3.3.2 byte[] 转 short[]
这是最常见的转换。因为 read() 返回的是 byte 数组,但很多算法库要 short 数组。
public short[] byteToShort(byte[] audioData, int bytesRead) {
int sampleCount = bytesRead / 2;
short[] shortData = new short[sampleCount];
ByteBuffer.wrap(audioData, 0, bytesRead)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.get(shortData);
return shortData;
}
ByteBuffer,一定要设 ByteOrder.nativeOrder()。我见过有人直接强转,结果在高通和联发科设备上表现不一样。
3.3.3 short[] 转 float[]
做音频算法时,浮点运算更方便。比如滤波、FFT 这些。
public float[] shortToFloat(short[] shortData) {
float[] floatData = new float[shortData.length];
for (int i = 0; i < shortData.length; i++) {
floatData[i] = shortData[i] / 32768.0f;
}
return floatData;
}
这里有个小技巧:除以 32768.0f 而不是 32767.0f。为什么?因为 short 范围是 -32768 到 32767,除以 32768 可以保证对称性,不会出现 -1.0 到 0.9999 这种不对称的情况。
3.3.4 采样率转换
这个比较复杂。比如你采集的是 44100Hz,但编码器要 16000Hz。怎么办?
我的建议是:不要自己写重采样算法。直接用 Android 的 AudioRecord 配合 AudioTrack 做重采样,或者用 MediaCodec 的格式转换能力。
如果非要自己转,可以用线性插值:
public short[] resample(short[] input, int inputSampleRate, int outputSampleRate) {
double ratio = (double) outputSampleRate / inputSampleRate;
int outputLength = (int) (input.length * ratio);
short[] output = new short[outputLength];
for (int i = 0; i < outputLength; i++) {
double srcIndex = i / ratio;
int srcIndexInt = (int) srcIndex;
double frac = srcIndex - srcIndexInt;
if (srcIndexInt + 1 < input.length) {
output[i] = (short) (input[srcIndexInt] * (1 - frac)
+ input[srcIndexInt + 1] * frac);
} else {
output[i] = input[srcIndexInt];
}
}
return output;
}
AudioRecord 直接设置目标采样率,或者用 MediaExtractor + MediaCodec 做专业重采样。自己写的重采样质量很难保证。
3.4 知识体系总览
说了这么多,咱们用一张图把 AudioRecord 的核心知识串起来:
这张图把咱们今天讲的内容串起来了。左边是创建配置,中间是数据读取,右边是格式转换。三个环节环环相扣,任何一个出问题,整个音频链路都会受影响。
3.5 避坑总结
最后,我把这些年踩过的坑总结一下:
- 不要硬编码 buffer 大小——用
getMinBufferSize()获取 - 不要在主线程读数据——开独立线程,设高优先级
- 不要忽略字节序——Android 是小端,转换时注意
- 不要自己写重采样——用系统 API 或专业库
- 不要用 MIC 做低延时——用 UNPROCESSED
嗯,这一章就到这儿。AudioRecord 的基础操作,说白了就是「创建 - 读取 - 转换」这三板斧。但每一板斧都有讲究,细节决定成败。
下一章咱们聊 AudioTrack,把音频输出的坑也填一填。
公众号:蓝海资料掘金营,微信deep3321