18、音频策略与电话:通话音频路由(听筒/扬声器/蓝牙)、通话状态与策略切换、VoIP音频路由
通话音频路由,说白了就是决定「声音从哪出来、从哪进去」。这事儿在 Android 里远比想象中复杂。我最早接触这块时,以为就是个简单的开关切换,结果被各种状态机、策略优先级、蓝牙 SCO 折腾得够呛。
今天咱们就把通话路由这块彻底捋清楚。我会结合自己踩过的坑,把听筒、扬声器、蓝牙这些设备的路由逻辑,以及 VoIP 的特殊处理,一次性讲透。
18.1 通话音频路由的三要素
音频路由决策,本质上依赖三个核心要素:
- 音频设备:听筒、扬声器、有线耳机、蓝牙耳机、USB 耳机等
- 通话状态:空闲、来电响铃、通话中、通话保持等
- 策略优先级:用户选择 > 设备插拔事件 > 默认策略
嗯,这里要注意:策略优先级不是死的。我在项目中遇到过,用户插着蓝牙耳机时,系统却把音频路由到了听筒——原因是蓝牙 SCO 连接还没建立完成,系统就按「默认听筒」去路由了。这种时序问题,是通话路由里最常见的坑。
18.2 通话状态与策略切换
Android 的通话状态机,是路由切换的「触发器」。咱们看看核心状态:
| 通话状态 | 典型路由策略 | 触发条件 |
|---|---|---|
| IDLE(空闲) | 不路由通话音频 | 无通话 |
| RINGING(响铃) | 扬声器(铃声) | 来电事件 |
| OFFHOOK(摘机) | 听筒(默认) | 接通电话 |
| HOLD(保持) | 保持当前路由 | 用户操作或网络事件 |
你想想看,从 RINGING 切换到 OFFHOOK 时,路由要从扬声器切到听筒。这个切换时机如果没处理好,就会出现「接通瞬间声音从扬声器爆出来」的尴尬。我调试过一台设备,就是因为 AudioPolicyManager 里状态变更和路由切换之间差了 200ms,导致用户每次接电话都被吓一跳。
核心逻辑:状态变更 → 策略评估 → 设备选择 → 路由切换。每一步都有回调,每一步都可能出问题。
18.3 蓝牙通话路由的「特殊待遇」
蓝牙通话路由,跟媒体音频路由完全是两码事。媒体走 A2DP,通话走 SCO(同步面向连接链路)。SCO 链路的建立和释放,是蓝牙通话路由的关键。
我个人习惯把蓝牙通话路由分成三个阶段:
- 连接阶段:蓝牙耳机配对成功后,系统发起 SCO 连接请求
- 稳定阶段:SCO 链路建立,音频走蓝牙
- 断开阶段:通话结束或用户切回听筒,释放 SCO
这里有个大坑:SCO 连接不是瞬间完成的。我曾经调试过一个 case,用户按了蓝牙耳机上的接听键,系统立刻把音频路由到蓝牙,但 SCO 还没建立——结果就是前 1 秒通话没声音。解决方案是:等 SCO 连接状态回调确认后,再切换路由。
避坑指南:我曾经在 SCO 连接超时处理上栽过跟头。某些蓝牙耳机在信号弱时,SCO 连接可能耗时 3-5 秒。如果系统在超时后直接切回听筒,用户会听到「声音突然从耳机跳到手机」的体验。建议设置合理的超时阈值(比如 8 秒),并给用户一个明确的提示。
18.4 VoIP 音频路由:不一样的玩法
VoIP(如微信、Skype)的音频路由,跟传统电话有本质区别。传统电话走 Telephony 框架,VoIP 走的是 AudioTrack/AudioRecord 直接通路。
为什么不一样?因为 VoIP 应用自己控制音频流,系统只能提供「建议路由」,不能强制。举个例子:
- 传统电话:系统检测到蓝牙连接,自动把通话路由到蓝牙
- VoIP 电话:系统通知应用「有蓝牙设备可用」,应用自己决定用不用
嗯,这里要注意:Android 10 之后引入了 AudioManager.setCommunicationDevice() 接口,让 VoIP 应用可以主动选择路由设备。但很多老应用还在用 setSpeakerphoneOn() 这种过时方式,导致路由行为不可控。
我的建议:如果你在开发 VoIP 应用,优先使用 AudioManager 的 getAvailableCommunicationDevices() 和 setCommunicationDevice() 接口。这样能保证路由行为跟系统策略一致,避免出现「选了蓝牙但声音还在听筒」的怪事。
18.5 路由策略的代码级实现
咱们看看 AudioPolicyManager 里路由决策的核心逻辑。简化后的伪代码:
// AudioPolicyManager::getDeviceForStrategy
audio_devices_t getDeviceForStrategy(audio_strategy_t strategy) {
switch (strategy) {
case STRATEGY_PHONE:
// 通话策略:优先蓝牙 SCO
if (isBluetoothScoAvailable()) {
return AUDIO_DEVICE_OUT_BLUETOOTH_SCO;
}
// 其次有线耳机
if (isWiredHeadsetConnected()) {
return AUDIO_DEVICE_OUT_WIRED_HEADSET;
}
// 默认听筒
return AUDIO_DEVICE_OUT_EARPIECE;
case STRATEGY_SONIFICATION:
// 铃声策略:强制扬声器
return AUDIO_DEVICE_OUT_SPEAKER;
// ... 其他策略
}
}
这段代码看着简单,但实际项目里会复杂得多。比如蓝牙 SCO 可用性判断,要检查 SCO 状态、蓝牙配置文件、甚至蓝牙耳机的电池电量(某些厂商策略会低电量时禁用 SCO)。
18.6 路由切换的时序问题
路由切换不是瞬间完成的。从策略决策到设备实际切换,中间涉及:
- 策略引擎计算新设备
- AudioFlinger 关闭旧设备上的音频流
- AudioFlinger 在新设备上打开音频流
- 底层 HAL 执行设备切换
- 通知上层路由变更完成
我调试过一个 case:用户从听筒切到扬声器,声音有 300ms 的中断。原因是 AudioFlinger 在关闭听筒流之后,才去打开扬声器流——中间有个「无设备」的空窗期。解决方案是采用「先开后关」的策略:先打开扬声器流,再关闭听筒流,实现无缝切换。
关键点:路由切换的「无缝」体验,取决于底层 HAL 是否支持同时打开多个输出设备。如果不支持,那就只能忍受短暂的中断。
18.7 知识体系总览
下面这张图,把通话音频路由的核心逻辑串起来了:
这张图展示了从触发源到最终执行的完整链路。你会发现,VoIP 在图中没有直接出现——因为它走的是另一条路:应用层直接控制 AudioTrack,系统只提供设备列表和建议。
18.8 实战中的避坑清单
最后,我把自己这些年踩过的坑整理成清单,希望能帮你少走弯路:
- 蓝牙 SCO 时序:不要在 SCO 连接完成前切换路由,否则会丢音频
- 听筒/扬声器切换:先开新设备,再关旧设备,避免音频中断
- VoIP 路由:用
setCommunicationDevice()替代过时的setSpeakerphoneOn() - 多设备场景:同时插着有线耳机和蓝牙耳机时,策略优先级要明确
- 状态机边界:响铃状态切到通话状态时,路由切换要延迟到状态稳定后
通话音频路由这块,说难不难,说简单也不简单。核心就是理解状态机、设备优先级、以及时序控制。你只要把这三点吃透了,大部分路由问题都能迎刃而解。