12、AudioTrack与AudioRecord:Android原生音频API、低延迟播放、音频录制
说到Android上的音频开发,绕不开的两个核心API就是AudioTrack和AudioRecord。我刚开始做音视频那会儿,也踩过不少坑,尤其是低延迟播放这块,折腾了好几个通宵。今天咱们就把这两个API彻底聊透。
12.1 AudioTrack:原生音频播放的基石
AudioTrack是Android提供的原生音频播放API。说白了,它就是把PCM数据直接扔给音频硬件去播放。和MediaPlayer不同,它不负责解码,只负责播放裸数据。
核心特点:
- 直接操作PCM数据流
- 支持两种模式:静态模式和流式模式
- 延迟可控,适合实时性要求高的场景
- 不包含解码器,需要自己处理编码数据
12.1.1 两种工作模式
AudioTrack有两种模式,我习惯这么理解:
| 模式 | 适用场景 | 特点 |
|---|---|---|
| MODE_STATIC | 短音频、提示音、按键音 | 一次性写入,内存占用小,延迟最低 |
| MODE_STREAM | 长音频、实时流、VoIP | 持续写入,缓冲区循环,适合大数据量 |
我记得有一次做游戏音效,用了MODE_STREAM去播放一个只有几百毫秒的点击音,结果延迟高得离谱。后来换成MODE_STATIC,问题瞬间解决。你想想看,短音频用流式模式,就像用卡车运一袋米,太浪费了。
12.1.2 基本使用流程
创建一个AudioTrack其实不复杂,但参数配置很关键。我给大家一个标准模板:
// 配置参数
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
// 创建AudioTrack
AudioTrack audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize,
AudioTrack.MODE_STREAM
);
// 开始播放
audioTrack.play();
// 写入数据(循环写入)
byte[] audioData = new byte[1024];
while (isPlaying) {
int bytesRead = readFromSource(audioData);
if (bytesRead > 0) {
audioTrack.write(audioData, 0, bytesRead);
}
}
// 停止并释放
audioTrack.stop();
audioTrack.release();
我的经验:bufferSize不要自己随便算,一定要用getMinBufferSize()。我曾经图省事写了个固定值,结果在低端机上直接崩溃,教训深刻。
12.2 低延迟播放:那些年我们追过的延迟
低延迟播放是音频开发里的硬骨头。为什么?因为Android音频架构本身就有好几层缓冲。我给大家画个图就明白了:
12.2.1 延迟到底从哪里来?
我给大家拆解一下。一个音频数据从write()到喇叭出声,经历了:
- 应用缓冲区:你设置的bufferSize,默认通常偏大
- AudioFlinger混音:系统把多个音频流混在一起,这里有一层缓冲
- HAL层缓冲:驱动层的环形缓冲区,通常2-3个period
- 硬件DAC:数模转换,也有固定延迟
嗯,这里要注意。Android 8.0之后引入了AAudio API,它可以直接绕过AudioFlinger,延迟能降到10ms以内。我去年做一个K歌App,延迟要求特别苛刻,最后就是用AAudio搞定的。
避坑指南:我曾经在某个项目里,为了追求极致低延迟,把bufferSize设得特别小。结果在华为某款手机上直接爆音,因为它的硬件最小缓冲区比getMinBufferSize()返回的还大。所以,不要低于getMinBufferSize()的返回值。
12.3 AudioRecord:音频录制的正确姿势
AudioRecord是AudioTrack的镜像API,一个负责播放,一个负责录制。它的核心也是操作PCM数据,只不过方向反了。
12.3.1 录制流程
// 配置参数
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
// 创建AudioRecord
AudioRecord audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
// 开始录制
audioRecord.startRecording();
// 读取数据(循环读取)
byte[] audioData = new byte[1024];
while (isRecording) {
int bytesRead = audioRecord.read(audioData, 0, audioData.length);
if (bytesRead > 0) {
// 处理音频数据,比如写入文件或发送网络
processAudioData(audioData, bytesRead);
}
}
// 停止并释放
audioRecord.stop();
audioRecord.release();
12.3.2 音频源的选择
AudioRecord的构造函数里有个AudioSource参数,很多人不注意这个。我给大家列个表:
| 音频源 | 说明 | 适用场景 |
|---|---|---|
| MIC | 默认麦克风 | 通用录音 |
| VOICE_RECOGNITION | 语音识别优化 | 语音搜索、语音输入 |
| VOICE_COMMUNICATION | VoIP通话优化 | 网络电话、对讲 |
| UNPROCESSED | 原始音频,无处理 | 专业录音、音频分析 |
我个人习惯,做语音识别用VOICE_RECOGNITION,做VoIP用VOICE_COMMUNICATION。为什么?因为这些源会触发系统级的音频处理优化,比如回声消除、降噪等。
小技巧:如果你需要原始音频数据做分析,一定要用UNPROCESSED源。我见过有人用MIC源做频谱分析,结果数据被系统处理过,分析结果完全不对。
12.4 实战:低延迟音频录制与播放
咱们来个实战,把AudioRecord和AudioTrack结合起来,做一个低延迟的音频回环(Loopback)应用。说白了,就是一边录一边放,延迟越低越好。
12.4.1 核心思路
回环的关键在于:
- 录制线程和播放线程独立运行
- 使用共享缓冲区传递数据
- 缓冲区大小要匹配,避免溢出或饥饿
public class AudioLoopback {
private AudioRecord recorder;
private AudioTrack track;
private volatile boolean isRunning = false;
private int bufferSize;
public void start() {
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
// 获取最小缓冲区
bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
// 创建录制和播放对象
recorder = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate, channelConfig, audioFormat, bufferSize);
track = new AudioTrack(AudioManager.STREAM_VOICE_CALL,
sampleRate, AudioFormat.CHANNEL_OUT_MONO, audioFormat,
bufferSize, AudioTrack.MODE_STREAM);
isRunning = true;
recorder.startRecording();
track.play();
// 启动处理线程
new Thread(this::loopbackLoop).start();
}
private void loopbackLoop() {
byte[] buffer = new byte[bufferSize];
while (isRunning) {
int bytesRead = recorder.read(buffer, 0, buffer.length);
if (bytesRead > 0) {
track.write(buffer, 0, bytesRead);
}
}
}
public void stop() {
isRunning = false;
recorder.stop();
track.stop();
recorder.release();
track.release();
}
}
关键点:这里用了STREAM_VOICE_CALL而不是STREAM_MUSIC,因为VoIP流的延迟更低。另外,录制和播放的采样率、声道数、编码格式必须完全一致,否则会有杂音。
12.4.2 延迟测试
我实际测试过这个回环代码,在Pixel 6上延迟大约30ms,在小米11上大约45ms。为什么有差异?因为不同厂商的音频驱动和缓冲区策略不一样。这也是Android碎片化的一个体现。
如果你追求更低的延迟,可以考虑:
- 使用AAudio API(Android 8.0+)
- 使用OpenSL ES(NDK层)
- 使用Oboe库(Google官方推荐)
12.5 常见问题与解决方案
做音频开发这么多年,我总结了一些高频问题:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 播放有杂音/爆音 | 缓冲区太小或写入不及时 | 增大bufferSize,或使用双缓冲 |
| 录制声音小 | 麦克风增益不够 | 使用VOICE_COMMUNICATION源,或手动增益 |
| 延迟忽高忽低 | 系统负载波动 | 设置线程优先级,使用实时线程 |
| 多设备兼容性问题 | 厂商定制差异 | 使用getMinBufferSize()动态计算 |
曾经踩过的坑:有一次我在某个平板上做录音,发现录出来的声音有回声。排查了半天,原来是那个平板的麦克风和扬声器距离太近,产生了声学回声。最后加了AEC(回声消除)才解决。所以,硬件问题有时候比软件问题更头疼。
好了,AudioTrack和AudioRecord的核心内容就这些。这两个API虽然基础,但用好它们,你能做出很多有意思的东西——实时变声、网络K歌、语音对讲等等。关键是要理解它们的延迟模型和缓冲区管理,这是所有音频应用的基础。