在Android BSP这个圈子里,Qcom音频架构是绕不开的话题。做系统定制的、做驱动移植的、做VoIP优化的,几乎天天跟“FE/BE”打交道;面试问起“从FE/BE到ADSP的完整链路”,也基本算是送分题里最容易翻车的。高通这套音频其实不复杂,麻烦在链条太长:上层App、AudioFlinger、AudioPolicy、Audio HAL、tinyalsa、ALSA core、machine驱动、q6audio驱动、ADSP固件、Codec,任何一个环节掉链子,最终都表现为“没声音”或者“音频卡顿”。这篇就把整条链路完整串一遍,重点说清楚FE和BE在高通平台到底指什么、数据怎样通过AFE端口进入ADSP、又怎样被路由到Codec,同时把我调试时最常用的命令和排障思路一并整理出来。适合正在做Audio HAL或驱动移植的人,也适合想真正弄懂音频路由的新手。我尽量用实际项目里观察到的现象来讲,不干巴巴贴源码。
1. 先从FE/BE说起:理解Qcom音频的骨架
1.1 为什么是FE和BE
FE的全称是Front End,BE是Back End,这两个词最早是从ASoC(ALSA System on Chip)框架里来的。放在高通平台上,FE一般指ALSA暴露给上层的pcm设备,比如pcmC0D0p、pcmC0D0c这种节点;BE则是指向物理音频外设的链路,比如某个I2S、SLIMbus、TDM或SoundWire端口。播放时,数据从App进入FE,经过ADSP处理后从BE出来到Codec;录音时反向,麦克风信号进Codec,经过BE到ADSP,再从FE返回上层。
为什么要这样分?最直接的原因是不能让每个App直接去操作Codec。Codec和物理总线是稀缺资源,多个App同时要放声音,总不能大家都去抢同一个I2S。高通的做法是让FE变成一个“虚拟会话入口”,每个App的音频流先进入ADSP里的某个会话,在DSP内部做混音、重采样、EQ、降噪等处理,最后再由DSP统一从BE推送出去。这也解释了为什么Qcom平台经常有多个pcm设备:pcm0p、pcm1p、pcm5p可能都是FE,但它们对应不同的usecase,比如低延迟播放、deep buffer播放、语音通话。
打个比方:FE就像是微信里的聊天窗口,每个App都可以开一个窗口说话;ADSP是电话局的交换机;BE则是连接到具体电话线或者基站的那条物理线路。你对着窗口说的话,不会直接飞到对方耳朵里,而是先经过交换机路由、混音、处理,再从对应的线路出去。理解了这一步,后面看音频路由就不会晕。
1.2 Qcom音频分层的整体脉络
Qcom的音频分层从上到下大概是这样的:
App和Framework层:App调用AudioTrack/AudioRecord,数据交给audioserver进程里的AudioFlinger和AudioPolicyService;AudioPolicy负责路由决策,AudioFlinger负责调度和混音。
Audio HAL层:高通的开源HAL在hardware/qcom/audio目录,会创建一个或多个usecase,通过tinyalsa打开对应的ALSA pcm设备,同时负责加载mixer_paths.xml配置路由。
Kernel ALSA层:使用ASoC框架,machine驱动负责把各个dai link组装起来,platform驱动就是高通这一套q6audio相关驱动,codec驱动负责具体Codec芯片的寄存器控制。
ADSP固件层:AP侧通过APR(Asynchronous Packet Router)和DSP通信。ADSP内部有ASM、ADM、AFE三大核心模块:ASM负责音频流会话,ADM负责路由和拓扑,AFE负责物理端口收发。
一颗移动SoC里有CPU、GPU、NPU、VPU、DPU,还有Audio,市场上常把它们叫“几大引擎”。但Audio和GPU/DPU有个明显区别:GPU主要靠AP侧算力,而音频从某个版本开始,越来越依赖独立ADSP。数据不是简单地从内存DMA到Codec,而是先进入ADSP处理,再由ADSP控制端口送出去。这带来一个好处,就是AP在音频低负载时可以休眠,同时DSP可以做到比AP核更稳定的实时调度。代价就是链路复杂度上来了,排查问题时要同时懂AP侧和DSP侧。
从App到喇叭,一条完整播放路径是这样的:
App创建的AudioTrack把数据写到共享内存,AudioFlinger混合后交给HAL;HAL根据usecase调用pcm_write,数据进入ALSA FE设备;内核驱动通过q6asm创建一个ASM会话,让DSP接管数据;DSP的ADM根据路由关系把流送到指定AFE端口;AFE端口驱动对应的物理总线,比如SLIMbus或MI2S;最后Codec接收数字信号,转成模拟信号推动Speaker。
这条链路里的每个箭头都对应一个独立模块,也对应不同的日志和调试工具。下面几节逐个拆开看。
2. 从用户空间到内核:关键节点逐一拆解
2.1 audioserver与AudioPolicy
Android 8之后,音频核心服务集中在audioserver进程里,包含AudioFlinger和AudioPolicyService。AudioPolicyService的核心职责是“路由决策”,它决定当前的播放/录音请求应该走哪个设备。比如音乐播放时,如果耳机插入,策略就从Speaker切到WiredHeadset;如果连了蓝牙耳机,可能又切到A2DP。这个决策结果最终会通过HAL的start_output/start_input应用下去。
决策依据是什么?一是App请求的audio attributes,比如stream type、usage;二是当前系统里的audio devices状态。高通平台的策略实现通常在AudioPolicyManager基础上扩展,比如hardware/qcom/audio的policy_hal会解析audio_policy_configuration.xml、audio_policy_engine_*.xml等配置。这些xml里大量出现mixPort、devicePort、route,也就是前面说的audio端口定义。简单理解,mixPort是软件侧的虚拟端口,devicePort是物理设备端口,route是把它们连起来的一条条路径。
实际改路由配置时,大多数情况不该直接改C++代码,而是改xml。比如新增一个USB声卡、调整某个usecase的默认输出设备,多半在audio_policy_configuration.xml里加端口和route就行。有个常见坑:修改了devicePort但忘了给对应的strategy添加路由,结果dumpsys显示available devices是对的,但AudioPolicy就是找不到匹配route,最终回退到None或默认设备。检查路由配置时,要把“设备已连接”和“策略能找到路径”两件事分开验证。
2.2 Audio HAL与tinyalsa
高通HAL最核心的一个概念是usecase。一个usecase代表一类完整的音频场景,比如USECASE_AUDIO_PLAYBACK、USECASE_AUDIO_PLAYBACK_LOW_LATENCY、USECASE_AUDIO_RECORD、USECASE_VOICE_CALL等等。HAL内部维护一张usecase列表,每个usecase会绑定对应的pcm设备id、设备类型、路由信息。比如播放低延迟音频时,HAL会创建low latency usecase,然后pcm_open一个特定的pcm节点;播放普通音乐时可能走deep buffer usecase,pcm节点就换成了另一个id。
pcm_open这些接口来自tinyalsa库。tinyalsa是用户在ALSA基础上封装的轻量客户端,它负责打开类似/dev/snd/pcmC0D0p的设备,完成frame读写。高通HAL内部不用复杂的直接ioctl,就是靠tinyalsa解决绝大部分数据收发。这也是为什么我们经常在HAL日志里看到pcm_name、pcm_id这些关键词。
pcm设备编号在平台间差异很大,不是固定不变的。同一颗芯片,不同产品定制时可能会裁剪掉某些声卡,导致pcm id整体偏移。HAL侧有一张pcm设备表,内核侧也有自己的注册顺序,两者必须对齐。改机器配置后如果出现HAL报“Failed to open pcm device”或者音频路由切换后打开错误节点,优先去核对audio_platform_info.xml和内核pcm table。
分享一个很实用的调试手法:在HAL开启PCM数据dump。很多最新平台支持通过属性把输入输出pcm数据存成文件:
adb shell setprop debug.audio.pcm 1 adb shell stop && adb shell start之后在/data/misc/audiorecord目录下能找到dump出来的pcm文件。播放无声时,只要把dump到的文件拉出来用Audacity看波形,就能判断是HAL之前的问题还是HAL之后的问题:dump里没数据,说明上层就没给到HAL;dump里有数据但喇叭不响,问题就跑不了要往下看内核和Codec。这个手段比反复加log高效得多。
2.3 ADSP侧的AFE端口与audio topology
到了ADSP侧,视角要切换成“DSP世界”的语言。AP侧通过APR协议给DSP发消息,DSP内部有三大块:ASM管理audio stream session,ADM管理audio routing和topology,AFE管理物理端口。
AFE端口是DSP对外部数字总线的抽象,比如AFE_PORT_ID_SLIMBUS_0_RX、AFE_PORT_ID_QUATERNARY_MI2S_RX。当内核侧的BE dai_link启动时,q6afe驱动会向ADSP发送AFE端口配置和启动命令,把采样率、位深、声道数、总线类型这些参数告诉DSP。一旦AFE port start成功,DSP和Codec之间的物理链路才真正建立起来。很多“无声”问题,最后定位到AFE port start失败,原因是adsp子系统crash后没有重启成功。
ADM干的是路由和拓扑活儿。DSP内部可以把来自ASM的多个流经过mixer合到一起,再通过某个AFE端口输出,也可以把一路流同时送到两个端口。拓扑里的“copp”其实就是每个端口对应的输出处理通道,EQ、DRC、SRC这些效果器都挂在拓扑里。所以如果某个音效开关改了但实际听感没变化,大概率是ADM里的topology实例没选对,或者ACDB里对应拓扑的参数没刷进去。
ACDB(Audio Calibration Database)也要提一句。Qcom很多音频参数,比如不同采样率下的滤波器系数、设备校准值、topology选择,都存在ACDB里。系统起来时adsp_loader负责把ACDB加载进ADSP。ACDB版本和HAL配置不匹配是很经典的坑,表现是某些usecase能出声音、某些usecase无声或声音怪。遇到这种问题,先别急着改DSP固件,重新校准ACDB并确认加载配置反而更快。
3. 实操链路:怎么看清楚一条音频通路
3.1 用dumpsys快速定位上层问题
接到一个“没声音”的bug,我习惯先在上层确认整个链路有没有正常建立。最常用的就是两个dumpsys:
adb shell dumpsys media.audio_flinger adb shell dumpsys media.audio_policyaudio_flinger的输出里要先看有没有对应的Output Thread,Thread里有没有Track,Track类型和状态是什么。比如音乐播放没声音,查Track还在不在,如果Track已经停止,那是App侧主动停了;如果Track状态正常但HAL没有数据,可能是HAL start失败或者AudioFlinger的mixer线程卡死。输出的末尾一般还有underrun计数,如果underrun持续上涨,说明数据供给速度跟不上消费速度,大概率是buffer配置或性能问题。
audio_policy的输出主要看设备和路由状态。比如用蓝牙播放没声音,检查ActiveOutput、Devices、DeviceId这些字段,能确认policy是不是真的把设备切到了Bluetooth A2DP。如果policy还停在Speaker,那是上层路由问题,跟蓝牙硬件链路无关;如果policy已经切到A2DP但没声音,那问题就转移到HAL和蓝牙协议栈。
还有一个容易被忽略的技巧:dumpsys audio_flinger里能看到每个Thread的采样率、通道、format,以及mixer配置。当上层协商出来的采样率和HAL/ADSP期望的不一致时,会出现“有声音但变调/卡顿”的现象。先通过dumpsys确认两端参数一致,再往下查,能省很多时间。
3.2 tinymix/tinyplay直连内核链路
如果怀疑问题在HAL或更低层,可以用tinyplay和tinymix绕开Android音频栈,直接测试内核ALSA链路。常见的操作:
adb root adb push sine440.wav /data/local/tmp/ adb shell cd /data/local/tmp tinymix | head tinyplay sine440.wav -D 0 -d 5tinyplay会直接打开指定的pcm设备并写入wav文件数据。如果打开后能听到声音,说明内核ALSA到Codec再到喇叭的链路基本正常;如果tinymix能看到各个音频寄存器,但tinyplay没声音,问题很可能在路由配置或者Codec/功放控制上。
但这里有个特别容易踩的坑:高通平台不像普通Linux声卡,打开pcm设备后数据不会自动送到某个物理端口。FE只是入口,后面还要经过DSP路由才到BE。直接用tinyplay播放一个FE设备,如果HAL没有设置过对应路由,数据可能在DSP里没被接到任何AFE端口,结果就是“播放看起来成功了,但喇叭没声音”。这不代表链路坏,而是你还没做路由。所以用tinyplay做测试时,要先用tinymix确认把对应BE的MUX选到正确的数据源,或者直接调用HAL已经拉起来的路由。
tinypcminfo也有用,它能列出pcm设备支持的格式、采样率范围、通道数:
tinypcminfo -D 0 -d 5比如想确认某个pcm设备是否支持24bit/192kHz,一跑就知道。很多“格式不支持”的问题,根源上就是HAL和pcm设备能力不匹配,没必要去内核里翻半天。
3.3 抓log与debugfs:让DSP开口说话
上层log和内核log的tag要先记熟。HAL层主要看audio_hw_primary、audio_hw_utils、audio_route,还有acdb_loader;framework层看APM_AudioPolicyManager、AudioFlinger;内核层看dmesg里和q6、apr、slim、i2s、msm_dai相关的关键词。抓现场时建议一条命令把日志带时间戳一起拉:
adb logcat -v threadtime -b all > logcat_all.txt & adb shell dmesg > dmesg.txtDSP内部状态在量产机上通常拿不到,工程机或debug版可以看一些debugfs节点,比如/sys/kernel/debug/q6下面有apr、afe、asm相关的状态。更高阶的做法是抓subsystem ramdump,当ADSP crash之后,把ramdump拿回到高通工具里解析,能看到DSP侧卡在哪条消息上。不过这套东西对一般调试来说太重,平时先把APR消息是否正常确认了,大部分问题都能定位出一半。
举一个真实例子:有次遇到“播放偶尔中断,而且没有任何App层报错”,logcat和dmesg刷了很久,最后在dmesg里看到类似AFE port enable failed、APR timeout的字段。这就说明AP侧到ADSP的通信出了问题,后来发现adsp子系统温度过高触发了重启动,重启动过程中AFE端口全部被释放,等AP侧重新拉起AFE端口时已经丢了一个session。这种问题靠HAL层怎么优化都解决不了,得回到DSP负载和散热策略上处理。
4. 常见问题与排障记录
4.1 多应用同时录音:Android 9.0上可以这么改
Android系统默认情况下,同一时刻只有一个普通应用能占用麦克风录音。后请求录音的应用会拿到“麦克风被占用”的拒绝结果,或者干脆进入wait状态。这个是策略层在拦,不是DSP能力不够。高通平台的ADSP其实支持同时打开多个录音流,只是Android默认策略不允许共用输入设备。
如果想在Android 9.0上支持多个应用同时录音,大致思路是在AudioPolicyManager里放宽输入流的独占约束。默认逻辑是在getInputForAttr里找到一个合法的input descriptor,如果这个input已经在active,通常就直接拒绝新请求。要做并发录音,就要改成允许新的App挂载到同一个input流上,让AudioFlinger的RecordThread同时为多个RecordTrack提供数据。这种方案要求各个App的采样率、通道、格式尽量一致,不一致还得加一路重采样,复杂度会明显上升。
高风险点是路由切换。如果其中某个App要求切换麦克风设备,整个输入流的路由都会跟着变,其他App的录音数据也会被带过去。所以并发录音的定制项目里,不建议开放太灵活的设备切换能力。并且要记得重新过CTS相关用例,尤其是testConcurrentCapture这类测试,不然很容易在系统升级后暴露兼容性问题。这个改动属于深度定制,不是通用补丁,做之前要评估好产品场景确实需要并发录音。
4.2 无声、杂音、单侧声道的常规排查顺序
无声问题要先确认路径方向:是播放、录音,还是通话?是所有场景都无声,还是只有特定usecase无声?这两问能砍掉一半排查面。播放无声我一般按这个顺序来:先dumpsys确认Track和路由;再检查HAL有没有打开正确的pcm设备;然后看tinymix里相关MUX和Codec寄存器是否配到了预期值;最后查功放使能GPIO和硬件连接。每一步都找一个能证明链路通断的日志或值,而不是反复试。
杂音问题要先区分是偶发还是持续。持续杂音多半是采样率不匹配、DSP某段SRC配置不对、或地线/供电问题;偶发杂音优先怀疑underrun,也就是buffer饥饿。看underrun可以直接在dumpsys audio_flinger里搜索underrun字段,如果数值在播放过程中持续增长,说明数据供不上,需要调大buffer或优化CPU/DSP调度。
单侧声道问题倒是比较有意思。遇到“左边没声音/右边没声音”,先用单声道正弦波测试文件播放,分别确认左右声道在原始wav里是否都有数据。如果源文件本身没问题,再看DSP拓扑里channel mapping是否配置正确,比如某些平台用AIF1对应Left、AIF2对应Right,配置反了会导致声道互换或单侧无声。最后再查Codec到功放的硬件链路。多数情况下,这种问题不是硬件坏了,而是链路某一层把左右声道搞错位了。
4.3 低延迟与系统性能:buffer不是越小越好
低延迟是音频项目里最常被提起的性能指标。Qcom平台处理低延迟的思路是给不同场景分配不同pcm设备:低延迟场景走专门的low latency pcm,普通音乐走deep buffer。low latency pcm的buffer申请得比较小,数据路径也尽量短,代价是对系统调度更敏感;deep buffer pcm数据路径长、延迟大,但对实时性要求低,可以更好地省电。
延迟可以由buffer大小简单估算。比如采样率48000Hz,period设为192帧,那么每个period对应的时间就是192/48000=4ms。如果整个链路有2个period的buffer,至少就有8ms的基础延迟,还要再加上ADSP内部处理、Codec内部FIFO、总线传输等额外延迟。所以不要盲目追求极小的buffer size,太小的话一次CPU调度不及时就会underrun,产生爆音或卡顿。
检查低延迟情况下的性能问题,可以用:
adb shell dumpsys media.audio_flinger | grep -i underrun adb shell cat /proc/asound/card0/pcm0p/sub0/hw_paramshw_params里能看到当前实际使用的period_size、buffer_size、rate。如果发现allowed和actual不一致,就要回头查HAL是否真的按预期配置到了设备端。
我在项目里的体会是,低延迟设计最优先考虑“稳定”而不是“数字最小”。有一次做K歌场景,把period压到2ms,本地测试延迟确实漂亮,但一跑到高负载压力测试,underrun次数肉眼可见地涨。后来把period调到4ms,感官延迟只多了不到2ms,稳定性却完全不一样。做性能优化必须先有衡量指标,不然为了纸面延迟牺牲稳定性,最终伤害的还是用户体验。
最后说一个我自己的调试习惯
处理音频问题多了之后,我现在接bug第一时间不急着看代码,而是先问自己:这条链路的哪个环节还没有证据证明它是正常的?通过dumpsys确认策略层,通过HAL log确认usecase和pcm设备,通过tinymix确认路由和Codec寄存器,通过AFE日志确认ADSP端口状态,一层层找那个“没有证据”的地方。绝大多数问题都出在还没被验证到的环节里,而不是大家一开始怀疑的那个环节。这套方法在Qcom上管用,换到其他平台也同样适用,因为音频链路本质上都逃不开“虚拟端点—DSP处理—物理端口—Codec”这条主线。