1. 你每天听到的声音,到底是怎么从App走到喇叭的
做Android系统开发的人,几乎都绕不过音频这座山。尤其是Qcom平台,从上层App调用AudioTrack,到声音真正从扬声器或者耳机里出来,中间隔着一整套复杂的软件栈和硬件链路。很多刚转到系统方向的朋友,第一次接触音频框架时都会被这一堆概念砸晕,什么FE、BE、ADSP、ACDB、DAPM、offload,每个词单拎出来好像都懂,串在一起就懵了。
这篇文章我想把这整条链路彻底拆开,用我能讲出来的最简单方式,把Android Qcom音频架构从应用层一路拆到底层驱动和DSP。你不需要先精通内核,也不需要先啃完ALSA文档,只要跟着我的思路走一遍,就能在脑海里建立起一张完整的脉络图。
这篇内容适合谁看?做Android Framework开发的、做Qcom平台BSP的、做音频算法集成的、还有那些被“设备无声”、“蓝牙没声音”、“录音有回声”这类bug折磨的测试和开发同学。看完你至少能知道:问题报上来之后,我应该从哪一层开始查,每一层都有哪些关键节点,以及ADSP在里面到底干了什么活。
先说结论:整个音频架构的本质,就是一套“分工明确的生产流水线”。上层的App是“下单客户”,AudioFlinger是“调度中心”,HAL层是“车间主任”,内核ALSA驱动是“传送带”,ADSP则是“高级加工车间”。声音从客户下单,到最终出厂被扬声器播放出来,每一层都有自己的职责,少了任何一环都玩不转。
拿我们最常用的“播放一首歌”举例:音乐App通过AudioTrack把PCM数据丢给AudioFlinger,AudioFlinger根据策略找到合适的输出设备(扬声器、耳机还是蓝牙),然后通过HAL把数据写到内核的ALSA接口,内核再通过I2S、SLIMbus这些物理总线把数据送给Codec或者ADSP,最后由Codec完成数模转换,驱动扬声器发声。这一条线跑下来,就是整篇文章的主角。
2. 为什么要分FE和BE:Android音频框架的分层设计哲学
2.1 从HAL掉了一层皮开始说起
我第一次看Qcom音频代码时,第一反应是:为什么一个简单的播放操作,要整出这么多结构体和层级?直接在App里写个驱动把数据送出去不行吗?后来被现实教育了——Android要跑的设备太多了,每家的Codec不一样,每家的DSP方案不一样,如果Framework层直接面对硬件,那Google每出一个版本,所有芯片厂商都得重写一遍系统。
所以Google在中间塞了一层HAL(Hardware Abstraction Layer)。在Android 8.0之前是audio_hw_modules,到了Android 8.0之后演进成了HIDL(Android 9之后逐步转向AIDL),但万变不离其宗:HAL层把上层复杂的策略逻辑和底层的硬件细节彻底隔离开。
Qcom在这层之上,又根据自己的硬件架构做了一层抽象,这才有了我们常说的FE和BE的概念。FE是前端(Front End),代表的是上层能看到、能操作的音频端点,比如Primary output、Deep buffer playback、FM、VoIP这些。BE是后端(Back End),连接的是物理硬件端口,比如I2S0、SLIMBUS0、MI2S1这些。
简单说:FE负责和上层打交道,BE负责和硬件打交道,中间的桥梁由ALSA和ASoC框架来搭。你从音频的视角看,系统里有多少个声卡,每个声卡上有多少个pcm设备,这些都是FE层面的呈现。而每个pcm设备内部,通过路由判定最终连到哪个BE上,这就是声音从软件走向硬件的最后一公里。
2.2 ASoC框架在Qcom平台上的落地
Qcom的音频驱动是基于ASoC(ALSA System on Chip)框架实现的。ASoC框架把音频驱动拆成了三块:Codec驱动、Platform驱动和Machine驱动。Codec驱动管DAC/ADC这些数模转换器件,Platform驱动管DMA和CPU侧的DAI(Digital Audio Interface),Machine驱动则负责把Codec和Platform绑在一起,并定义两者之间的DAI Link。
在Qcom平台上,Machine驱动一般就是那个snd-soc-msmXXX,它会把平台自带的音频接口封装成一个个snd_soc_dai_link,每个dai_link里定义了它用的是哪个FE、哪个BE、中间经过哪些路由。如果你在设备上跑cat /proc/asound/cards,看到类似msm8952-snd-card这种名字,那就是Machine驱动注册出来的逻辑声卡。
FE和BE在ASoC里的对应关系是怎样的?以播放为例,上层写入的PCM数据会走到FE的dai_link上,然后由DAPM(Dynamic Audio Power Management)机制根据当前的路由状态,把数据通路切到对应的BE上。DAPM是整个音频驱动的精髓,它不光是电源管理,还负责音频路径的动态切换。你插上耳机,DAPM会自动把通路从扬声器切到耳机,这个切换不是凭空发生的,而是由一系列kcontrol的开关状态决定的。
这可能听起来有点抽象,但我建议你直接在设备上跑一个命令:tinymix。这个命令会打印出当前所有音频控件的状态,你会发现里面有很多类似RX1 MIX1_INP0、SLIM RX0、PRI_MI2S_TX这样的名字。这些名字背后,就是一条条实际的硬件通路。理解了这些控件的含义和它们之间的连接关系,再去分析路由问题,基本就等于开了天眼。
2.3 FE不只是一个pcm设备那么简单
很多做应用开发的同学觉得,FE不就是个声卡上的pcm节点吗?我open一个pcm设备,write数据进去,就能出声。话是没错,但在Qcom平台上,FE远不止这么简单。
同样是一个pcm设备,根据不同的使用场景,Qcom会定义出不同的pcm类型。最常见的有:primary(普通播放)、deep_buffer(低功耗长缓冲播放)、compress_offload(压缩音频直通)、voip(语音通话)、voice_call(CSD语音)、FM(收音机)等。每种类型的pcm设备,在HAL层对应不同的音频路径和buffer配置,也对应不同的DSP处理流程。
比如播放一首MP3,传统方式是把MP3解成PCM再送过去播放,这需要CPU持续参与;但Qcom支持compress offload,App只需要把压缩码流直接丢给ADSP,ADSP内部完成解码、混音、EQ、左右声道平衡等一系列处理,然后直接输出到Codec。这个过程CPU几乎不参与,功耗大大降低。这不只是省电的问题,在手机这种对续航极其敏感的设备上,这种架构直接决定了产品体验的优劣。
理解FE的多样性,对后面排查问题至关重要。比如“播放音乐有杂音”和“通话有回声”,虽然都是音频问题,但它们走的FE完全不一样,牵涉的底层模块也完全不同。如果你一上来就在primary pcm的设备上抓数据,去分析一个发生在voip链路上的回声问题,那必然一无所获。
3. ADSP到底是干什么的:音频处理的大脑与神经中枢
3.1 为什么要单独搞一个处理器来管声音
手机主CPU(Application Processor,AP)要管的事情太多了,跑App、渲染UI、处理网络、跑各种后台任务。如果音频处理这块也全部交给AP,一方面会占用CPU资源,另一方面在功耗和延迟上也很难做到最优。Qcom的解决办法是:把音频相关的计算任务从CPU上搬走,交给一颗专用的数字信号处理器,也就是我们常说的ADSP(Audio Digital Signal Processor)。
ADSP在Qcom的音频架构里扮演的角色,相当于一个“音频处理大脑”。主CPU把音频数据通过共享内存或者总线发送给ADSP,ADSP内部完成音效处理、回声消除、噪音抑制、重采样、混音等任务之后,再把处理好的数据通过物理总线送给Codec或者直接驱动扬声器。
这个架构带来最直接的好处是:即便屏幕息屏、CPU进入低功耗状态,音乐依然可以正常播放;即便同时开着音乐、导航、电话,ADSP也能通过内部的多个session完成混音与路由切换。这就是为什么Qcom的音频方案能支持如今这么多复杂的并发场景。
3.2 ADSP上的关键处理模块
ADSP内部的软件框架,过去是QDSP6(Qualcomm Digital Signal Processor version 6),现在已经演进到更复杂的模块化架构,但核心的处理思路是一致的。在ADSP里,音频数据流被组织成一个个session(会话),每个session内部可以挂载不同的模块(modules),这些模块对数据流做各种处理。
常见的模块包括:音量控制(Volume Control)、采样率转换(Sample Rate Converter)、混音器(Muxer)、回声消除(AEC)、噪音抑制(NS)、自动增益控制(AGC)、各种音效(EQ、Bass Boost、Reverb)等。这些模块在ADSP上排列组合,就形成了一条条完整的音频处理流水线。
这个处理流水线和你在应用层用AudioEffect做音效是完全不同的。应用层的音效处理,数据流还在AP侧,会占用CPU;而ADSP里的音效处理,是在DSP内部完成,不需要占用AP的CPU资源。因为这种机制,Qcom才敢把Dolby、DTS、Dirac这些重度的音频后处理算法全部放到ADSP上跑,实现真正的零CPU开销。
3.3 ACDB与Calibration:决定声音好不好听的关键
ADSP负责处理数据,而ACDB(Audio Calibration Database)负责告诉ADSP:在某个设备上、某个采样率下、某个音量级别时,应该给音频数据加多少增益、做多少滤波、走哪条物理通路。ACDB里存的是一张张校准表,每张表对应着不同的设备、不同的路径、不同的场景。
你插上耳机、拔掉耳机、调到最大音量、切换通话音量,这些动作背后都对应着ACDB里的一次查询和加载。Qcom在HAL层有一个专门的库叫libacdb,它负责管理ACDB的加载和应用。如果你调过音频音量觉得“最大声也小”、“底噪大”、“开EQ有破音”,多半需要去ACDB里调整对应的gain值。
ACDB的数据来源通常是音频调试工程师在实验室里通过QACT(Qualcomm Audio Calibration Tool)调出来的。Qcom平台常见流程是:硬件工程师先在实验室用音频分析仪测量器件的频响曲线、失真、信噪比等指标,然后调试工程师把这些指标转换成ACDB里的参数,最终经过几轮主观听音和客观数据验证后,把校准表编到设备的固件或者文件系统里。
这部分很多做上层开发的同事很少接触,但恰恰是这种“黑盒子”最容易出问题。比如你遇到“耳机有电流声”,不要只会看DAPM路由,先用tinymix确认对应设备的PA(功放)有没有打开、ACDB里对应的PRI_MI2S_RX的gain是否合理,有时候问题根本不在数据通路,而在于电源和地没处理好——这类问题音频驱动工程师往往是第一个背锅的。
4. 由外到内看链路:从AudioTrack到扬声器的一次完整旅行
4.1 上层的声音是怎么进到AudioFlinger的
现在我们把视角拉回到最顶层,看看一个App调用AudioTrack.play()之后,声音数据经历了什么。
App通过JNI调用Framework层的android.media.AudioTrack,最终通过Binder调用到AudioFlinger。AudioFlinger是Android音频系统的核心服务之一,它管理着系统中所有的音频流(track)、混音线程(mixer thread)和输出设备。它会根据上层传入的音频属性(采样率、声道、格式、使用场景等)和当前的音频策略,决定这个track要放到哪个播放线程,以及最终输出到哪个设备。
这里就有一个很关键的概念:音频策略(AudioPolicy)。Android有一套独立的音频策略服务,叫AudioPolicyManager,它在Android 13之后已经从原来的audio_policy模块升级到了更细粒度的AIDL版本。策略服务负责回答一个问题:“当前这个播放请求,应该用哪个输出设备、走哪条路径?”
比如你在播放音乐时插入耳机,AudioPolicyManager会收到设备切换事件,然后它会看当前播放的track属性,如果track是可暂停的,它会先把track暂停,切换到耳机通路后重新恢复播放。这中间的设备切换、路由切换、音量控制、音效迁移,全部是策略服务在驱动。你平时遇到的“插拔耳机后音乐停了”、“蓝牙耳机连接后扬声器还响一下”,绝大多数问题都出自这一层的逻辑。
4.2 混音线程与FastMixer:让多个App同时出声的魔术
AudioFlinger内部有多个播放线程,每个线程管理一组音频流,并负责把这些流混音成一路PCM数据。最常见的几个线程分别是:mixer thread、fast mixer thread、offload thread、MMAP thread等。
常规的mixer thread处理普通播放任务,它使用固定的buffer大小和时间片,polling周期一般在20ms左右。而FastMixer是Android 4.4引入的一种低延迟混音机制,它通过一个高优先级的线程,每5ms左右就唤醒一次,专门处理那些对延迟要求高的音频流,比如游戏音效、按键音、VoIP等。
为什么手游对触控音效的延迟那么敏感?因为在游戏场景里,用户点击屏幕的动作和听到的声音反馈之间,如果延迟超过100ms,就会有明显的“拖沓感”。FastMixer能把这条链路的延迟压缩到几十毫秒以内,很大程度靠的就是抢占式的线程调度和更小的音频buffer。
混音之后的数据,会通过AudioFlinger的EffectChain,如果有启用的音效(比如均衡器、环绕声),会在这里做一次处理,然后通过HAL层的start_output_stream接口,把数据写入到对应的输出流中。这个时候,数据已经到达了HAL层,接下来就是Qcom驱动的主场了。
4.3 HAL层的路由选择与声卡节点
Qcom的HAL层实现,核心在hardware/qcom/audio/hal目录下。它围绕着一个核心结构体audio_device展开,这个结构体里定义了所有支持的声音设备,以及每个设备对应的pcm节点、路由信息、增益信息等。
当需要播放时,HAL会做这么几件事:先根据上层传来的audio_output_flags_t确定使用哪个pcm设备(primary还是deep_buffer还是compress_offload);然后打开对应的pcm节点(通过tinyalsa的接口);接着加载对应的ACDB校准数据;最后通过路由切换,把音频数据从FE引向对应的BE。
路由切换在HAL层有一套独立的逻辑,最核心的接口是select_devices。它根据当前的设备场景(voice_call、speaker、headphone、bluetooth_sco等),遍历一张预先定义好的路由表,逐一设置对应的kcontrol值。这些kcontrol最终映射到内核ALSA驱动的put函数里,完成硬件上的通路切换。
在Qcom平台上,如果音频链路出了问题,HAL层的audio_route以及底层的tinymix是你最先要用的两个武器。很多无声问题,查到HAL层就能定位了:硬件设备枚举正常、pcm也能open,但就是不出声,那多半是路由表里某个kcontrol没配对,或者ACDB里的通路没有加载成功。
4.4 内核ALSA与DAPM:驱动层面的最后一道关卡
当数据从HAL层写入到pcm设备后,内核里的ALSA驱动就开始接管了。在ASoC框架下,数据通过platform driver的pointer、copy这些回调函数,把用户空间传下来的buffer搬运到DMA缓冲区,再由DMA控制器按照配置好的传输格式,通过I2S、SLIMbus等接口把数据发往Codec或者ADSP方向。
如果数据是发给ADSP的,情况会稍微复杂一些:AP侧并没有直接的Codec,而是通过一个虚拟的音频设备传数据给ADSP。ADSP处理完之后,再通过SLIMbus等接口把数据送给物理Codec。在这个模式下,AP侧的数据链路相对短,但ADSP内部的处理链路非常长,一个问题可能出现在ADSP前级、ADSP内部、ADSP后级任何一段。
DAPM(Dynamic Audio Power Management)在这个过程里扮演的角色,是保证即便硬件通路异常复杂,也不会出现“放大器没开却把数据送到耳机”这种尴尬局面。DAPM会根据当前活跃的音频路径,自动启用或禁用相关组件的电源。在调试中,DAPM最常用来诊断两类问题:一是某个设备长时间没声音,二是切换路由后出现pop音(爆音)。前者多半是DAPM没把对应的电源打开,后者多半是DAPM在路径切换时没有按正确的时序执行上下电。
我自己调试过一个典型的pop音问题:拔掉耳机瞬间,扬声器会“啪”一声。从日志看,路由从耳机切回扬声器的过程里,扬声器的PA先被DAPM打开了,然后耳机通路才完全关闭,这两个动作之间产生了短暂的信号重叠。最后通过调整kcontrol的配置顺序,让耳机通路先关闭再开启扬声器PA,问题才彻底解决。这种问题,不深入到驱动层根本无从下手。
5. 实际调试一个音频问题:无声故障的完整排查实录
5.1 先复现,再抓日志,别急着猜
我在跟团队里新人讲音频问题排查时,永远强调第一件事:先复现,再抓日志,最后才开始猜问题。很多人在报障同学那边听完一句“播放无声”,就打开代码开始找,这不靠谱。不同的无声,背后的原因差着十万八千里;播放本地文件无声、播放网络流无声、呼叫铃声无声、刷视频无声,完全是不同的排查路径。
以最常见的一个例子:播放本地MP3无声。我通常的排查步骤是:第一步先确认这个MP3文件本身能不能正常放,换一个源试试;第二步确认是“所有音源都无声”还是“特定音源无声”,如果是前者,问题基本在底层,如果是后者,问题可能在上层的音效或解码格式上。
然后我会用adb shell dumpsys audio来看当前系统的音频状态。这个命令会输出当前所有播放线程、活动track、设备连接情况、路由信息等。重点看几个字段:active tracks里面有没有我们的track?它被分配到了哪个线程?devices当前连接的是什么设备?routes当前选择的路由是否和预期一致?如果track存在于线程上、但设备显示不正确,那问题可能出在设备切换逻辑上;如果线程上根本没有track,那就是上层没把播放请求真正发下来。
5.2 用dumpsys和tinymix定位断点
如果dumpsys显示track存在且路由正常,但依然无声,我会把数据流进一步下探到HAL层。这个时候tinymix就是最重要的工具。tinymix的全名可能有些朋友不熟,其实就是tinyalsa命令行工具,可以读取和设置当前ALSA设备的所有kcontrol寄存器。
操作方法是:播放的时候,在另一个shell里执行tinymix,把所有kcontrol的值打印出来,对照当前应该处于active状态的路径,检查对应的switch和volume是否打开。比如扬声器播放,应该检查类似RX3 MIX1 INP0这一个多路选择器是否选对了,RX3 Digital Volume音量是否不为0,SPK PA这个功放开关是否打开。任何一个环节是off或者0,声音就出不来。
还有一类问题是“pcm节点都正常但数据没出去”,这种就需要抓取pcm的dump。Tinyalsa自带一个神器:tinyplay配合tinypcminfo可以看到pcm节点当前打开的格式和buffer情况;如果你想确认DMA到底有没有搬运数据,可以抓/proc/asound/pcm目录下各个pcm子设备的状态,以及对应substream的hw_params。
如果看到hw_params的buffer size、period size和上层设置的完全不匹配,那就说明驱动配置或者HAL传入的参数有问题。反正音频问题的排查就是一层层剥洋葱:从App到Framework,从Framework到HAL,从HAL到内核,从内核到DSP,每一层都有对应工具和日志,只要你肯耐心逐层排除,问题一定能水落石出。
5.3 ADSP侧日志的抓取与分析
到了ADSP这一层,普通开发者就没什么直接工具可以用了。常用的办法是抓取内核日志中ADSP相关的部分,或者看/sys/kernel/debug/adspsd下的状态信息。在一些Qcom平台上,还可以通过adb shell cat /d/audio/audio_state之类的方式获取ADSP当前的session和模块状态。
我自己遇到过一种情况:播放正常,但声音每隔几秒卡顿一次。从上层看,track一直在跑,buffer也在正常写入,但声音就是周期性中断。后来在ADSP日志里发现,ADSP在做sample rate conversion时,因源采样率和输出采样率不匹配,导致内部buffer溢出。这是典型的“在DSP里做重采样”引发的次生问题,排查它只能在DSP侧找线索。
如果你手头的平台支持录音,还可以用tinycap抓一段ADSP处理后的数据,然后离线分析这段数据是否正常。这是判断问题出在ADSP之前还是之后的一个有效手段。比如播放无声,你在ADSP后端抓数据,如果抓到的数据已经是全0或者噪声,那问题基本锁在ADSP内部;如果数据正常但扬声器依然不出声,那就要把矛头指向Codec和模拟链路。
5.4 常见问题速查表
遇到音频问题不要慌,对照这个表可以缩小排查范围:
| 症状 | 可能原因 | 优先排查方向 |
|---|---|---|
| 全部媒体无声 | 默认输出设备路由异常或声卡初始化失败 | dumpsys audio查看设备连接,tinymix确认路由 |
| 特定App无声 | App音效处理异常或AudioTrack参数错误 | 换播放源验证,检查App使用AudioTrack参数 |
| 插拔耳机无响应 | AudioPolicy设备切换逻辑异常 | dumpsys audio_acl查看设备列表,检查耳机检测中断 |
| 蓝牙播放无声 | A2DP offload加载失败或协议栈异常 | 抓BT日志,确认offload session状态 |
| 通话有回声 | AEC未使能或参考信号接错 | 查询ADSP回声消除模块状态,检查AEC校准 |
| 播放有pop音 | DAPM路由切换时序问题 | 检查kcontrol切换顺序,调整上下电时序 |
| 音量调节不生效 | HAL层音量映射配置错误 | 查看ACDB各音量步进增益,检查HAL音量范围映射 |
6. 关于未来的演进和新特性,有什么值得关注的
音频这套架构发展了这么多年,演进的方向从来只有两个:更低的延迟、更好的音质。在Qcom平台最近几年的版本里,这两个方向上的变化越来越明显。
ULE(Ultra Low Energy)和FTRT(Fast Track Recording)这些新名词背后,本质是Qcom在不断压缩音频链路的延迟和功耗。比如你玩音游时,按下按键到听到音符反馈的时间,从过去的80-100ms压缩到了现在的20-30ms,这背后既有ADSP算法的优化,也有音频管线从AP到DSP的搬运方式的改变。多应用同时录音,在Android 10之前是做不到的,因为底层只有一条录音通路。后来Google在Android 10里正式支持并发录音,就需要Framework、HAL、DSP三方配合:Framework放开访问限制,HAL增加多条录音pcm通道,DSP支持多个session并行处理,才最终实现了系统级的多应用录音能力。
如果你对底层感兴趣,可以重点关注offload的演进。最初的compress offload只支持播放,现在已经覆盖了录音、语音通话等更多场景。这个趋势的背后,是Qcom在努力把越来越多的音频任务从AP搬到ADSP,让AP彻底从音频处理中解放出来。未来音频处理的主战场,只会离应用层越来越远、离DSP越来越近。
我自己的体会是:搞音频系统开发,不怕你懂的东西少,就怕你没有全局视角。每次看代码、抓log之前,先花几分钟想清楚当前这个问题处在整条链路中的哪个环节,手里有哪些工具可以验证这个环节是否正常,然后一层层定位下去。这个思路比你自己闷头去看一堆驱动代码要高效得多。
这次先把整体链路的骨架搭出来,后面如果有机会,我再针对HAL层路由、ADSP校准、蓝牙A2DP offload、低延迟调优这些方向,单独展开聊。你那边的项目如果有音频相关的奇葩问题,也欢迎在评论区或者私信里丢过来,我们一起研究。