上一篇我们搭好了"舞台"——分析了AudioFlinger的整体架构。这一篇我们来追一个具体的"演员":AudioTrack。
如果你曾经用过 MediaPlayer 播放一首歌,或者用 SoundPool 触发游戏音效,甚至用 AudioTrack 直接写PCM数据,那这些操作的底层全都绕不开今天要讲的内容。AudioTrack 就是 App 进程和 audioserver 进程之间的"音频快递员",负责把你的音频数据高效、低延迟地送达声卡。
本文主要内容:
- AudioTrack API 核心参数和播放模式解析
- 创建流程:从 Java
Builder到 AudioFlinger 完整调用链 - 共享内存 Ring Buffer:零拷贝数据传输的核心机制
- 播放控制状态机:play/pause/stop/flush 的内部实现
- FAST Track 低延迟路径:20ms 内延迟的秘密
- 实战案例:PCM播放器 + 低延迟游戏音效
源码版本:Android 15 AOSP 关键路径:
frameworks/base/media/java/android/media/AudioTrack.java、frameworks/av/media/libaudioclient/AudioTrack.cpp
AudioTrack API 基础
两种播放模式的本质区别
AudioTrack 有两种播放模式,选错了轻则多占内存,重则产生明显延迟:
// MODE_STREAM:流式播放,适合长音频
AudioTrack streamTrack = new AudioTrack.Builder()
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(44100)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build())
.setBufferSizeInBytes(minBufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
// MODE_STATIC:静态缓冲,适合短音效
AudioTrack staticTrack = new AudioTrack.Builder()
.setBufferSizeInBytes(pcmData.length)
.setTransferMode(AudioTrack.MODE_STATIC)
.build();
staticTrack.write(pcmData, 0, pcmData.length); // 一次性写入全部数据
| 特性 | MODE_STREAM | MODE_STATIC |
|---|---|---|
| 适用场景 | 音乐、长视频、实时录音回放 | 游戏音效、短提示音(< 1MB) |
| 内存使用 | 按缓冲区大小分配 | 全部PCM数据一次性加载 |
| 延迟 | 取决于缓冲区大小 | 极低(数据已在内存) |
| 数据写入 | 持续调用 write() | play() 前一次写完 |
| 缓冲区位置 | 进程间共享内存 | AudioFlinger 内部 |
经验之谈:游戏开发者经常犯一个错误——用 MODE_STREAM 播放子弹飞的"嗖"声,然后发现延迟明显。对于 < 1MB 的短音效,MODE_STATIC + 提前加载才是正道。
关键参数详解
// 获取最小缓冲区大小(这是API强制要求的下限)
int minBufferSize = AudioTrack.getMinBufferSize(
sampleRate, // 采样率:8000/16000/22050/44100/48000 Hz
channelConfig, // 声道配置:CHANNEL_OUT_MONO / STEREO / 5_1
audioFormat // 数据格式:PCM_8BIT / PCM_16BIT / PCM_FLOAT
);
getMinBufferSize() 的内部逻辑值得一提。它并不是简单地返回一个固定值,而是查询 AudioFlinger 得到设备支持的最小帧数,再乘以 frameSize(单帧字节数)。这个值直接反映了硬件的能力上限。
为何不能设太小? 缓冲区太小意味着 write() 阻塞频繁,CPU 调度开销增大;更糟糕的是,一旦数据供给跟不上,AudioFlinger 就会产生 underrun(欠载),你会听到断音或爆音。
AudioAttributes:音频属性的隐藏影响
很多开发者不知道,AudioAttributes 不只是标签,它直接影响音频路由和焦点管理:
AudioAttributes attrs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME) // 用途:游戏
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
// USAGE_GAME 会影响:
// 1. AudioPolicyService 的路由决策
// 2. 音量控制归属(游戏音量流 vs 媒体音量流)
// 3. 音频焦点的优先级
AudioTrack 创建流程深度解析
用一张图来看整个创建过程:
这个创建链路跨越了3个进程边界,我们逐层拆解。
第一层:Java Builder 模式
AudioTrack.Builder.build() 最终调用的是 AudioTrack 的私有构造函数,然后触发 native_setup():
// frameworks/base/media/java/android/media/AudioTrack.java
private AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int mode, int sessionId, boolean offload, int encapsulationMode,
@Nullable TunerConfiguration tunerConfiguration) {
// ...参数校验...
// 核心:调用 Native 层初始化
int initResult = native_setup(
new WeakReference<AudioTrack>(this),
mAttributes,
sampleRate, channelMask, channelIndexMask,
audioFormat, buffSizeInBytes, mode,
sessionId, 0 /*nativeAudioTrack*/,
offload, encapsulationMode, tunerConfiguration,
getCurrentOpPackageName());
if (initResult != SUCCESS) {
loge("Error code " + initResult + " when initializing AudioTrack.");
return;
}
}
值得注意的是 native_setup 的第一个参数:new WeakReference<AudioTrack>(this)。Native 层持有的是 Java 对象的弱引用,这样可以防止 Native 层阻止 Java 层的 GC 回收。这是 Android JNI 中的标准实践。
第二层:JNI 桥接
JNI 层在 android_media_AudioTrack.cpp 中,这里做了几件关键事情:
// frameworks/base/core/jni/android_media_AudioTrack.cpp
static jint android_media_AudioTrack_setup(JNIEnv *env, jobject thiz,
jobject weak_this, jobject jaa, jintArray jSampleRate, ...) {
// 1. 创建 Native AudioTrack 对象
sp<AudioTrack> lpTrack = new AudioTrack();
// 2. 初始化(这里开始跨进程)
status_t status = lpTrack->set(
AUDIO_STREAM_DEFAULT,
sampleRateInHertz,
format,
nativeChannelMask,
frameCount,
(AudioTrack::transfer_type)transferType,
...);
// 3. 将 Native 指针存入 Java 对象
setAudioTrack(env, thiz, lpTrack);
return (jint)AUDIO_JAVA_SUCCESS;
}
第三层:Native AudioTrack::set()
这是最关键的环节。AudioTrack::set() 做了两件大事:参数校验与格式协商 和 与 AudioFlinger 建立连接。
// frameworks/av/media/libaudioclient/AudioTrack.cpp
status_t AudioTrack::set(
audio_stream_type_t streamType,
uint32_t sampleRate,
audio_format_t format,
audio_channel_mask_t channelMask,
size_t frameCount,
...) {
// 1. 参数校验:格式、采样率、声道数合法性检查
if (!audio_is_valid_format(format)) {
ALOGE("Invalid format %#x", format);
return BAD_VALUE;
}
// 2. 格式协商:查询 AudioFlinger 确定最终参数
// 例如:App 请求 44100Hz,硬件支持 48000Hz
// AudioFlinger 会返回实际的采样率
status_t status = createTrack_l();
if (status != NO_ERROR) {
return status;
}
// 3. 注册播放完成回调
if (cbf != NULL) {
mAudioTrackThread = new AudioTrackThread(*this);
}
return NO_ERROR;
}
第四层:createTrack_l() —— 最关键的 Binder 调用
// frameworks/av/media/libaudioclient/AudioTrack.cpp
status_t AudioTrack::createTrack_l() {
// 1. 获取 AudioFlinger 的 Binder 代理
const sp<IAudioFlinger>& audioFlinger = AudioSystem::get_audio_flinger();
// 2. 构造 createTrack 请求参数
IAudioFlinger::CreateTrackInput input;
input.attr = mAttributes;
input.config.sample_rate = mSampleRate;
input.config.channel_mask = mChannelMask;
input.config.format = mFormat;
input.frameCount = mReqFrameCount;
// 3. 跨进程调用 AudioFlinger.createTrack()
// 这是一次 Binder IPC,执行在 audioserver 进程
IAudioFlinger::CreateTrackOutput output;
sp<IAudioTrack> track = audioFlinger->createTrack(input, output, &status);
// 4. 拿到返回的共享内存描述符,进行 mmap
sp<IMemory> iMem = track->getCblk();
void* iMemPointer = iMem->unsecurePointer();
// 5. mCblk:控制块,包含 writePtr、readPtr、frameCount 等
mCblk = static_cast<audio_track_cblk_t*>(iMemPointer);
// 6. 实际音频数据区紧跟在 mCblk 后面
mBuffer = (char*)iMemPointer + sizeof(audio_track_cblk_t);
return NO_ERROR;
}
Binder 调用的另一端,AudioFlinger::createTrack() 做了什么?
// frameworks/av/services/audioflinger/AudioFlinger.cpp
sp<IAudioTrack> AudioFlinger::createTrack(const CreateTrackInput& input,
CreateTrackOutput& output, status_t *status) {
// 1. 根据 AudioAttributes 选择合适的 PlaybackThread
// (MixerThread / DirectOutputThread / OffloadThread)
sp<PlaybackThread> thread = checkPlaybackThread_l(output.outputId);
// 2. 在选定的线程上创建 Track 对象
sp<PlaybackThread::Track> track = thread->createTrack_l(
client, streamType, attr, &sampleRate, format, channelMask,
&frameCount, ...);
// 3. 分配共享内存(这是 Ring Buffer 的核心)
// ashmem 匿名共享内存,两个进程都能 mmap
size_t bufferSize = roundup(frameCount) * mFrameSize;
sp<MemoryDealer> heap = new MemoryDealer(bufferSize + ...);
sp<IMemory> cblkMemory = heap->allocate(sizeof(audio_track_cblk_t) + bufferSize);
// 4. 返回 IAudioTrack 代理和共享内存 fd
sp<TrackHandle> trackHandle = new TrackHandle(track);
return trackHandle;
}
整个过程下来,App 进程和 audioserver 进程共同持有同一块物理内存——这就是零拷贝的基础。
共享内存 Ring Buffer 机制
这是 Android 音频子系统最精妙的设计之一。理解了 Ring Buffer,你就理解了为什么 Android 音频能做到低延迟且高效。
mCblk 控制块的结构
// frameworks/av/media/libaudioclient/include/media/audio_track_cblk_t.h
struct audio_track_cblk_t {
Mutex lock; // 用于状态同步(非数据传输)
Condition cv; // 条件变量,用于 blocking write
volatile int32_t mFront; // 读指针(AudioFlinger 消费者)
volatile int32_t mRear; // 写指针(App 生产者)
uint32_t frameCount_; // Ring Buffer 总帧数
uint32_t mMinimum; // 触发回调的最小可用帧数
// 状态标志
volatile int32_t mFutex; // futex 无锁同步
// 音量(App 可以直接修改,无需 Binder IPC)
float mVolumeLR; // 左右声道音量
// 时间戳
ExtendedTimestamp mExtendedTimestampQueue[...];
};
精妙之处:音量控制直接写 mCblk->mVolumeLR,不需要 Binder IPC!这是为什么 AudioTrack.setStereoVolume() 几乎是瞬时生效的原因。
生产者-消费者模型
Ring Buffer 的核心是 mFront(读指针)和 mRear(写指针):
Ring Buffer 状态示意(总大小 = 8 帧):
初始状态:
[ ][ ][ ][ ][ ][ ][ ][ ]
↑
mFront = mRear = 0(空)
App 写入 4 帧后:
[D0][D1][D2][D3][ ][ ][ ][ ]
↑ ↑
mFront=0 mRear=4
AudioFlinger 消费 2 帧后:
[ ][ ][D2][D3][ ][ ][ ][ ]
↑ ↑
mFront=2 mRear=4
// 可写帧数 = frameCount - (mRear - mFront)
// 可读帧数 = mRear - mFront
write() 的内部实现
// frameworks/av/media/libaudioclient/AudioTrack.cpp
ssize_t AudioTrack::write(const void* buffer, size_t userSize, bool blocking) {
// 1. 获取可写空间
Buffer audioBuffer;
status_t err = obtainBuffer(&audioBuffer,
blocking ? &ClientProxy::kForever : &ClientProxy::kNonBlocking);
if (err == TIMED_OUT || err == WOULD_BLOCK) {
// 缓冲区满,blocking=false 时直接返回
// blocking=true 时会在这里等待 AudioFlinger 消费数据
return 0;
}
// 2. 直接内存拷贝(目标地址是共享内存)
size_t toWrite = min(audioBuffer.size, userSize);
memcpy(audioBuffer.raw, buffer, toWrite);
// 3. 更新写指针(原子操作)
releaseBuffer(&audioBuffer);
return toWrite;
}
obtainBuffer() 的无锁设计
这里是性能的关键。App 和 AudioFlinger 通过 futex(Fast Userspace muTEX)进行同步,大多数情况下不需要进入内核:
// frameworks/av/media/libaudioclient/AudioTrackShared.cpp
status_t ClientProxy::obtainBuffer(Buffer* buffer, const struct timespec *requested, ...) {
int32_t front = android_atomic_acquire_load(&mCblk->u.mStreaming.mFront);
int32_t rear = mCblk->u.mStreaming.mRear;
// 计算可写帧数
ssize_t filled = rear - front; // mFront 绕回时自动处理
ssize_t avail = mFrameCount - filled;
if (avail > 0) {
// 有可用空间,直接返回,无需等待(用户空间完成)
buffer->mFrameCount = avail;
buffer->mRaw = ...; // 指向共享内存的实际地址
return NO_ERROR;
}
// 缓冲区满,需要等待
if (requested == &kNonBlocking) {
return WOULD_BLOCK;
}
// futex_wait:等待 AudioFlinger 消费数据(可能进入内核)
(void) syscall(__NR_futex, &mCblk->mFutex, FUTEX_WAIT_PRIVATE, ...);
goto start;
}
关键数字:在正常情况下,write() 大约只需要 1~5 微秒(1μs = 0.001ms),因为大多数时候是用户空间的原子操作,不涉及系统调用。
播放控制状态机
AudioTrack 有一套清晰的状态机,搞清楚状态转换能避免很多坑:
STATE_UNINITIALIZED
↓ new AudioTrack().build()
STATE_INITIALIZED ←─────────────────────────────┐
↓ play() │
STATE_PLAYING │
↓ pause() ↓ stop() │
STATE_PAUSED ──── play()──→ STATE_PLAYING │
↓ flush() │
STATE_PAUSED_STOPPING │
↓ stop() completes │
STATE_STOPPED ──────────┘
(release() 可随时调用)
play() 的内部流程
// AudioTrack.java
public void play() throws IllegalStateException {
// Java 层状态检查
if (mState == STATE_UNINITIALIZED) {
throw new IllegalStateException("play() called on uninitialized AudioTrack.");
}
// 调用 Native
native_start();
synchronized(mPlayStateLock) {
mPlayState = PLAYSTATE_PLAYING;
// 唤醒 AudioTrackThread,开始处理回调
if (mAudioTrackThread != null) {
mAudioTrackThread.wakeup();
}
}
}
Native 层的 start() 最终通过 Binder 调用 ITrack::start(),这会通知 AudioFlinger 的 PlaybackThread:"我有数据了,请把我加入混音队列"。
// AudioFlinger 端:PlaybackThread 的主循环
bool AudioFlinger::PlaybackThread::threadLoop() {
while (!exitPending()) {
// 1. 等待有 Active Track
if (mActiveTracks.isEmpty()) {
mWaitWorkCV.wait(mLock);
}
// 2. 遍历所有 Active Track,从 Ring Buffer 读取数据
for (auto& track : mActiveTracks) {
AudioBufferProvider::Buffer buf;
status_t status = track->getNextBuffer(&buf);
if (status == OK) {
mixer->setBufferProvider(trackId, track);
}
}
// 3. 混音所有 Track 的数据到输出缓冲区
mixer->process();
// 4. 写入 Audio HAL
mOutput->write(mMixBuffer, mFrameCount);
}
}
pause() vs stop() 的区别
这是很多开发者的疑问:
| 操作 | Ring Buffer 中的数据 | 再次 play() 时 |
|---|---|---|
pause() | 保留,writePtr/readPtr 不变 | 从断点继续播放 |
stop() | 保留,等待播完后再停 | 必须 flush() + 重新 write() |
flush() | 清空,readPtr=writePtr | 从头开始写新数据 |
// 正确的暂停/恢复
audioTrack.pause();
// ... 一段时间后 ...
audioTrack.play(); // 从暂停位置继续
// 正确的停止并重新播放
audioTrack.stop();
audioTrack.flush(); // 清空缓冲区中的剩余数据
// 重新 write() 数据...
audioTrack.play();
常见Bug:调用 stop() 后不调用 flush() 就 play(),会把上次残留在缓冲区的数据也播出来,听起来像"鬼音"。
FAST Track 低延迟路径
正常的 MixerThread 延迟在 50-100ms,而 FAST Track 可以做到 10-20ms 甚至更低。这是 Android 音频系统最重要的性能特性之一。
FAST Track 的判断条件
并非所有 AudioTrack 都能进入 FAST Track,AudioFlinger 有严格的条件检查:
// frameworks/av/services/audioflinger/Threads.cpp
// MixerThread::checkForNewParameters_l()
bool AudioFlinger::MixerThread::isTrackAllowedForFastTrack(
const sp<Track>& track) {
// 条件1:采样率必须与硬件输出一致(通常48000Hz)
if (track->sampleRate() != mSampleRate) {
return false; // 需要重采样,无法走快速路径
}
// 条件2:声道数必须与硬件输出一致
if (track->channelCount() != mChannelCount) {
return false;
}
// 条件3:格式必须是 PCM_16BIT 或 PCM_FLOAT
if (!audio_is_linear_pcm(track->format())) {
return false;
}
// 条件4:缓冲区必须足够小(避免引入太大延迟)
if (track->frameCount() > mFrameCount * FastMixerState::kMaxFastTracks) {
return false;
}
// 条件5:没有附加音效链(AudioEffect 会破坏实时性)
if (track->getEffectChain() != nullptr) {
return false;
}
return true;
}
FAST Track 的实时调度
FAST Track 由专门的 FastMixer 线程处理,它与普通 MixerThread 最大的区别是实时调度策略:
// frameworks/av/services/audioflinger/FastMixer.cpp
void FastMixer::onStateChange() {
// 设置 SCHED_FIFO 实时调度,优先级 19(高于所有普通线程)
pid_t tid = gettid();
int err = set_sched_policy(tid, SP_FOREGROUND);
struct sched_param param = {.sched_priority = 19};
sched_setscheduler(tid, SCHED_FIFO, ¶m);
// 使用 CPU 亲和性绑定到特定核心(避免核间迁移)
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(FAST_MIXER_CPU, &cpuset);
sched_setaffinity(tid, sizeof(cpuset), &cpuset);
}
FAST Track 的无锁状态传递
FastMixer 的状态传递使用的是著名的 MWSR(Multi-Writer Single-Reader) 无锁队列:
// frameworks/av/services/audioflinger/StateQueue.h
// 原理:多个写线程通过 CAS 原子操作提交状态更新
// FastMixer 读线程轮询最新状态,无需持锁
template<typename T>
class StateQueue {
volatile int32_t mAck; // 确认序号
volatile int32_t mIndex; // 当前活跃状态索引
T mStates[kN]; // 状态槽位(固定数量)
// 写端:原子地发布新状态
void push(const T* state) {
int next = (mIndex + 1) % kN;
mStates[next] = *state;
android_atomic_release_store(next, &mIndex);
}
// 读端:轮询获取最新状态(零拷贝,无锁)
const T* poll() {
int index = android_atomic_acquire_load(&mIndex);
return &mStates[index];
}
};
如何让你的 App 进入 FAST Track
// 方法1:使用 AudioTrack.Builder 的 PERFORMANCE_MODE_LOW_LATENCY
AudioTrack lowLatencyTrack = new AudioTrack.Builder()
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(48000) // 必须匹配硬件采样率!
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build())
.setBufferSizeInBytes(
AudioTrack.getMinBufferSize(48000,
AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT)) // 使用最小缓冲区
.build();
// 验证是否真正进入了 FAST Track
int mode = lowLatencyTrack.getPerformanceMode();
if (mode == AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) {
Log.d(TAG, "成功进入 FAST Track!");
} else if (mode == AudioTrack.PERFORMANCE_MODE_POWER_SAVING) {
Log.w(TAG, "降级为省电模式,请检查参数");
}
实际可达延迟参考值(Pixel 9 Pro 测试):
- 普通 MixerThread:~85ms
- FAST Track:~12ms
- AAudio + FAST Track:~8ms
- OpenSL ES + FAST Track:~10ms
实战案例
案例一:PCM 原始音频播放器
这是 AudioTrack 最基础的用法,适合需要完全控制音频数据的场景(如自定义解码器输出):
public class PcmPlayer {
private AudioTrack mAudioTrack;
private Thread mPlayThread;
private volatile boolean mIsPlaying = false;
public void init(int sampleRate, int channelCount) {
int channelConfig = channelCount == 2
? AudioFormat.CHANNEL_OUT_STEREO
: AudioFormat.CHANNEL_OUT_MONO;
int minBufferSize = AudioTrack.getMinBufferSize(
sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
// 实际缓冲区设为最小值的 2 倍,平衡延迟和稳定性
int bufferSize = minBufferSize * 2;
mAudioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(channelConfig)
.build())
.setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
}
public void startPlaying(InputStream pcmStream) {
mIsPlaying = true;
mAudioTrack.play();
mPlayThread = new Thread(() -> {
byte[] buffer = new byte[4096];
int bytesRead;
try {
while (mIsPlaying && (bytesRead = pcmStream.read(buffer)) != -1) {
// write() 在缓冲区满时会阻塞,这是期望行为
int written = mAudioTrack.write(buffer, 0, bytesRead);
if (written < 0) {
Log.e(TAG, "写入失败: " + written);
break;
}
}
} catch (IOException e) {
Log.e(TAG, "读取流失败", e);
} finally {
// 等待缓冲区中的数据播完
mAudioTrack.stop();
}
});
mPlayThread.start();
}
public void pause() {
mAudioTrack.pause();
}
public void resume() {
mAudioTrack.play();
}
public void release() {
mIsPlaying = false;
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.flush();
mAudioTrack.release();
mAudioTrack = null;
}
}
}
案例二:低延迟游戏音效播放器
游戏场景需要极低延迟,使用 MODE_STATIC + PERFORMANCE_MODE_LOW_LATENCY:
public class GameSoundEffect {
// 预加载所有音效,避免播放时解码延迟
private final Map<String, AudioTrack> mEffects = new HashMap<>();
public void preload(String key, byte[] pcmData, int sampleRate) {
int minSize = AudioTrack.getMinBufferSize(sampleRate,
AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
// 缓冲区至少要能容纳整个音效
int bufferSize = Math.max(pcmData.length, minSize);
AudioTrack track = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build())
.setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STATIC) // 静态模式
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) // 低延迟
.build();
// 预先写入全部数据
track.write(pcmData, 0, pcmData.length);
mEffects.put(key, track);
Log.d(TAG, "预加载音效: " + key + " 模式: " + track.getPerformanceMode());
}
/**
* 触发音效播放(可以从任意线程调用,几乎零延迟)
*/
public void play(String key) {
AudioTrack track = mEffects.get(key);
if (track == null) return;
// MODE_STATIC 重播需要先 reloadStaticData(Android 9+)
// 或者先 stop() + flush() + play()
if (track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
track.stop();
// 重置到开头(MODE_STATIC 特有)
track.reloadStaticData();
}
track.play();
}
public void release() {
for (AudioTrack track : mEffects.values()) {
track.release();
}
mEffects.clear();
}
}
案例三:获取精确播放时间戳
音视频同步场景下,需要精确知道当前播放到哪一帧:
// Android 6.0+ 推荐使用 AudioTimestamp
AudioTimestamp timestamp = new AudioTimestamp();
if (audioTrack.getTimestamp(timestamp)) {
// timestamp.framePosition:已经送到硬件的帧号
// timestamp.nanoTime:对应的系统时间(System.nanoTime())
long currentPositionUs = timestamp.framePosition * 1_000_000L / sampleRate;
long elapsedNs = System.nanoTime() - timestamp.nanoTime;
// 估算当前实际播放位置(考虑硬件缓冲区中的延迟)
long estimatedPositionUs = currentPositionUs + elapsedNs / 1000;
Log.d(TAG, "当前播放位置: " + estimatedPositionUs / 1000 + " ms");
} else {
// 还没开始播放或时间戳无效,用 getPlaybackHeadPosition() 降级
int headPosition = audioTrack.getPlaybackHeadPosition();
}
调试技巧
dumpsys 分析 Track 状态
# 查看所有 AudioTrack 的详细信息
adb shell dumpsys media.audio_flinger | grep -A 30 "Output thread"
# 典型输出示例:
# Output thread 0x7f3d200000 type 0 (MixerThread):
# Output 1: ...
# 1 Tracks of which 1 are active
# active [ 0] state:0x03 id:1 sessionId:xx 5.1 44100Hz PCM_16_BIT
# FAST Volume: 1.000000 buffer:0xb4001234
# 关注字段说明:
# state:0x03 → 0x01=ACTIVE 0x02=PAUSED 0x04=STOPPING
# FAST → 确认此 Track 走了 FAST Track 路径
# buffer:... → 共享内存地址
检测 Underrun(欠载)
# 查看欠载统计
adb shell dumpsys media.audio_flinger | grep -i underrun
# 典型输出:
# underruns: total=10, recent=2
# 如果 recent 持续增加,说明 App 的 write() 速度跟不上消费速度
Systrace 分析音频延迟
# 抓取音频相关的 trace
adb shell atrace --async_start -b 32768 audio
# ... 播放音频 ...
adb shell atrace --async_stop
# 在 Perfetto UI 中查看:
# 关注 "audio.write" 和 "AudioTrack::write" 的时间间隔
# FAST Track 的 AudioMixer::process 应该 < 2ms
常见问题排查
问题:播放时有断音、爆音
# 方法1:检查 underrun
adb shell dumpsys media.audio_flinger | grep "underruns"
# 方法2:检查是否有主线程调用 write()
# write() 在缓冲区满时会阻塞!如果在主线程,会导致 ANR
# 解决:始终在专用线程调用 write()
问题:明明是 48000Hz 却没进 FAST Track
# 检查设备实际采样率
adb shell dumpsys media.audio_flinger | grep "Sampling rate"
# 检查是否有 AudioEffect 被附加
adb shell dumpsys media.audio_flinger | grep "EffectChain"
问题:getMinBufferSize() 返回 -1(ERROR_BAD_VALUE)
// 原因:采样率或格式不支持
// 安全的采样率检查
int[] supportedRates = {48000, 44100, 22050, 16000};
for (int rate : supportedRates) {
int minSize = AudioTrack.getMinBufferSize(rate,
AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT);
if (minSize > 0) {
Log.d(TAG, "支持采样率: " + rate + " Hz");
break;
}
}
总结
我们沿着 AudioTrack 的生命周期走了一遍完整旅程:
| 阶段 | 关键操作 | 核心机制 |
|---|---|---|
| 创建 | new AudioTrack.Builder().build() | Java→JNI→Native→Binder IPC |
| 建立连接 | AudioFlinger.createTrack() | ashmem 共享内存分配 |
| 写入数据 | audioTrack.write(pcm, 0, len) | Ring Buffer + futex 无锁 |
| 播放 | audioTrack.play() | 加入 PlaybackThread 混音队列 |
| 低延迟 | PERFORMANCE_MODE_LOW_LATENCY | FastMixer + SCHED_FIFO |
| 释放 | audioTrack.release() | 共享内存解映射 + Track 销毁 |
最重要的三个结论:
- 零拷贝是核心:App 和 AudioFlinger 共享同一块物理内存,
write()只是更新了指针,没有数据复制开销 - FAST Track 不是免费的:它要求你的音频参数与硬件完全一致,代价是无法使用 AudioEffect
write()不能在主线程:它会阻塞等待 Ring Buffer 空间,阻塞主线程会导致 ANR
下一篇,我们换个方向,看音频的"反向通道"——AudioRecord 音频录制流程,揭秘从麦克风到你的 read() 调用之间发生了什么。