3、AudioPolicy服务:策略决策流程、路由规则配置、动态策略切换、多音区策略
好,咱们今天聊聊 AudioPolicy 服务。说实话,这是整个音频系统里最“聪明”的部分。它不像 AudioFlinger 那样只管干活,它负责的是——决定怎么干活。
我刚开始接触 Android 音频时,总觉得 AudioPolicy 就是个配置文件。后来踩了坑才明白,它其实是个动态决策引擎。你想想看,车机里同时有导航、电话、音乐、语音助手,谁先出声?谁该被混音?谁该被静音?这些全是 AudioPolicy 说了算。
3.1 策略决策流程:谁说了算?
AudioPolicy 的核心,说白了就是一套“谁优先级高谁先上”的规则。但具体怎么决策的?我画个简化的流程给你看:
应用发起播放请求
↓
AudioPolicyService 收到请求
↓
检查当前音频焦点(Audio Focus)
↓
根据音频属性(Usage/AudioAttributes)匹配策略
↓
查询路由规则(哪个设备输出)
↓
下发命令给 AudioFlinger
↓
通知 HAL 层切换设备
这里面有个关键点:音频焦点。我记得有一次做项目,客户反馈导航播报时音乐声音没变小。查了半天,原来是导航应用没申请焦点,AudioPolicy 压根不知道它要“抢麦”。
核心原则:没有焦点的音频流,在策略决策中会被视为“背景音”。只有拿到焦点的流,才能触发策略切换。
决策流程里还有个容易被忽略的点——延迟决策。什么意思?就是当多个音频流几乎同时到达时,AudioPolicy 不会立刻做决定,而是等一小段时间(通常是几十毫秒),收集完所有请求后再统一决策。我习惯把这个叫做“集束处理”,能有效避免频繁的策略抖动。
3.2 路由规则配置:audio_policy_configuration.xml 的秘密
路由规则,说白了就是“什么声音走哪个喇叭”。在 Android Automotive 里,这个配置写在 audio_policy_configuration.xml 里。嗯,这里要注意,车机和手机的路由配置完全是两码事。
手机通常只有扬声器和耳机两个输出。车机呢?前门喇叭、后门喇叭、低音炮、中置、头枕喇叭、蓝牙耳机... 我见过最多的有 16 个独立输出通道。
来看一个简化版的配置片段:
<audioPolicyConfiguration version="1.0">
<globalConfiguration speaker_drc_enabled="false"/>
<modules>
<module name="primary" halVersion="3.0">
<attachedDevices>
<item>Speaker</item>
<item>Built-In Mic</item>
</attachedDevices>
<routeMixes>
<!-- 导航走前门喇叭 -->
<mix type="navigate" address="front_left|front_right"/>
<!-- 电话走头枕喇叭 -->
<mix type="call" address="headrest_left|headrest_right"/>
</routeMixes>
</module>
</modules>
</audioPolicyConfiguration>
这里有个坑,我必须要说。很多工程师以为配置了 routeMixes 就万事大吉了。其实不对!你还要在 audio_policy_engine_configuration.xml 里定义策略规则,告诉系统“导航这个 Usage 应该匹配 navigate 这个 mix”。
避坑指南:我曾经遇到一个项目,配置了路由但策略没生效。查了两天才发现,是策略规则的优先级写反了。记住:策略规则是按顺序匹配的,第一个匹配到的规则会被执行。所以通用规则要放后面,特殊规则放前面。
3.3 动态策略切换:不是简单的 if-else
动态策略切换,是车机音频最考验功底的地方。为什么?因为车机里的音频场景是实时变化的。比如:
- 车辆从 P 档切换到 D 档,倒车雷达声音要立刻出现
- 导航播报时,音乐要 duck(压低音量)而不是静音
- 来电时,所有娱乐音频要暂停
这些场景切换,AudioPolicy 是怎么做到的?核心机制是 Policy Engine。它维护了一张状态机,每个状态对应一组路由和音量策略。
我举个例子,导航播报的场景切换:
// 伪代码示意
状态: MUSIC_PLAYING
条件: 收到 NAVIGATION_ANNOUNCE 焦点请求
动作:
- 音乐音量从 100% 降到 30%(duck)
- 导航音频路由到前门喇叭
- 导航音量设为 80%
状态: NAVIGATION_ANNOUNCING
条件: 导航播报结束
动作:
- 音乐音量从 30% 恢复到 100%
- 导航音频停止路由
你可能会问:为什么不用简单的 if-else?因为车机场景太复杂了。if-else 写到最后就是一团乱麻。我见过一个项目,策略切换代码写了 3000 多行,全是 if-else 嵌套,后来根本没人敢改。
我的建议:用策略模式(Strategy Pattern)来组织动态切换逻辑。每个场景是一个独立的策略类,切换时通过策略管理器统一调度。这样新增一个场景,只需要加一个类,不用改原有逻辑。
3.4 多音区策略:前后排各听各的
多音区,这是 Automotive 和手机最大的区别。手机只有一个用户,车机呢?驾驶员、副驾、后排乘客,可能想听不同的内容。
多音区的实现,依赖两个东西:
- 硬件支持:独立的音频通道,比如前排和后排用不同的 DAC
- 软件策略:AudioPolicy 根据音区 ID 做路由决策
来看一个多音区的策略配置思路:
// 音区定义
ZONE_DRIVER: 路由到前门喇叭
ZONE_PASSENGER:路由到副驾头枕喇叭
ZONE_REAR: 路由到后门喇叭
// 策略规则
if (audioAttributes.getUsage() == USAGE_MEDIA) {
if (zoneId == ZONE_DRIVER) {
routeTo("front_left|front_right");
} else if (zoneId == ZONE_REAR) {
routeTo("rear_left|rear_right");
}
}
// 注意:导航和电话始终路由到驾驶员音区
if (audioAttributes.getUsage() == USAGE_NAVIGATION) {
routeTo("front_left|front_right"); // 忽略音区 ID
}
这里有个容易犯的错误:音区隔离不彻底。我曾经遇到一个 case,后排看电影,前排导航播报时,后排电影声音突然变小了。查了半天,原来是策略里把 duck 操作应用到了所有音区,没有做音区隔离。
多音区设计原则:
- 每个音区有独立的音量曲线
- 焦点管理按音区隔离(后排焦点不影响前排)
- 系统音(导航、电话)可以跨音区,但要有优先级
- 硬件通道必须物理隔离,否则软件隔离是假的
说到多音区,我还想提一个进阶话题——音区动态合并。比如后排没人时,后门喇叭可以合并到前排,提供更好的立体声效果。这个功能在高端车型里越来越常见。实现方式是在 AudioPolicy 里加一个“音区状态检测”,根据座椅传感器或摄像头数据,动态调整路由策略。
嗯,关于 AudioPolicy 服务,今天就聊这么多。总结一下:策略决策看焦点,路由配置看 XML,动态切换用状态机,多音区要物理隔离。下一章咱们聊聊 AudioFlinger 的混音和低延迟实现,那个更有意思。