☰
Android 13音频混音链路深度解析:AudioFlinger核心工作原理
2026/10/2 7:39:11 网站建设 项目流程

搞音频底层调试这些年,最绕不开的就是AudioFlinger。不管是App播放无声、HAL层引入杂音,还是延迟怎么压都压不下去,最后几乎都要回到那条核心链路上:AudioFlinger拿到各App的音频数据,通过混音线程把它们叠加成一份PCM,再交给Audio HAL输出。

Android 13上这条链路其实变化不小,但核心骨架还是prepareTracks_l决定本轮混哪些音轨、threadLoop_mix执行混音、threadLoop_write把结果送出去。这篇文章就把这三个阶段彻底拆开,从源码级别的判断逻辑到排查真实问题时的分析路径,给正在啃AudioFlinger的同学一条参考线索。内容适合做系统音频开发、ROM移植或者HAL适配的工程师,纯框架应用层开发也可以当背景知识储备。

1. 先把整个混音链路摆到桌面上:AudioFlinger在音频栈里的定位

1.1 一个Audio请求从App到HAL的完整旅程

我们平时在App里调用AudioTrack.write(),数据并不会直接进到AudioFlinger。中间要经过AudioTrack(Java层/NDK层)到native的AudioTrack,再通过binder跨进程到AudioFlinger的Track。到了AudioFlinger内部之后,根据路由策略,这个Track会被挂到某个MixerThread(混音线程)上。之后混音线程每一轮循环都会做三件事:分析本轮的音轨状态(prepareTracks_l)、把需要发声的Track数据叠加到一起(threadLoop_mix)、把叠加后的PCM数据写进输出设备(threadLoop_write)。

很多初学者会把注意力放在AudioTrack构造参数、audio_policy_configuration.xml这些外围配置上,这没错。但真正决定播放质量、延迟表现和异常行为的地方,全都在AudioFlinger这轮循环的判断里。比如一个Track明明在play,但prepareTracks_l里判定它为IDLE,那么本轮混音就直接跳过它了;再比如一个Track延迟太大,往往是prepareTracks_l里对mFramesReady的判断不够及时,导致数据在buffer里等待过久。

1.2 mixThread与fast mixer:两套混音路径的选择逻辑

Android的AudioFlinger里主要有两种混音执行路径,一个是标准的MixerThread,另一个是FastMixer。标准路径就是我们这篇文章的主线,它负责大多数常规音频流,采样率、通道数按原样混音,周期一般对齐到20ms左右的buffer。FastMixer是后来为了降低延迟引入的轻量混音器,跑在独立的高优先级线程里(sched_fifo),专门处理低延迟场景,比如按键音、游戏音效。

FastMixer出现之后,很多同学以为标准MixerThread已经不重要了。实际上只要是正常的媒体播放、后台通知、混响处理,绝大多数都是走标准MixerThread。FastMixer只服务特定条件下的快速通道,而且它最终产生的数据同样要汇入输出。真正要理解AudioFlinger的核心,标准MixerThread的prepareTracks_l到threadLoop_write是躲不开的主路径。

1.3 顺着类关系认识混音主角

围绕这条链路,几个核心类得先眼熟:

  • AudioFlinger:音频服务的总管理类,负责创建Track、管理线程、对外暴露binder接口。
  • MixerThread:继承自ThreadBase,是标准混音线程,内部维护了一堆Track和几个输出相关的对象。
  • Track:代表一路音频流,AudioFlinger内部对客户端的Track封装,保存着buffer、音量、状态机等。
  • Mixer / MixerBase:真正干混音体力活的核心,把多路Track的数据按系数叠加到mMixBuffer里。
  • AudioStreamOut:对Audio HAL输出流的封装,threadLoop_write最终就是调用它。

理解这层关系之后,我们再顺着MixerThread的主循环一点一点往里钻。

2. prepareTracks_l:每轮混音开始前的那次“排兵布阵”

2.1 prepareTracks_l的输入输出与整体职责

在MixerThread::threadLoop()的每一轮里,最先调用的就是prepareTracks_l()。函数声明基本是这样:

