1. 从一个真实的音频打架问题说起
做Android蓝牙音频开发的朋友,大概率都遇到过这种场景:戴着蓝牙耳机听歌,突然来了一通电话,音乐停了,通话正常,挂断之后音乐又自动恢复。整个过程行云流水,用户觉得很自然。但如果你恰好是那个写代码的人,就知道这背后其实藏着一套相当精密的音频路由切换机制——A2DP要暂停,SCO要启动,两者之间还有严格的时序依赖。稍有不慎,就会出现音乐和通话音同时从耳机里冒出来、通话前两秒没声音、挂断后音乐不恢复、甚至SCO建链失败导致通话直接走手机听筒等一系列问题。
我在过去几年里做过好几个车载蓝牙和TWS耳机的项目,几乎每个项目都会在SCO和A2DP的协同上踩坑。最典型的一次是通话建立后,A2DP流没有及时挂起,结果对方听到的是我这边音乐的回声,而我这边听到的是音乐和通话混在一起,体验极差。后来把整个流程从上层到协议栈捋了一遍,才把问题定位清楚。
这篇内容就是把这套机制彻底拆开讲清楚。核心围绕三个问题展开:A2DP为什么必须暂停、SCO什么时候启动、两者之间的时序怎么保证不出错。适合做Android蓝牙音频开发、车载蓝牙、TWS耳机固件、以及需要处理通话音频路由的工程师参考。不管你是刚接触蓝牙协议栈的新手,还是已经调过几轮音频路由的老手,应该都能从里面找到一些之前没注意到的细节。
2. 先搞清楚A2DP和SCO到底各管什么
2.1 两个Profile的本质区别
很多人一开始会把A2DP和SCO混为一谈,觉得都是蓝牙音频,无非一个音质好一个音质差。这个理解不算错,但不够准确,因为它们在蓝牙协议栈里的定位完全不同。
A2DP全称Advanced Audio Distribution Profile,走的是ACL链路,底层依赖L2CAP和AVDTP,传输的是高质量立体声音频,典型编码格式包括SBC、AAC、aptX、LDAC等。它的设计目标是单向、高带宽、音乐级音质,延迟通常在100ms到200ms级别,对实时性要求不高,但对音质和稳定性要求很高。
SCO全称Synchronous Connection Oriented,是蓝牙经典协议里专门为双向语音设计的链路类型。它走的是同步连接,带宽固定,典型是8kHz或16kHz采样率的窄带或宽带语音,延迟极低,通常在几毫秒到十几毫秒级别。HFP(Hands-Free Profile)和HSP(Headset Profile)都依赖SCO来传输通话语音。
用一个生活化的类比:A2DP像是一条宽阔的高速公路,专门用来运大件货物,跑得稳但反应慢;SCO像是一条专用的应急通道,窄但随时待命,反应极快。两者在物理层共享同一个蓝牙射频,但在协议栈上层是两套独立的通路。
2.2 为什么不能同时跑
既然A2DP和SCO是两套独立通路,那能不能同时跑?理论上蓝牙控制器支持多链路,但在实际产品里,同时跑A2DP和SCO会带来几个致命问题。
第一是射频资源争抢。蓝牙经典模式下的射频时分复用,A2DP占用的时隙多,SCO需要固定的同步时隙。如果两者同时活跃,SCO的同步时隙可能被A2DP挤占,导致通话断续。第二是功耗和带宽。同时维持两条高负载链路,对耳机端的芯片压力很大,尤其是低成本方案,很容易出现丢包和爆音。第三是回声和混音问题。如果A2DP音乐没有暂停,SCO通话建立后,耳机端可能把两路音频同时送到DAC,用户就会听到音乐和通话混在一起。
所以行业里的通用做法是:通话建立时,A2DP必须暂停或挂起,把射频和音频通路让给SCO;通话结束后,SCO断开,A2DP再恢复。这套“让路”机制就是标题里说的协同机制。
2.3 协同机制涉及的关键角色
要理解这套机制,得先知道有哪些角色参与。在Android侧,主要有这几个:
- BluetoothA2dp:管理A2DP连接和音频流状态,负责suspend和resume。
- BluetoothHeadset:管理HFP连接,负责SCO的建立和断开。
- AudioPolicyManager:音频策略管理器,决定音频走哪个输出设备。
- AudioFlinger:音频混音和输出,实际把PCM数据送到HAL层。
- HAL层蓝牙音频模块:把PCM通过SCO或A2DP送到蓝牙控制器。
在耳机或车机侧,则涉及蓝牙协议栈、DSP音频通路、以及本地的音频路由逻辑。两边通过AT命令、HFP状态机、以及AVDTP的信令来协调。
3. 通话建立时A2DP暂停的完整流程
3.1 从电话接入到A2DP Suspend
一通电话进来,整个链路是这样走的。首先Telecom服务收到来电或去电请求,触发音频模式切换,AudioPolicyManager把音频模式从NORMAL切到IN_CALL或IN_COMMUNICATION。这个模式切换会触发音频设备重新路由,系统开始寻找可用的通话输出设备。
如果此时蓝牙耳机已连接且支持HFP,AudioPolicyManager会优先选择SCO设备作为通话输出。但SCO链路还没建立,所以系统会先通知BluetoothHeadset去建立SCO。与此同时,BluetoothA2dp收到音频模式变化的通知,开始执行suspend流程。
这里有个关键点:A2DP的suspend不是简单地把音频流停掉,而是要通过AVDTP信令发送SUSPEND命令给远端设备,让远端也停止解码和播放。如果只停本地流不发信令,远端可能还在播放缓冲区里的残留数据,导致通话前几百毫秒还有音乐。
3.2 Suspend的信令交互细节
A2DP suspend的AVDTP信令流程大致如下:
- 本地AVDTP层构造SUSPEND命令,指定要暂停的流端点。
- 通过L2CAP通道发送给远端。
- 远端收到后,停止解码,返回SUSPEND响应。
- 本地收到响应后,标记流状态为SUSPENDED,停止向HAL层送PCM数据。
这个过程在Android源码里对应A2dpStateMachine的状态迁移,从Started状态收到suspend事件后,会调用sendSuspend,然后等待远端响应。如果远端响应超时,本地会强制把流标记为挂起,但远端可能还在播放,这就是为什么有些耳机在通话建立瞬间会有短暂的音乐残留。
注意:suspend的超时时间在AOSP里默认是几秒级别,实际产品里建议根据耳机兼容性调整。我遇到过某些耳机响应特别慢,把超时从默认值缩短到500ms后,通话建立速度明显改善,但兼容性测试要覆盖足够多的机型。
3.3 为什么必须先Suspend再启动SCO
顺序不能反。如果先启动SCO再suspend A2DP,中间会有一个时间窗口,两条链路同时活跃。这个窗口哪怕只有几十毫秒,也足以让用户听到音乐和通话的混音。更严重的是,SCO建立过程中需要射频资源,如果A2DP还在占用大量时隙,SCO的建链可能失败或延迟。
所以正确的顺序是:音频模式切换 → A2DP suspend → SCO建立 → 通话音频走SCO。这个顺序在Android的AudioPolicy和Bluetooth服务里是有明确依赖的,但不同厂商的定制ROM可能会有差异,这也是为什么同一款耳机在不同手机上表现不一样的原因之一。
4. SCO启动的时机与建链过程
4.1 SCO建链的触发条件
SCO的建立不是随便什么时候都能触发的。它需要满足几个条件:HFP服务连接已建立、音频模式已切换到通话模式、远端设备支持SCO、以及射频资源可用。
在Android侧,SCO建立通常由BluetoothHeadset的connectAudio方法触发。这个方法会通过HFP的AT命令或直接通过HCI层发起SCO连接请求。具体走哪条路取决于芯片方案和协议栈实现。高通方案通常走HCI直连,MTK方案可能走AT命令协商。
SCO建链的HCI命令序列大致是:HCI_Enhanced_Setup_Synchronous_Connection,里面会指定带宽、编码、重传窗口等参数。这些参数直接影响通话质量和延迟。比如重传窗口设大了,抗干扰强但延迟高;设小了延迟低但容易丢包。
4.2 SCO参数选择与计算
SCO的参数选择是个技术活。以窄带语音为例,典型参数是:
- 采样率:8kHz
- 编码:CVSD或mSBC
- 时隙:每帧1个时隙
- 重传窗口:通常2到4个时隙
如果用mSBC宽带语音,采样率升到16kHz,带宽需求翻倍,重传窗口要相应调整。计算带宽需求的公式大致是:采样率 × 位深 × 声道数 × 开销系数。8kHz × 16bit × 1声道 = 128kbps,加上协议开销大约160kbps。16kHz mSBC大约320kbps。蓝牙经典模式的总带宽大约1Mbps左右,所以SCO占用不算大,但同步时隙的固定性要求高。
实操心得:在调试SCO参数时,不要只看理论值。实际产品里,耳机端的DSP处理能力、天线设计、以及和WiFi的共存情况都会影响SCO质量。我一般会准备几组参数做A/B测试,用实际通话录音来评估,而不是只看HCI日志里的建链成功。
4.3 SCO建立后的音频路由切换
SCO建链成功后,AudioPolicyManager会把通话音频的输出设备切换到SCO。这个切换涉及AudioFlinger重新配置输出线程,把PCM数据从原来的A2DP输出转到SCO输出。同时,输入侧也要从手机麦克风或耳机麦克风切换到SCO输入。
这个切换过程中,如果A2DP的suspend还没完成,就会出现短暂的音频路由冲突。Android的AudioPolicy里有一个setOutputDevice的调用,会等待所有输出线程完成切换。如果某个线程卡住,整个切换就会延迟,用户就会感觉通话前几百毫秒没声音。
5. 通话结束后的恢复流程
5.1 SCO断开与A2DP Resume
通话结束后,Telecom服务触发音频模式切回NORMAL。AudioPolicyManager开始把音频路由切回A2DP。同时,BluetoothHeadset收到断开SCO的请求,通过HCI或AT命令断开SCO链路。
SCO断开后,BluetoothA2dp收到音频模式变化通知,开始执行resume流程。resume同样要通过AVDTP信令发送START命令给远端,远端重新启动解码,本地重新向HAL层送PCM数据。
这里有个常见的坑:如果SCO断开和A2DP resume之间的间隔太短,远端可能还没完全释放SCO资源,导致A2DP resume失败。我遇到过某些耳机在挂断后音乐不恢复,就是因为resume命令发得太早,远端还在处理SCO断开。解决办法是在SCO断开后加一个短暂的延迟,或者等待远端返回SCO断开确认后再发resume。
5.2 恢复失败的常见原因
恢复失败的原因很多,我整理了几类常见的:
| 失败现象 | 可能原因 | 排查方向 |
|---|---|---|
| 挂断后音乐不恢复 | resume信令超时或远端未响应 | 抓AVDTP日志,看START命令是否发出和响应 |
| 音乐恢复但没声音 | 音频路由未切回A2DP | 检查AudioPolicy的setOutputDevice调用 |
| 音乐恢复后音质变差 | A2DP编码协商回退 | 检查AVDTP的编码配置是否被重置 |
| 恢复后左右声道反了 | 远端解码器状态异常 | 尝试重新建链而非resume |
注意:有些耳机在SCO断开后会自动重连A2DP,这时候如果本地也发resume,可能会冲突。建议在resume前先检查A2DP连接状态,如果已经断开就重新建链,而不是发resume。
6. 时序协同的核心难点与排查方法
6.1 时序窗口到底有多窄
整个协同过程的时间窗口其实很窄。从音频模式切换到SCO建链成功,理想情况下应该在200ms到500ms内完成。如果超过1秒,用户就会明显感觉到通话延迟。而A2DP suspend必须在SCO建链前完成,否则会有混音。
我实测过几款主流手机和耳机,表现最好的组合能在300ms内完成整个切换,表现差的要1.5秒以上。差异主要来自协议栈实现、耳机响应速度、以及射频环境。
6.2 抓日志的正确姿势
排查这类问题,光看应用层日志没用,必须抓到协议栈和HCI层。Android上可以用btsnoop抓HCI日志,用Wireshark分析。重点看几个时间点:
- 音频模式切换的时间戳
- AVDTP SUSPEND命令的发送和响应时间
- HCI Enhanced Setup Synchronous Connection的时间
- SCO建链完成的事件
- 音频路由切换的日志
把这些时间点对齐,就能看出哪个环节拖了后腿。我一般会做一个时间线表格,把每个事件的时间戳和耗时列出来,一眼就能定位瓶颈。
6.3 常见问题速查
| 问题 | 排查步骤 | 解决方向 |
|---|---|---|
| 通话前几百毫秒有音乐 | 抓AVDTP日志看SUSPEND响应时间 | 缩短suspend超时,或强制本地先停流 |
| 通话建立慢 | 看SCO建链耗时和射频环境 | 优化SCO参数,检查WiFi共存 |
| 挂断后音乐不恢复 | 看resume信令和A2DP状态 | 加延迟或重新建链 |
| 通话有回声 | 检查A2DP是否真的suspend | 确认远端停止播放,检查本地混音 |
| SCO建链失败 | 看HCI错误码 | 检查射频资源,重试建链 |
7. 几个容易忽略的细节
7.1 音频焦点与A2DP暂停的关系
Android的音频焦点机制和A2DP suspend是两回事。音频焦点是应用层的概念,A2DP suspend是协议栈层的。有些开发者以为请求了音频焦点就能让A2DP暂停,其实不然。音频焦点只影响应用能否播放,不直接控制蓝牙链路。真正触发A2DP suspend的是音频模式切换和AudioPolicy的路由决策。
7.2 多设备连接时的优先级
现在很多耳机支持同时连接两台设备。当一台设备来电话时,另一台设备的A2DP可能还在播放。这时候协同机制会更复杂,因为耳机端要决定把SCO给哪台设备,A2DP要不要暂停。这个逻辑主要在耳机端实现,手机端只能通过HFP状态来间接影响。
7.3 不同Android版本的差异
Android 8.0到Android 14,蓝牙音频架构改了不少。比如Android 10引入了新的蓝牙音频HAL,Android 13对LE Audio的支持也影响了经典蓝牙的音频路由。同一个问题在不同版本上的表现可能不一样,排查时一定要确认版本。
8. 我个人在实际项目中的几点体会
做这类音频协同问题,最深的体会是:不要只盯着代码看,一定要结合实际抓包和听感测试。代码逻辑再完美,实际射频环境和耳机兼容性都可能让结果完全不同。我一般会准备一个测试矩阵,覆盖主流手机型号和耳机型号,每个组合都跑一遍通话建立、通话中、通话结束的完整流程,记录时间和听感。
另外,A2DP suspend和SCO启动的时序,不要试图用固定的延迟去凑。不同耳机响应速度差异很大,固定延迟要么不够要么浪费。更好的做法是基于信令响应来驱动,收到SUSPEND响应后再启动SCO,收到SCO建链完成后再切音频路由。这样虽然逻辑复杂一点,但兼容性和稳定性会好很多。
最后分享一个小技巧:如果遇到某些耳机在通话建立瞬间有音乐残留,可以尝试在发送AVDTP SUSPEND的同时,本地先静音A2DP输出。这样即使远端响应慢,本地也不会有音乐送到耳机。等SUSPEND响应回来后再正式标记挂起。这个做法在几个项目里都验证过,能明显改善通话建立瞬间的听感。