
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数据丢给AudioFlingerAudioFlinger根据策略找到合适的输出设备扬声器、耳机还是蓝牙然后通过HAL把数据写到内核的ALSA接口内核再通过I2S、SLIMbus这些物理总线把数据送给Codec或者ADSP最后由Codec完成数模转换驱动扬声器发声。这一条线跑下来就是整篇文章的主角。2. 为什么要分FE和BEAndroid音频框架的分层设计哲学2.1 从HAL掉了一层皮开始说起我第一次看Qcom音频代码时第一反应是为什么一个简单的播放操作要整出这么多结构体和层级直接在App里写个驱动把数据送出去不行吗后来被现实教育了——Android要跑的设备太多了每家的Codec不一样每家的DSP方案不一样如果Framework层直接面对硬件那Google每出一个版本所有芯片厂商都得重写一遍系统。所以Google在中间塞了一层HALHardware Abstraction Layer。在Android 8.0之前是audio_hw_modules到了Android 8.0之后演进成了HIDLAndroid 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的音频驱动是基于ASoCALSA System on Chip框架实现的。ASoC框架把音频驱动拆成了三块Codec驱动、Platform驱动和Machine驱动。Codec驱动管DAC/ADC这些数模转换器件Platform驱动管DMA和CPU侧的DAIDigital Audio InterfaceMachine驱动则负责把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上然后由DAPMDynamic 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_callCSD语音、FM收音机等。每种类型的pcm设备在HAL层对应不同的音频路径和buffer配置也对应不同的DSP处理流程。比如播放一首MP3传统方式是把MP3解成PCM再送过去播放这需要CPU持续参与但Qcom支持compress offloadApp只需要把压缩码流直接丢给ADSPADSP内部完成解码、混音、EQ、左右声道平衡等一系列处理然后直接输出到Codec。这个过程CPU几乎不参与功耗大大降低。这不只是省电的问题在手机这种对续航极其敏感的设备上这种架构直接决定了产品体验的优劣。理解FE的多样性对后面排查问题至关重要。比如“播放音乐有杂音”和“通话有回声”虽然都是音频问题但它们走的FE完全不一样牵涉的底层模块也完全不同。如果你一上来就在primary pcm的设备上抓数据去分析一个发生在voip链路上的回声问题那必然一无所获。3. ADSP到底是干什么的音频处理的大脑与神经中枢3.1 为什么要单独搞一个处理器来管声音手机主CPUApplication ProcessorAP要管的事情太多了跑App、渲染UI、处理网络、跑各种后台任务。如果音频处理这块也全部交给AP一方面会占用CPU资源另一方面在功耗和延迟上也很难做到最优。Qcom的解决办法是把音频相关的计算任务从CPU上搬走交给一颗专用的数字信号处理器也就是我们常说的ADSPAudio Digital Signal Processor。ADSP在Qcom的音频架构里扮演的角色相当于一个“音频处理大脑”。主CPU把音频数据通过共享内存或者总线发送给ADSPADSP内部完成音效处理、回声消除、噪音抑制、重采样、混音等任务之后再把处理好的数据通过物理总线送给Codec或者直接驱动扬声器。这个架构带来最直接的好处是即便屏幕息屏、CPU进入低功耗状态音乐依然可以正常播放即便同时开着音乐、导航、电话ADSP也能通过内部的多个session完成混音与路由切换。这就是为什么Qcom的音频方案能支持如今这么多复杂的并发场景。3.2 ADSP上的关键处理模块ADSP内部的软件框架过去是QDSP6Qualcomm 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负责处理数据而ACDBAudio Calibration Database负责告诉ADSP在某个设备上、某个采样率下、某个音量级别时应该给音频数据加多少增益、做多少滤波、走哪条物理通路。ACDB里存的是一张张校准表每张表对应着不同的设备、不同的路径、不同的场景。你插上耳机、拔掉耳机、调到最大音量、切换通话音量这些动作背后都对应着ACDB里的一次查询和加载。Qcom在HAL层有一个专门的库叫libacdb它负责管理ACDB的加载和应用。如果你调过音频音量觉得“最大声也小”、“底噪大”、“开EQ有破音”多半需要去ACDB里调整对应的gain值。ACDB的数据来源通常是音频调试工程师在实验室里通过QACTQualcomm 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后级任何一段。DAPMDynamic 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音量是否不为0SPK 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平台最近几年的版本里这两个方向上的变化越来越明显。ULEUltra Low Energy和FTRTFast 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、低延迟调优这些方向单独展开聊。你那边的项目如果有音频相关的奇葩问题也欢迎在评论区或者私信里丢过来我们一起研究。