status_t AudioFlinger::MixerThread::prepareTracks_l( const Vector< sp<Track> > &tracks, Vector< sp<Track> > *tracksToRemove)

注意这个_l后缀,表示调用者必须持有线程锁。它接收当前线程维护的所有Track列表,同时带出一个输出参数tracksToRemove,本轮判定需要销毁的Track会塞进这个vector,稍后统一清理。

这个函数的核心职责可以用一句话概括:遍历所有Track,逐个判断它的状态、是否需要混音、本轮需要混多少帧,并计算所需要的音量、ramp参数,最后汇总出本轮混音状态mMixerStatus。这个状态直接决定了threadLoop后续是执行mix、还是只写静音数据、或者干脆进入standby。

2.2 音轨状态机:为什么一个Track会有这么多状态要处理

Android的Track内部有一套状态机,涉及IDLE、STOPPED、STOPPING_1、STOPPING_2、PAUSED、RESUMING、ACTIVE等。很多刚接触源码的同学会被这么多种状态吓到,但其实它们都是为了精确处理“播放结束前数据是否完全送出”这个问题。

以一段正常播放结束为例:应用调用stop()后,Track并不会立刻从列表里消失。AudioFlinger要保证已经送进Track buffer的数据还能被混音输出完,所以就有了STOPPING_1、STOPPING_2这种渐进状态。在prepareTracks_l里,对处于STOPPING_1的Track,代码会检查track->presentationCompleteFrames(),看看是否已经写到指定帧数,如果还没写够,就继续让它混音;写够了才切到STOPPING_2,再往后才是remove。

这个过程如果你在开发中遇到了“应用stop之后声音还持续了一小会儿”的现象,不要慌,很可能不是bug,而是状态机在按节奏走完剩余数据。

2.3 Track状态的逐个分支判断

我们来看一段高度简化但思路一致的源码逻辑(基于Android 13 AOSP,细节做了精简):

