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 和手机最大的区别。手机只有一个用户,车机呢?驾驶员、副驾、后排乘客,可能想听不同的内容。

多音区的实现,依赖两个东西:

  1. 硬件支持:独立的音频通道,比如前排和后排用不同的 DAC
  2. 软件策略: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 的混音和低延迟实现,那个更有意思。