AudioTrack音频播放流程深度解析

上一篇我们搭好了"舞台"------分析了AudioFlinger的整体架构。这一篇我们来追一个具体的"演员":AudioTrack

如果你曾经用过 MediaPlayer 播放一首歌,或者用 SoundPool 触发游戏音效,甚至用 AudioTrack 直接写PCM数据,那这些操作的底层全都绕不开今天要讲的内容。AudioTrack 就是 App 进程和 audioserver 进程之间的"音频快递员",负责把你的音频数据高效、低延迟地送达声卡。

本文主要内容:

  1. AudioTrack API 核心参数和播放模式解析
  2. 创建流程 :从 Java Builder 到 AudioFlinger 完整调用链
  3. 共享内存 Ring Buffer:零拷贝数据传输的核心机制
  4. 播放控制状态机:play/pause/stop/flush 的内部实现
  5. FAST Track 低延迟路径:20ms 内延迟的秘密
  6. 实战案例:PCM播放器 + 低延迟游戏音效

源码版本 :Android 15 AOSP 关键路径frameworks/base/media/java/android/media/AudioTrack.javaframeworks/av/media/libaudioclient/AudioTrack.cpp


AudioTrack API 基础

两种播放模式的本质区别

AudioTrack 有两种播放模式,选错了轻则多占内存,重则产生明显延迟:

java 复制代码
// 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 + 提前加载才是正道。

关键参数详解

java 复制代码
// 获取最小缓冲区大小(这是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 不只是标签,它直接影响音频路由焦点管理

java 复制代码
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()

java 复制代码
// 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 中,这里做了几件关键事情:

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 建立连接

cpp 复制代码
// 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 调用

cpp 复制代码
// 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() 做了什么?

cpp 复制代码
// 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 控制块的结构

cpp 复制代码
// 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(写指针):

ini 复制代码
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() 的内部实现

cpp 复制代码
// 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)进行同步,大多数情况下不需要进入内核:

cpp 复制代码
// 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 有一套清晰的状态机,搞清楚状态转换能避免很多坑:

scss 复制代码
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() 的内部流程

cpp 复制代码
// 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:"我有数据了,请把我加入混音队列"

cpp 复制代码
// 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 从头开始写新数据
java 复制代码
// 正确的暂停/恢复
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 有严格的条件检查:

cpp 复制代码
// 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 最大的区别是实时调度策略

cpp 复制代码
// 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, &param);

    // 使用 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) 无锁队列:

cpp 复制代码
// 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

java 复制代码
// 方法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 最基础的用法,适合需要完全控制音频数据的场景(如自定义解码器输出):

java 复制代码
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

java 复制代码
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();
    }
}

案例三:获取精确播放时间戳

音视频同步场景下,需要精确知道当前播放到哪一帧:

java 复制代码
// 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 状态

bash 复制代码
# 查看所有 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(欠载)

bash 复制代码
# 查看欠载统计
adb shell dumpsys media.audio_flinger | grep -i underrun

# 典型输出:
# underruns: total=10, recent=2
# 如果 recent 持续增加,说明 App 的 write() 速度跟不上消费速度

Systrace 分析音频延迟

bash 复制代码
# 抓取音频相关的 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

常见问题排查

问题:播放时有断音、爆音

bash 复制代码
# 方法1:检查 underrun
adb shell dumpsys media.audio_flinger | grep "underruns"

# 方法2:检查是否有主线程调用 write()
# write() 在缓冲区满时会阻塞!如果在主线程,会导致 ANR
# 解决:始终在专用线程调用 write()

问题:明明是 48000Hz 却没进 FAST Track

bash 复制代码
# 检查设备实际采样率
adb shell dumpsys media.audio_flinger | grep "Sampling rate"

# 检查是否有 AudioEffect 被附加
adb shell dumpsys media.audio_flinger | grep "EffectChain"

问题:getMinBufferSize() 返回 -1(ERROR_BAD_VALUE)

java 复制代码
// 原因:采样率或格式不支持
// 安全的采样率检查
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 销毁

最重要的三个结论

  1. 零拷贝是核心 :App 和 AudioFlinger 共享同一块物理内存,write() 只是更新了指针,没有数据复制开销
  2. FAST Track 不是免费的:它要求你的音频参数与硬件完全一致,代价是无法使用 AudioEffect
  3. write() 不能在主线程:它会阻塞等待 Ring Buffer 空间,阻塞主线程会导致 ANR

下一篇,我们换个方向,看音频的"反向通道"------AudioRecord 音频录制流程 ,揭秘从麦克风到你的 read() 调用之间发生了什么。

相关推荐
菜鸟国国4 分钟前
一步到位学 Compose + Paging3:从 0 到 1 实现分页加载(超详细新手教程)
android
TO_ZRG9 分钟前
Android Service基础
android
ECT-OS-JiuHuaShan1 小时前
功夫不负匠心人,渡劫代谢舞沧桑
android·开发语言·人工智能·算法·机器学习·kotlin·拓扑学
ZC跨境爬虫3 小时前
移动端爬虫工具Fiddler完整配置流程:PC+安卓模拟器全覆盖,零基础一次配置成功
android·前端·爬虫·测试工具·fiddler
巴德鸟3 小时前
DaVinci 常用技巧 关键帧 自动字幕 追踪 音频 冻结帧 快捷键 多轨道字幕 扩充边缘
android·编辑器·音视频·视频·davinci·davin
学习使我健康3 小时前
Android 广播介绍详情
android·开发语言·kotlin
dalancon4 小时前
AudioTrack Start 执行流程分析
android
众少成多积小致巨4 小时前
Android 初始化语言入门
android·linux·c++
Carson带你学Android5 小时前
谁才是地表最强 Android Agent 大模型?Google官方测评来了!
android·openai
followYouself5 小时前
ASM开源库实现函数耗时插桩
android·asm·asm插桩·字节码插桩