2、AudioTrack基础:AudioTrack的创建与配置、阻塞式与非阻塞式写入、PCM数据的准备与投递
好,我们正式开始聊 AudioTrack。这是 Android 音频输出的核心类,没有之一。你写的所有音频数据,最终都要通过它送到音频 HAL 层。说白了,它就是 App 和底层音频驱动之间的那根管子。
我个人习惯把 AudioTrack 比作一个水龙头。你拧开它(创建),调好水流大小(配置),然后往里面倒水(写入数据)。水龙头有两种模式:一种是倒水进去必须等水流完才能继续倒(阻塞式);另一种是倒完就走,不管水流没流完(非阻塞式)。嗯,这个比喻虽然糙,但道理是通的。
2.1 创建与配置:别小看这几行代码
创建 AudioTrack 其实很简单,但坑不少。我见过太多人直接 new 一个出来,参数随便填,结果要么延迟爆炸,要么声音断断续续。
先看一个标准创建流程:
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioTrack audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(audioFormat)
.setSampleRate(sampleRate)
.setChannelMask(channelConfig)
.build())
.setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
这里有几个关键点,我重点说一下:
- 采样率:尽量用 44100 或 48000。用奇怪的采样率,底层会重采样,增加延迟。我在项目中遇到过用 22050 的,结果延迟直接翻倍。
- 声道配置:立体声还是单声道?如果你只是播放人声,单声道就够了。立体声会多一倍的 buffer 开销。
- 编码格式:PCM_16BIT 是最通用的。PCM_8BIT 虽然省空间,但音质差,不推荐。PCM_FLOAT 精度高,但部分老设备不支持。
- Buffer 大小:用
getMinBufferSize()获取最小值。这个值是根据采样率和声道算出来的,太小会丢数据,太大会增加延迟。
核心原则:Buffer 越小,延迟越低,但 CPU 负担越重。Buffer 越大,越稳定,但延迟越高。低延时场景下,我建议在 getMinBufferSize() 的基础上乘以 1.5 到 2 倍,既保证稳定性,又不至于延迟太高。
2.2 阻塞式写入 vs 非阻塞式写入
这是 AudioTrack 最核心的概念之一。两种模式,对应不同的应用场景。
阻塞式写入(MODE_STREAM + 默认模式)
当你调用 write() 时,如果底层 buffer 满了,线程就会卡住,直到 buffer 有空位才返回。这就像你往一个满的杯子里倒水,必须等水流走一些才能继续倒。
// 阻塞式写入
int bytesWritten = audioTrack.write(audioData, 0, audioData.length);
// 这行代码执行完,数据才真正被消费掉
阻塞式的优点是简单、安全。你不用担心数据丢失,系统会帮你排队。缺点是如果你在主线程里写,UI 会卡。我曾经在一个直播项目里看到同事在主线程里写音频数据,结果画面一帧一帧地跳,用户直接骂娘。
警告:永远不要在 UI 线程里做阻塞式写入。哪怕只是几毫秒的阻塞,也会让界面掉帧。请务必放到子线程。
非阻塞式写入(MODE_STATIC 或设置 WRITE_NON_BLOCKING)
非阻塞式写入,调用 write() 后立即返回。它返回实际写入的字节数,可能小于你传入的长度。剩下的数据怎么办?你得自己处理,要么丢弃,要么下次再写。
// 非阻塞式写入
int bytesWritten = audioTrack.write(audioData, 0, audioData.length, AudioTrack.WRITE_NON_BLOCKING);
if (bytesWritten < audioData.length) {
// 没写完,需要处理剩余数据
// 我一般会记录偏移量,下次继续写
}
非阻塞式的优势是延迟极低,适合实时性要求高的场景,比如游戏音效、VoIP 通话。但代价是你要自己管理数据流,写不完的数据不能丢,得缓存起来。
我个人习惯:播放音乐用阻塞式,播放实时音效用非阻塞式。音乐可以容忍几十毫秒的延迟,但音效不行。你想想看,开枪的音效如果晚了几十毫秒,玩家体验直接崩了。
2.3 PCM 数据的准备与投递
PCM 数据是什么?说白了就是裸的音频采样点。没有压缩,没有编码,就是一个个数字。你从麦克风录到的是 PCM,从文件解码出来的也是 PCM。AudioTrack 只认 PCM。
数据准备:从文件到 PCM
如果你从文件播放,需要先解码。Android 的 MediaCodec 或 MediaExtractor 可以帮你把 MP3、AAC 等格式转成 PCM。这里我直接给一个简化的解码流程:
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);
MediaFormat format = extractor.getTrackFormat(0);
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
// ... 其他参数
MediaCodec codec = MediaCodec.createDecoderByType(mimeType);
codec.configure(format, null, null, 0);
codec.start();
// 循环读取、解码、写入 AudioTrack
ByteBuffer[] inputBuffers = codec.getInputBuffers();
// ... 省略具体解码逻辑
嗯,这里要注意:解码出来的 PCM 数据,采样率和声道数必须和 AudioTrack 创建时一致。不一致的话,要么重采样,要么声道映射,都会增加延迟。我建议在创建 AudioTrack 之前,先拿到音频文件的真实参数,再创建匹配的 AudioTrack。
数据投递:写入的时机和策略
数据准备好了,怎么投递给 AudioTrack?这里有个常见的误区:一次性把所有数据都写进去。千万别这么做。AudioTrack 内部 buffer 有限,写太多会阻塞(阻塞模式)或丢失(非阻塞模式)。
正确的做法是:按需写入,保持 buffer 半满状态。我一般会开一个子线程,循环检查 AudioTrack 的 buffer 剩余空间,然后写入合适大小的数据块。
// 伪代码:按需写入
new Thread(() -> {
while (isPlaying) {
int available = audioTrack.getBufferSizeInFrames() - audioTrack.getPlaybackHeadPosition();
if (available > MIN_WRITE_SIZE) {
int bytesToWrite = Math.min(available * frameSize, dataRemaining);
audioTrack.write(data, offset, bytesToWrite);
offset += bytesToWrite;
} else {
// buffer 还满着,等一会再写
Thread.sleep(5);
}
}
}).start();
小技巧:getPlaybackHeadPosition() 返回的是当前播放到的帧位置。用这个值可以算出 buffer 里还有多少空间。但注意,这个值在非阻塞模式下可能更新不及时,需要配合回调使用。
2.4 知识体系总览
为了让你更直观地理解 AudioTrack 的整个工作流程,我画了一张图:
从这张图可以清楚看到,整个流程分五步:创建配置 → 准备 PCM → 选择写入模式 → 实际写入 → 投递到 HAL。每一步都有坑,每一步都影响延迟。
2.5 避坑指南
最后,分享几个我踩过的坑:
- 不要用 MODE_STATIC 播放长音频:MODE_STATIC 是一次性把所有数据加载到 buffer 里,适合短音效。如果你用它播放一首歌,内存直接爆炸。我曾经犯过这个错,3 分钟的 MP3 解码成 PCM 后,内存飙到 200MB。
- 注意线程安全:AudioTrack 不是线程安全的。不要在多个线程里同时 write。我一般用一个专门的写入线程,配合一个 ConcurrentLinkedQueue 做数据缓冲。
- 释放资源:播放结束后,记得调用
release()。不释放的话,AudioTrack 对象会一直占用底层资源,导致后续创建失败。我见过一个 App 反复创建不释放,最后系统直接报 "AudioTrack could not be created"。
好了,这一章的内容就到这里。AudioTrack 的基础操作,说白了就是创建、配置、写入这三板斧。但每一板斧都有讲究,尤其是写入模式的选择,直接决定了你的延迟表现。下一章我们会深入 buffer 的管理,看看怎么进一步压榨延迟。
一句话总结:AudioTrack 是音频输出的起点。创建时参数要匹配,写入时模式要选对,数据投递要按需。这三步做好了,低延时就有了基础。