4、音频路由基础:路由决策模型、路由策略规则、动态路由切换机制

音频路由,说白了就是系统决定「声音从哪进、从哪出」的那套逻辑。你插上耳机,声音自动切到耳机;拔掉耳机,声音回到扬声器——这背后就是路由在干活。但真实场景远比这个复杂,我做了这么多年音频,踩过的坑大多都跟路由决策有关。

4.1 路由决策模型:谁说了算?

Android 的音频路由不是一个人拍脑袋决定的。它是一套分层决策模型,每一层都有自己的职责。

我个人习惯把路由决策模型分成三层:

  • 应用层:App 通过 AudioAttributes 告诉系统「我要播什么类型的声音」。比如音乐 App 设 USAGE_MEDIA,导航 App 设 USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
  • 策略层:AudioPolicyService 根据这些属性,结合当前设备状态,决定走哪条路。这是核心。
  • 硬件抽象层:HAL 层负责把策略层的决策翻译成硬件能懂的命令。比如切换 codec、打开某个麦克风。

你想想看,如果应用层乱设属性,策略层再聪明也没用。我在项目中遇到过某个第三方 App 把通话音频设成了 USAGE_MEDIA,结果声音死活不走听筒,用户投诉说「打电话听不见」——查了半天,就是属性设错了。

核心原则:路由决策是「自顶向下」的。上层提供信息,下层执行决策。上层如果给错信息,下层再努力也白搭。

4.2 路由策略规则:具体怎么判?

策略层不是瞎判的。它有一套规则引擎,我把它拆成三个维度:

4.2.1 设备可用性规则

系统先问一个问题:「当前有哪些设备可用?」

  • 有线耳机插入了?——可用
  • 蓝牙耳机连上了?——可用,但要看 A2DP 还是 HFP
  • 扬声器?——永远可用(除非被强制关闭)
  • USB 音频设备?——可用,但可能有权限限制

嗯,这里要注意:蓝牙设备的状态变化是异步的。我曾经调试过一个 bug,蓝牙耳机已经连上了,但路由没切过去,原因是策略层收到连接事件时,蓝牙协议栈还没准备好。后来加了延迟重试才解决。

4.2.2 优先级规则

当多个设备同时可用时,谁优先?Android 有一套硬编码的优先级表:

优先级 设备类型 典型场景
1(最高) 有线耳机(带麦克风) 插入耳机,通话和媒体都走耳机
2 蓝牙耳机(SCO) 蓝牙通话场景
3 蓝牙音箱(A2DP) 蓝牙音乐播放
4 扬声器 默认外放
5(最低) USB 音频 外接 DAC 或耳机

但这不是绝对的。比如你插着有线耳机,又连了蓝牙音箱,系统会优先走有线——因为有线延迟更低、音质更稳定。这个逻辑我一开始也觉得合理,直到有用户反馈「我想让蓝牙音箱出声,但插着充电线就不行」——嗯,充电线不算音频设备,所以不影响。

4.2.3 策略覆盖规则

有些场景下,系统会强行覆盖默认优先级。比如:

  • 通话中:强制走听筒或蓝牙 SCO,不会走扬声器(除非开了免提)
  • 录音场景:强制走麦克风,不会走线路输入
  • Car Mode:强制走蓝牙或 AUX

避坑提醒:策略覆盖规则是厂商可以定制的。我见过某厂商在 Car Mode 下强制走 USB 音频,结果用户插了 U 盘听歌,导航声音却从手机扬声器出来——因为 USB 被 U 盘占用了。这种问题很难排查,因为策略层日志里不会告诉你「为什么没走 USB」。

4.3 动态路由切换机制:切的时候发生了什么?

路由切换不是瞬间完成的。它是一套有状态机的流程。我画了一张图,帮你理解这个过程:

动态路由切换状态机 空闲状态 设备事件触发 策略评估 路由切换中 新路由生效 切换失败 插入/拔出/连接 查询策略 决策完成 执行切换 成功 失败/超时 等待下次事件

这张图展示了切换的完整生命周期。我重点讲几个关键阶段:

4.3.1 设备事件触发

系统通过 AudioManagerregisterAudioDeviceCallback 监听设备变化。事件类型包括:

  • onAudioDeviceAdded:新设备接入
  • onAudioDeviceRemoved:设备拔出
  • onAudioDeviceChanged:设备属性变化(比如蓝牙从 A2DP 切到 SCO)

我记得有一次调试,蓝牙耳机连上了但 onAudioDeviceAdded 没触发。后来发现是 HAL 层上报的设备类型跟策略层预期的不匹配——HAL 报的是 DEVICE_OUT_BLUETOOTH_A2DP,但策略层在等 DEVICE_OUT_BLUETOOTH_SCO。这种类型不匹配的问题,在定制 ROM 上特别常见。

4.3.2 策略评估

策略评估的核心函数是 AudioPolicyManager::getDeviceForStrategy()。它会遍历所有可用设备,按优先级排序,然后选出最合适的。伪代码如下:

// 简化版策略评估逻辑
DeviceDescriptor getDeviceForStrategy(audio_usage_t usage) {
    // 1. 获取当前可用设备列表
    Vector<sp<DeviceDescriptor>> availableDevices = getAvailableDevices();

    // 2. 按优先级排序
    sortByPriority(availableDevices);

    // 3. 根据 usage 过滤
    for (auto &device : availableDevices) {
        if (device->supportsUsage(usage)) {
            return device;
        }
    }

    // 4. 兜底:走默认设备(通常是扬声器)
    return getDefaultDevice();
}

嗯,这里有个隐藏问题:supportsUsage() 的判断逻辑各厂商不一样。我见过某厂商把 USAGE_VOICE_COMMUNICATIONUSAGE_MEDIA 混在一起,导致通话声音从扬声器出来——用户以为开了免提,其实没开。

4.3.3 路由切换执行

切换执行阶段,系统会做三件事:

  1. 静音旧设备:防止爆音。这个动作是渐变的,不是瞬间切断。
  2. 打开新设备:初始化硬件,建立音频通道。
  3. 重路由音频流:把正在播放的音频流从旧设备迁移到新设备。

个人经验:切换过程中最容易出问题的是「时序」。如果旧设备还没完全静音,新设备就开始出声,你会听到「噗」的一声爆音。我解决这个问题的方法是:在 HAL 层加一个 20ms 的交叉淡入淡出。虽然增加了延迟,但用户体验好很多。

4.4 常见路由问题与排查思路

最后分享几个我实际遇到过的路由问题,以及排查方法:

现象 可能原因 排查方法
插耳机没声音 耳机类型识别错误(OMTP/CTIA 搞反) 检查 dumpsys audio 中的设备类型
蓝牙切不回扬声器 蓝牙断开事件没被策略层收到 抓取 logcat -b events 看设备事件
通话声音从扬声器出 策略覆盖规则被厂商改过 查看 audio_policy_configuration.xml
切换时有爆音 HAL 层没有做淡入淡出 检查 HAL 的 setDeviceConnectionState 实现

我曾经遇到一个最诡异的 case:用户插上耳机后,声音正常,但一按音量键,声音就切到扬声器了。查了两天才发现,是某个定制的音量键处理逻辑里,误调用了 setSpeakerphoneOn(true),强制把路由切到了扬声器。嗯,这种问题,光看音频日志是看不出来的,得结合按键事件日志一起看。

总结一句话:音频路由的核心是「谁来决定、怎么决定、决定后怎么执行」。理解这三层,大部分路由问题你都能找到方向。剩下的,就是靠日志和耐心了。


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