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 的整个工作流程,我画了一张图:

AudioTrack 工作流程 创建与配置 采样率 / 声道 / 编码 / Buffer PCM 数据准备 解码 / 重采样 / 声道映射 写入模式选择 阻塞式 / 非阻塞式 阻塞式写入 等待 buffer 有空位 非阻塞式 立即返回 数据投递到 HAL AudioFlinger → Audio HAL → 驱动 图: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 是音频输出的起点。创建时参数要匹配,写入时模式要选对,数据投递要按需。这三步做好了,低延时就有了基础。

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