13、音频系统与AudioFlinger:音频策略、ALSA驱动、路由管理

音频系统,说白了就是Android设备发声的整套机制。你按下播放键,音乐从扬声器出来,这中间经历了什么?我当年刚接触这块时,觉得不就是放个声音嘛,能有多复杂?直到第一次遇到无声、杂音、路由错乱的问题,才意识到这里面的门道有多深。

今天我们就来拆解Android音频系统的三大核心:音频策略怎么定、ALSA驱动怎么干活、路由管理怎么切换。嗯,这些都是实战中绕不开的硬骨头。

13.1 音频系统整体架构

先看一张整体图,帮你建立全局认知。Android音频系统从上到下大致分四层:

应用层 (App / MediaPlayer / AudioTrack) Framework层 (AudioFlinger / AudioPolicyService) HAL层 (Audio HAL / TinyAlsa) 内核层 (ALSA Driver / ASOC) 硬件层 (Codec / Speaker / Mic)

每一层各司其职。应用层只管调用接口,Framework层做策略和混音,HAL层做硬件抽象,内核层直接操作硬件。我个人的习惯是,遇到音频问题先定位是哪一层出的问题,这样排查效率高很多。

13.2 AudioFlinger——音频系统的核心引擎

AudioFlinger是Android音频系统的核心服务。它运行在mediaserver进程中,负责所有音频数据的混音和输出。说白了,它就是音频数据的"调度中心"。

AudioFlinger主要干这几件事:

  • 混音:把多个音频流混合成一路输出
  • 路由:根据策略选择输出设备(扬声器、耳机、蓝牙等)
  • 采样率转换:不同音频源的采样率统一转换成输出设备支持的格式
  • 音量控制:管理各音频流的音量曲线

核心要点:AudioFlinger内部维护了一个混音线程(MixerThread),所有音频数据最终都要经过这个线程处理。我调试过一个项目,发现播放卡顿,最后定位到是混音线程被其他高优先级任务抢占导致的。嗯,线程优先级这块很容易被忽略。

13.3 音频策略——谁来决定声音从哪出来?

音频策略由AudioPolicyService负责。它不直接处理音频数据,而是做决策。比如:插上耳机后,声音自动从扬声器切换到耳机,这就是策略在起作用。

策略的核心是音频路由规则。这些规则定义在audio_policy_configuration.xml中。我见过不少开发者直接改这个文件来定制路由,但改完后经常出问题——因为策略的优先级和条件判断很复杂。

举个例子,典型的策略规则:

<!-- audio_policy_configuration.xml 片段 -->
<devicePort tagName="Speaker" type="AUDIO_DEVICE_OUT_SPEAKER" role="source">
    <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
             samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
</devicePort>

<route type="mix" sink="Speaker">
    <source name="primary output"/>
</route>

这段配置的意思是:主输出(primary output)默认路由到扬声器。如果插上耳机,策略会动态添加一条新路由,把输出切换到耳机。

实战技巧:我在项目中遇到过一个问题:插拔耳机后声音切换有延迟。后来发现是AudioPolicyService的"设备连接状态检测"轮询间隔太长。把轮询间隔从500ms改成100ms后,切换就流畅了。不过要注意,改太短会增加CPU开销,需要权衡。

13.4 ALSA驱动——内核层的音频管家

ALSA(Advanced Linux Sound Architecture)是Linux内核的音频子系统。Android虽然用了自己的HAL层,但底层还是离不开ALSA。

ALSA驱动主要提供:

  • PCM设备:用于播放和录音的数据通道
  • 控制接口:调节音量、切换路由、设置采样率等
  • MIDI接口:处理MIDI设备(嵌入式设备用得少)

在嵌入式Android开发中,我们最常打交道的是/dev/snd/目录下的设备节点:

# ls -l /dev/snd/
crw-rw---- 1 root audio 116,  2 2024-01-01 10:00 controlC0
crw-rw---- 1 root audio 116,  4 2024-01-01 10:00 pcmC0D0c   # 录音设备
crw-rw---- 1 root audio 116,  3 2024-01-01 10:00 pcmC0D0p   # 播放设备
crw-rw---- 1 root audio 116,  5 2024-01-01 10:00 timer

