16、蓝牙音频支持:A2DP与HFP协议栈集成、蓝牙音频编解码器管理、蓝牙设备切换、SCO链路管理
蓝牙音频,说实话,是Android音频系统里最让人头疼的一块。为什么?因为蓝牙协议栈本身就很复杂,再加上各家芯片厂商的私有实现,还有各种奇葩的蓝牙耳机……我在做AudioFlinger的时候,光是蓝牙设备切换的bug就修了不下二十个。今天咱们就好好聊聊这块。
16.1 A2DP与HFP协议栈集成
先搞清楚两个协议的区别。A2DP是Advanced Audio Distribution Profile,负责立体声音乐播放。HFP是Hands-Free Profile,负责通话。说白了,一个听歌,一个打电话。
在AudioFlinger里,这两个协议对应不同的音频路径。我画个图你就明白了。
你看,AudioFlinger往下走,分了两条路。A2DP走的是音乐播放的Audio HAL,HFP走的是通话的Audio HAL。这两条路在蓝牙协议栈里最终汇合,但处理逻辑完全不同。
我遇到过一个问题:某款手机播放音乐正常,但一打电话就没声音。查了半天,发现是HFP的Audio HAL没正确加载。说白了,就是协议栈集成没做好,AudioFlinger找不到对应的HFP设备节点。
16.2 蓝牙音频编解码器管理
编解码器这块,水很深。A2DP支持SBC、AAC、aptX、LDAC等等。HFP主要用CVSD和mSBC。AudioFlinger怎么管理这些编解码器?
核心逻辑在AudioPolicyService里。它会根据蓝牙设备的能力,选择合适的编解码器。举个例子:
// AudioPolicyService中编解码器选择逻辑(简化版)
status_t AudioPolicyService::selectBluetoothCodec(
const sp<BluetoothDevice>& device,
audio_format_t* selectedCodec) {
// 获取设备支持的编解码器列表
std::vector<audio_format_t> supportedCodecs;
device->getSupportedCodecs(&supportedCodecs);
// 按优先级选择:LDAC > aptX HD > AAC > SBC
for (auto codec : {AUDIO_FORMAT_LDAC, AUDIO_FORMAT_APTX_HD,
AUDIO_FORMAT_AAC, AUDIO_FORMAT_SBC}) {
if (std::find(supportedCodecs.begin(), supportedCodecs.end(), codec)
!= supportedCodecs.end()) {
*selectedCodec = codec;
return OK;
}
}
return BAD_VALUE;
}
这段代码看着简单,但实际坑很多。我曾经遇到一个case:某款耳机声称支持LDAC,但实际连接后音质很差。后来发现是耳机的LDAC实现有bug,采样率只能到44.1kHz,但AudioFlinger默认给它配了96kHz。嗯,这里要注意,编解码器协商不能只看格式,还要看采样率、比特率这些参数。
编解码器协商的关键参数:
- 音频格式(SBC/AAC/aptX/LDAC等)
- 采样率(44.1kHz/48kHz/96kHz)
- 比特率(越高音质越好,但延迟也大)
- 声道模式(立体声/单声道)
16.3 蓝牙设备切换
设备切换,说白了就是从一个蓝牙耳机切换到另一个。这个场景在现实中太常见了:你正在用耳机A听歌,突然想换成耳机B。AudioFlinger怎么处理?
流程大概是这样的:
- 用户发起切换请求(比如在蓝牙设置里点击连接新设备)
- BluetoothManagerService通知AudioService
- AudioService调用AudioPolicyService的
setDeviceConnectionState - AudioPolicyService更新路由策略,通知AudioFlinger
- AudioFlinger关闭旧设备的音频流,打开新设备的音频流
这里有个关键点:切换时机。如果正在播放音乐,突然切换设备,音频流会中断。我建议的做法是:
// 设备切换时的音频流处理
status_t AudioFlinger::switchBluetoothDevice(
const sp<DeviceHalInterface>& oldDevice,
const sp<DeviceHalInterface>& newDevice) {
// 1. 暂停当前播放
mPlaybackThread->pause();
// 2. 关闭旧设备
oldDevice->close();
// 3. 打开新设备
status_t status = newDevice->open();
if (status != OK) {
// 回退到旧设备
oldDevice->open();
mPlaybackThread->resume();
return status;
}
// 4. 重新配置音频路径
mPlaybackThread->setDevice(newDevice);
// 5. 恢复播放
mPlaybackThread->resume();
return OK;
}
注意:设备切换时,如果新设备打开失败,一定要有回退机制。我曾经见过一个产品,切换设备失败后直接静音了,用户只能重启手机。这就是没有做好异常处理。
16.4 SCO链路管理
SCO(Synchronous Connection-Oriented)链路,是HFP通话的基础。它建立了一条同步的、面向连接的通道,用于传输语音数据。
SCO链路的管理,主要在蓝牙协议栈层面。但AudioFlinger需要知道SCO链路的状态,才能正确路由音频。关键状态有:
| SCO状态 | 含义 | AudioFlinger处理 |
|---|---|---|
| SCO_STATE_INACTIVE | 无SCO连接 | 正常播放音乐 |
| SCO_STATE_ACTIVE | SCO连接建立 | 切换到通话音频路径 |
| SCO_STATE_PENDING | SCO连接正在建立 | 等待,不切换 |
| SCO_STATE_DISCONNECTING | SCO连接正在断开 | 保持通话路径,直到断开完成 |
我遇到过最头疼的问题:SCO链路建立后,语音延迟特别大。排查下来,发现是蓝牙协议栈的SCO参数配置不对。具体来说,是voice_setting这个参数没配好。
避坑指南:我曾经在调试SCO链路时,发现语音断断续续。后来查了蓝牙Core Spec才知道,SCO的voice_setting参数必须和编解码器匹配。比如用CVSD时,voice_setting要设为0x0003(线性PCM,16位,8kHz)。用mSBC时,要设为0x0060(线性PCM,16位,16kHz)。这个参数配错了,语音质量直接崩。
还有一个细节:SCO链路的建立和释放,需要和电话状态同步。比如来电时,系统要建立SCO链路;挂断时,要释放SCO链路。这个同步逻辑在TelephonyManager和BluetoothHeadset之间传递,AudioFlinger只管根据状态切换音频路径。
嗯,蓝牙音频这块内容确实多。但核心就三点:协议栈集成要正确、编解码器协商要合理、设备切换和SCO管理要稳健。把这些搞明白了,蓝牙音频的问题基本都能搞定。
公众号:蓝海资料掘金营,微信deep3321