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() 调用之间发生了什么。

相关推荐
青莲8434 小时前
查找算法详解
android·前端
青莲8434 小时前
排序算法详解
android·前端
zd2005724 小时前
用摩斯密码「听」时间:一款安卓报时应用的诞生
android
不会写代码的猴子5 小时前
Android17版本更新预览
android·android studio
用户41659673693556 小时前
记一次深坑:RecyclerView + FlexboxLayoutManager 导致 canScrollVertically 误判的剖析与修复
android
Be for thing7 小时前
Android 音频硬件(Codec / 喇叭 / 麦克风)原理 + 功耗与问题定位实战(手机 / 手表通用)
android·学习·智能手机·音视频
吉哥机顶盒刷机7 小时前
S905L3A/L3AB芯片迎来安卓14新纪元:Sicha移植版固件深度评测与刷机指南
android·经验分享·刷机
一个天蝎座 白勺 程序猿7 小时前
KingbaseES数据库MySQL兼容性解析:从TCO账本到“傻瓜式“迁移的密码
android·数据库·mysql·kingbasees