pcmC0D0p中的C0表示声卡0,D0表示设备0,p表示playback(播放)。c表示capture(录音)。

调试ALSA驱动时,我常用的工具是tinymixtinyplay

# 查看当前混音器控制项
tinymix

# 播放一个WAV文件
tinyplay /sdcard/test.wav

# 录制音频
tinycap /sdcard/record.wav -d 0 -c 2 -r 48000 -b 16

注意:我曾经在调试时直接用tinymix乱改混音器参数,结果把Codec芯片搞"哑"了,重启才恢复。所以建议你在改参数前,先用tinymix > backup.txt备份一份原始配置。

13.5 路由管理——音频信号的交通指挥

路由管理是音频系统中最容易出问题的环节。它决定了音频数据从哪个设备进来、从哪个设备出去。

Android的路由管理分两个层面:

  1. 软件路由:由AudioPolicyService根据策略规则决定
  2. 硬件路由:由ALSA混音器控制项决定,比如切换Codec的输入输出通道

两者必须配合好。软件路由决定了"应该走哪条路",硬件路由决定了"实际走哪条路"。如果两者不一致,就会出现"插了耳机但声音还在扬声器"这种问题。

我画了一张路由管理的流程图,帮你理清思路:

应用发起播放请求 AudioPolicyService 查询策略 设备状态? 耳机路由 扬声器路由 AudioFlinger 混音输出 硬件设备播放

你看,整个流程其实不复杂。关键点在于AudioPolicyService的决策是否正确,以及硬件路由是否跟得上软件决策。

13.6 实战:音频路由切换的调试方法

讲个我踩过的坑。有一次做平板项目,插上USB耳机后,系统识别到了设备,但声音还是从扬声器出来。我排查了整整一天,最后发现是ALSA混音器里的"Speaker Switch"没有正确关闭。

调试步骤是这样的:

  1. 先用dumpsys media.audio_policy查看软件路由状态,确认策略已经切换到USB耳机
  2. 再用tinymix查看硬件路由状态,发现Speaker的mixer通路还是开启的
  3. 手动关闭Speaker通路后,声音正常从USB耳机输出了

根本原因是什么?是HAL层的路由切换代码里,漏掉了对Speaker通路的关闭操作。说白了,软件说"走耳机",但硬件还在"走扬声器",两者没同步。

避坑指南:我建议你在做路由切换时,遵循"先关后开"的原则。先关闭当前通路的mixer控制项,再开启目标通路。这样可以避免两个通路同时打开导致的串音问题。我曾经因为顺序搞反,导致扬声器和耳机同时出声,那声音简直没法听。

13.7 音频策略的定制与优化

嵌入式设备往往需要定制音频策略。比如你的设备只有扬声器和蓝牙,没有耳机孔,那就要修改策略文件,去掉耳机相关的路由规则。

策略文件位于/vendor/etc/audio_policy_configuration.xml。修改后需要重启mediaserver才能生效:

# 重启mediaserver
killall mediaserver
# 或者
setprop ctl.restart media

修改策略文件时,有几个关键点要注意:

  • 设备端口定义:必须和硬件实际支持的设备一致
  • 路由规则:每条路由都要有明确的source和sink
  • 音量曲线:不同设备的音量映射曲线可能不同,需要单独配置

我记得有个项目,客户要求蓝牙耳机和扬声器同时出声(用于户外广播)。这其实不符合Android默认策略,但通过修改策略文件,添加一条"同时路由到蓝牙和扬声器"的规则,就实现了。嗯,策略文件的可定制性还是很强的。

13.8 总结

音频系统这块,说白了就是三件事:策略定方向、Flinger做混音、驱动管硬件。三者缺一不可。你在实际开发中遇到音频问题,按这个思路去排查,基本不会跑偏。

我个人觉得,音频系统最难的不是某个具体知识点,而是整个链路太长,问题定位需要跨层排查。所以建议你多动手,用tinymixdumpsys这些工具把各层的状态摸清楚,问题自然就浮出水面了。


公众号:蓝海资料掘金营,微信deep3321