for (size_t i = 0; i < count; i++) { const sp<Track> &track = tracks[i]; // 判断状态 if (track->isStopped()) { // 不参与混音,可能塞进tracksToRemove continue; } if (track->isPaused()) { // 暂停的Track也不参与混音 continue; } // 一个关键的判断:这轮需要给这个Track多少帧 size_t framesReady = track->mFramesReady; size_t framesToBePresented = track->mFramesToBePresented; if (framesReady >= framesToBePresented) { // 数据够了,可以混 track->mFramesToBePresented = 0; // 或者按算法推进 } else { // 数据不够,这个Track就不进入本轮混音 allTracksReady = false; } }

当然真实代码远比我这里写的复杂,还涉及mFramesToBePresented和maxFrames的换算、reloadVolume、limitBufferSizeIfNeeded等等。但主干就是这个逻辑:数据没准备好的Track,本轮不混,整体再判断还有没有其他Track准备好了,以此决定mMixerStatus是MIXER_TRACKS_READY还是MIXER_TRACKS_ENABLED。

这里我推荐读源码时以AOSP的frameworks/av/services/audioflinger/Threads.cpp为准,搜索status_t AudioFlinger::MixerThread::prepareTracks_l,对照着读。因为Android版本演进会导致部分分支名称变化,但只要抓住状态判断和数据量判断这两条线,就不会迷路。

2.4 音量、mute、ramp参数是怎么准备出来的

prepareTracks_l里还有一项核心输出:本轮混音时每个Track的音量系数。很多初学同学以为音量是应用直接传一个0.0到1.0的值,然后混音时拿这个值乘一下PCM就完了。

真实情况是:这里要处理主音量、streamType音量、应用音量、mute状态、ramp渐变等多层因素。Track内部保存了mVoiceVolume、mVolume[0]、mVolume[1]等参数,其中mVolume[0]代表左声道系数,mVolume[1]代表右声道系数。

在prepareTracks_l里,会调用类似track->getAudioVolumeLocked()的接口,把stream音量、主音量、mute这些都叠加上去,得到target volume,然后交给后续Mixer去执行ramp。如果目标音量和当前音量不一致,就会计算一个增量(volumeInc),让混音过程中音量从当前值平滑变化到目标值,防止爆音。

这也就是为什么你用dumpsys audio_flinger能查看到类似mVolume[0] = 0.5000这样的信息——那是不带ramp的最终目标值,实际每一帧还有一个渐变中的当前值。

2.5 从Track状态汇总出mMixerStatus

所有Track遍历完成后,prepareTracks_l会根据前面统计的结果设置mMixerStatus,主要有几个可能值:

  • MIXER_TRACKS_READY:本轮有Track需要混音,threadLoop后续会执行threadLoop_mix。
  • MIXER_TRACKS_ENABLED:虽然还有Track挂在线程上,但当前没有需要混音的数据。
  • MIXER_TRACKS_NONE:线上已经没有活跃Track了,可以进入standby或者sleep。

有些版本里还会有一个MIXER_TRACKS_FAST之类的状态,用于标记FastMixer相关路径,但在标准线程里一般还是以上面三种为主。这个返回值很大程度决定了threadLoop是干活还是睡觉,也是排查“播放无声”问题时最需要关注的状态点。

3. threadLoop真正开始混音:从volume ramp到mixing

3.1 混音buffer格式:float与Q8.23同台

prepareTracks_l忙活完之后,如果mMixerStatus是MIXER_TRACKS_READY,threadLoop紧接着就会进入threadLoop_mix()。内部核心就一行mMixer->process(),但这行的背后是整套混音引擎的运转。

先理解buffer格式。Android Mixer内部处理PCM数据时,通常会先把各种格式(PCM 16bit、PCM Float、PCM 24bit等)统一到float或者Q8.23格式,再进行混音叠加。之所以这么做,是因为不同音轨的格式、音量系数不同,如果直接在16bit整数域叠加,每次加法都可能产生溢出或截断误差,声音质量会明显下降;而float或者Q8.23动态范围大,叠加之后再做一次饱和转换,失真小很多。

从Android 8之后,Mixer已经支持按Track独立设置格式,AudioMixer会根据Track的格式选择不同的混音kernel(比如16bit的kernel和float的kernel)。在Android 13里,float路径是主流,特别是低延迟场景基本都会走到float混音。

3.2 MixerBase::process:从混音主循环到Track volume计算

打开MixerBase的源码,核心方法process的逻辑可以概括为:

  1. 清空输出buffer(mMixBuffer),以目标格式的静音值为基准。
  2. 遍历所有active的mixer channel(每个Track对应一个channel)。
  3. 对每个channel执行volumeRamp计算,把目标音量转换成每一帧的当前音量增量。
  4. 调用对应格式的混音函数,把Track的输入数据叠加到mMixBuffer上。
  5. 混音结束后,做后处理(比如dithering、limiter等,视配置而定)。

这里最值得注意的就是volumeRamp。在最终混音时不是直接拿目标音量乘所有帧,而是用volumeInc做逐帧渐变。比如目标音量从0.0升到0.8,假设有320帧,那么每一帧大概增加0.0025,这样能有效避免增益突变造成“啵”的爆音声。你耳机里听到的那种清脆的“啪”声,很多时候就是因为某个环节音量突变没做ramp。

一个简化版本的ramp循环大致长这样:

for (uint32_t i = 0; i < framesToMix; i++) { output[i] = input[i] * currentVolume; currentVolume += volumeInc; }

实际代码里为了性能,会用定点数或SIMD优化,但原理就是上面这样逐帧渐变。调试时如果想验证某个杂音是否由ramp引起,可以临时把volumeInc设成0,对比声音表现。

3.3 混音主循环中的采样率转换

混音线程还需要处理一个重要问题:不同Track的采样率可能和输出设备不一致。比如输出设备是48kHz,但某个音效是44.1kHz。如果直接叠加数据,会出现音调偏移和速度偏移。

AudioFlinger的解决办法是在Track数据进入Mixer之前,通过AudioResampler把采样率转换成线程输出采样率。这一层转换在Track的上游完成,对Mixer来说是透明的——混音器拿到的都是统一采样率、统一通道数的buffer。这也是为什么系统音频开发里经常说“采样率转换是绕不开的性能开销”,不同采样率的流越多,重采样计算量越大,耗电和CPU占用也就越高。

3.4 从源码看threadLoop_mix的整体结构

在Threads.cpp里,threadLoop_mix看起来非常简单:

void AudioFlinger::MixerThread::threadLoop_mix() { // mix all the tracks if (mMixerStatus == MIXER_TRACKS_READY) { mMixer->process(); } }

但mMixer->process()之前还有一步经常被忽视:prepareTracks_l里已经通过setTrackFormat、setTrackChannelMask等接口和Mixer沟通好了本轮的一个个参数。也就是说,prepareTracks_l不只是简单的“决策”,它还负责把决策结果灌进Mixer的各个hook点。如果你只看threadLoop_mix,根本看不出混音参数的来源,这也是我建议大家从prepareTracks_l开始读的原因。

另外,如果mMixerStatus不是READY,而是ENABLED,那么threadLoop_mix不会执行,之后threadLoop会根据情况决定是写静音数据还是进入standby。这对应到现象就是:应用还挂着,但没声音输出时,底层其实在不断写静音帧或者直接睡大觉。

4. threadLoop_write:把混音结果交给HAL的最后一步

4.1 write前到底发生了什么

混音完成之后,数据已经安安静静躺在mMixBuffer里了。接下来就是threadLoop_write的舞台。在正式调用write之前,源码里会有一系列簿记工作,比如:

  • 把mMixBuffer交给mOutput之前,先更新mFramesWritten、mBytesWritten等统计信息。
  • 处理callbackThread(用于通知AudioTrack数据写入进度、触发releaseBuffer等)。
  • 检查standby状态,避免在没有数据可写时还频繁调用HAL write。

你会发现,threadLoop_write不只是“写buffer”这么简单,它其实是这一轮threadLoop循环里和客户端、HAL产生双边交互的出口。

4.2 mOutput->write走下去:从MixerThread到AudioStreamOut

看Threads.cpp里的threadLoop_write,核心调用非常简单:

ssize_t AudioFlinger::MixerThread::threadLoop_write() { // ... ssize_t ret = mOutput->write(mMixBuffer, mMixBufferSize); // ... }

这个mOutput实际上是一个AudioStreamOut的封装,它内部会调用到Audio HAL的输出流write方法。如果Android 13设备用的是AIDL audio HAL,那么这里最终会通过AIDL调用到HAL实现里,底层再写到声卡驱动或者后端混音器。

值得注意的一点是,mMixBufferSize并不是随便定的,它通常等于mNormalFrameCount * frameSize,而mNormalFrameCount又跟输出设备的period大小和采样率有关。调试杂音、断音问题时,这个值直接决定每轮写入的数据量,如果太小,系统容易underrun;如果太大,延迟又会变高。

4.3 standby、timestamp与presentation complete

threadLoop_write除了调用write之外,还有一个重要分支:判断是否进入standby。比如本轮没有任何活跃Track,且write已经完成,MixerThread可能就会在下一轮sleep,不再持续调用HAL write。这个设计可以显著降低空转功耗。但对开发调试来说,standby逻辑有时会干扰我们观察问题。你dump audio_flinger时会看到类似State: STANDBY的标记,这就说明线程当前没有在工作。

另外,Android音频栈里还有一个很关键的概念:timestamp。每次write完成,AudioFlinger会记录本次写入对应的呈现时间戳pthread时间,用来帮助上层AudioTrack计算延迟和播放进度。这个时间戳是在write路径中更新的,但计算逻辑会牵涉到HAL支持的getPresentationPosition与否。如果HAL没有实现getPresentationPosition,AudioFlinger就只能用软件估算,延迟数据会不太准。

5. Android 13的实际代码变化与排查经验

5.1 Android 13在AudioFlinger上值得关注的变化

Android 13的AudioFlinger延续了近几年的演进,有一点对做系统适配的同学很关键:audio HAL逐步从HIDL迁到AIDL,AudioFlinger内部对AudioStreamOut的接口调用也做了适配。如果你手头的设备在Android 13上出现无声、断音,第一步不一定要去查混音逻辑,可以先用dumpsys audio确认当前走的是AIDL HAL还是HIDL兼容层。很多时候混音线程一切正常,但AIDL接口的write实现没有正确下发数据,表现出来跟混音问题一模一样。

另一个让人注意的点是Android 13在audio flinger代码里对std::vector等现代C++用得更彻底,老的Vector被清理得比之前更激进。我们读老版本代码或者网上的八股文时经常会看到旧风格的容器代码,在A13上会有些对不上,这是正常的,别在意。

5.2 常见问题速查表:underrun、杂音、无声

下面这张表是我在实际调试中遇到频率最高的几类问题,都跟混音链路直接相关。

现象可能原因排查切入点
播放时有“咔哒”爆音volume ramp缺失或参数不对查prepareTracks_l的volume计算,确认目标音量突变时有没有ramp
声音断断续续、周期性卡顿underrun,HAL write比数据生产快查mNormalFrameCount是否过小,dumpsys中看看underrun计数
应用播放无声但状态正常Track在prepareTracks_l被判定为数据不够查mFramesReady和mFramesToBePresented,确认buffer填充是否正常
多路音频混音后声音发闷混音时发生clip或格式转换损失确认mixer输入输出格式,打开dumpsys看mixer format
低延迟场景延迟依然高没走到FastMixer路径查线程调度策略和FastMixer的启用条件,确认track flag是否满足

一个排查小技巧:使用dumpsys media.audio_flinger可以直接看到MixerThread内部每路Track的状态、音量、帧数统计。当怀疑某路Track没参与混音时,先在dump结果里找mMixerStatus和每个track的mState,这一下能帮你判断问题是出在上游数据、prepare阶段还是write阶段。

5.3 调试混音链路的实用手段

除了dumpsys,我在调这类问题时经常会开systrace抓audio相关的trace事件。AudioFlinger内部在prepareTracks_l、threadLoop_mix、threadLoop_write附近都有trace打点,抓完trace能直接看到每一轮循环的执行耗时和调用频率。如果发现threadLoop_write耗时很高,多半是HAL的write阻塞;如果threadLoop_mix耗时很高,那就是混音计算压力大,或者采样率转换太重。

还有一个很实用的临时验证方法:在HAL层write函数里加一个统计,记录每次写入的时间戳和字节数。如果时间戳间隔抖动很大,基本可以断定是调度优先级或者CPU抢占问题,重点去查线程优先级、CPU affinity以及有没有被其他高负载任务抢了CPU时间。很多“无声”“断音”的根因根本不在音频代码本身,而是音频线程被饿死了。

5.4 动手改造时的三条经验

最后分享几条我在做Android 13音频适配时沉淀下来的经验,不一定适用于所有设备,但大概率能帮你少走弯路。

第一条:改混音参数前,先确认输出的buffer size是否合理。mNormalFrameCount太小,混音再快也会underrun;太大则延迟飙升。Android 13里HAL属性audio.hw.depth、audio.offload.buffer.size.kbytes这些参数要配合着看效果。

第二条:做多路混音测试时,尽量覆盖不同采样率、不同格式的音轨组合。只测48kHz/16bit的单路播放,基本测不出混音引擎的真实水平。44.1kHz和48kHz的Track同时播放,才能暴露重采样路径的性能和精度。

第三条:改完代码一定重新抓一遍trace和dumpsys,前后对比。不要只凭耳朵判断声音正不正常,因为某些失真在耳机和喇叭上表现完全不同。用dumpsys里的underrun计数和write耗时变化来量化改动效果,比纯主观试听靠谱得多。

我自己在调这类问题时的习惯是,先把代码路径完整走一遍,拿到dump数据再动手改。音频系统不像应用层,很多时候一个音量ramp没做对,表现出的声音问题会让你怀疑整个HAL都挂了。但只要确认prepareTracks_l里状态判断正常、volume计算正常,混音引擎的process也按预期执行了,那剩下的问题九成都在write这条出路上,排查范围一下就缩小了很多。希望这篇文章能帮你把从prepareTracks_l到threadLoop_write这条链路彻底吃透。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询