16、蓝牙音频支持:A2DP与HFP协议栈集成、蓝牙音频编解码器管理、蓝牙设备切换、SCO链路管理

蓝牙音频,说实话,是Android音频系统里最让人头疼的一块。为什么?因为蓝牙协议栈本身就很复杂,再加上各家芯片厂商的私有实现,还有各种奇葩的蓝牙耳机……我在做AudioFlinger的时候,光是蓝牙设备切换的bug就修了不下二十个。今天咱们就好好聊聊这块。

16.1 A2DP与HFP协议栈集成

先搞清楚两个协议的区别。A2DP是Advanced Audio Distribution Profile,负责立体声音乐播放。HFP是Hands-Free Profile,负责通话。说白了,一个听歌,一个打电话。

在AudioFlinger里,这两个协议对应不同的音频路径。我画个图你就明白了。

AudioFlinger蓝牙音频协议栈集成架构 AudioFlinger A2DP Audio HAL HFP Audio HAL Bluedroid / Fluoride Bluedroid / Fluoride 蓝牙控制器 (SoC / USB Dongle) A2DP路径 HFP路径

你看,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怎么处理?

流程大概是这样的:

  1. 用户发起切换请求(比如在蓝牙设置里点击连接新设备)
  2. BluetoothManagerService通知AudioService
  3. AudioService调用AudioPolicyService的setDeviceConnectionState
  4. AudioPolicyService更新路由策略,通知AudioFlinger
  5. 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链路。这个同步逻辑在TelephonyManagerBluetoothHeadset之间传递,AudioFlinger只管根据状态切换音频路径。

嗯,蓝牙音频这块内容确实多。但核心就三点:协议栈集成要正确、编解码器协商要合理、设备切换和SCO管理要稳健。把这些搞明白了,蓝牙音频的问题基本都能搞定。


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