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 设备事件触发
系统通过 AudioManager 的 registerAudioDeviceCallback 监听设备变化。事件类型包括:
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_COMMUNICATION 和 USAGE_MEDIA 混在一起,导致通话声音从扬声器出来——用户以为开了免提,其实没开。
4.3.3 路由切换执行
切换执行阶段,系统会做三件事:
- 静音旧设备:防止爆音。这个动作是渐变的,不是瞬间切断。
- 打开新设备:初始化硬件,建立音频通道。
- 重路由音频流:把正在播放的音频流从旧设备迁移到新设备。
个人经验:切换过程中最容易出问题的是「时序」。如果旧设备还没完全静音,新设备就开始出声,你会听到「噗」的一声爆音。我解决这个问题的方法是:在 HAL 层加一个 20ms 的交叉淡入淡出。虽然增加了延迟,但用户体验好很多。
4.4 常见路由问题与排查思路
最后分享几个我实际遇到过的路由问题,以及排查方法:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 插耳机没声音 | 耳机类型识别错误(OMTP/CTIA 搞反) | 检查 dumpsys audio 中的设备类型 |
| 蓝牙切不回扬声器 | 蓝牙断开事件没被策略层收到 | 抓取 logcat -b events 看设备事件 |
| 通话声音从扬声器出 | 策略覆盖规则被厂商改过 | 查看 audio_policy_configuration.xml |
| 切换时有爆音 | HAL 层没有做淡入淡出 | 检查 HAL 的 setDeviceConnectionState 实现 |
我曾经遇到一个最诡异的 case:用户插上耳机后,声音正常,但一按音量键,声音就切到扬声器了。查了两天才发现,是某个定制的音量键处理逻辑里,误调用了 setSpeakerphoneOn(true),强制把路由切到了扬声器。嗯,这种问题,光看音频日志是看不出来的,得结合按键事件日志一起看。
总结一句话:音频路由的核心是「谁来决定、怎么决定、决定后怎么执行」。理解这三层,大部分路由问题你都能找到方向。剩下的,就是靠日志和耐心了。
公众号:蓝海资料掘金营,微信deep3321