4. A2DP 协议剖析:角色定义、流端点配置、编解码器协商流程

好,咱们今天来聊聊 A2DP。这个协议在蓝牙音频里,可以说是核心中的核心。没有它,你的耳机就只能打电话,听不了高品质音乐。

我刚开始接触蓝牙时,觉得 A2DP 不就是传个音频嘛,能有多复杂?后来踩了不少坑才明白,这里面的门道,尤其是编解码器协商那块,稍不注意就会出兼容性问题。今天我就把这块掰开了讲清楚。

4.1 A2DP 的角色定义:谁是 Source,谁是 Sink?

A2DP 定义了两种角色,说白了就是谁发数据,谁收数据。

  • Source(信源):音频数据的发送端。通常是你的手机、电脑、或者播放器。它负责把音频文件(比如 MP3、AAC)编码成蓝牙能传的格式,然后发出去。
  • Sink(信宿):音频数据的接收端。就是你的蓝牙耳机、音箱。它收到数据后,解码还原成声音,让你听到。

这里有个容易混淆的点:角色是固定的。手机永远是 Source,耳机永远是 Sink。不像有些协议(比如 GATT)可以主从切换。A2DP 不行,设计之初就定死了。

我的经验: 在调试时,一定要先确认两边的角色对不对。我曾经遇到一个 case,客户把 Source 设备配置成了 Sink 角色,结果两边一直在等对方发数据,自然连不上。这种低级错误,排查起来最费时间。

4.2 流端点配置:SEP 与 Codec 的绑定

角色定好了,接下来要配置「流端点」(Stream Endpoint,简称 SEP)。你可以把 SEP 想象成一个虚拟的音频通道。每个设备可以有多个 SEP,每个 SEP 绑定一种编解码器。

比如你的手机(Source)可以有两个 SEP:

  • SEP 1:绑定 SBC 编码器
  • SEP 2:绑定 AAC 编码器

你的耳机(Sink)也可以有两个 SEP:

  • SEP 1:绑定 SBC 解码器
  • SEP 2:绑定 AAC 解码器

配置 SEP 时,需要填写一个关键结构体——avdtp_sep_config。里面包含了媒体类型、编码类型、以及最重要的 Codec Specific Information Elements(编解码器特定信息元素)

// 伪代码示例:配置一个 SBC 编码的 SEP
struct avdtp_sep_config sep_cfg;

sep_cfg.media_type = AVDTP_MEDIA_TYPE_AUDIO;   // 媒体类型:音频
sep_cfg.codec_type = A2DP_CODEC_TYPE_SBC;      // 编码类型:SBC

// 填充 SBC 的特定信息(采样率、比特率、声道模式等)
sep_cfg.codec_info[0] = 0x01;  // 采样率 44.1kHz,声道模式:联合立体声
sep_cfg.codec_info[1] = 0x02;  // 块长度 16,子带 8
sep_cfg.codec_info[2] = 0x04;  // 比特池最小值
sep_cfg.codec_info[3] = 0x35;  // 比特池最大值

// 注册这个 SEP 到协议栈
avdtp_register_sep(&sep_cfg);
注意: 编解码器特定信息(codec_info)的格式,不同编码器完全不同。SBC 有 SBC 的格式,AAC 有 AAC 的格式。千万别搞混了。我曾经见过有人把 AAC 的配置填到 SBC 的字段里,结果协商永远失败。

4.3 编解码器协商流程:一场「你来我往」的谈判

配置好 SEP 后,Source 和 Sink 之间要开始「谈判」了。谈判的目标是:找到一个双方都支持的编解码器,以及双方都能接受的参数配置。

这个流程,在 AVDTP(音频/视频分发传输协议)里定义得很清楚。我把它拆成几个关键步骤:

  1. 发现(Discover):Source 问 Sink:「你有哪些 SEP?支持什么编码?」
  2. 获取能力(Get Capabilities):Source 对感兴趣的 SEP 进一步问:「你这个编码器,支持哪些采样率?比特率范围是多少?」
  3. 设置配置(Set Configuration):Source 根据 Sink 的能力,选一个双方都支持的配置,然后告诉 Sink:「我们就用这个配置了。」
  4. 打开流(Open Stream):配置确认后,打开逻辑通道,准备传输数据。
  5. 开始流(Start Stream):正式开始传输音频数据。

嗯,这里要注意,协商的核心是第三步——Set Configuration。Source 发过去的配置,Sink 可能会拒绝。比如 Source 说「我们用 48kHz 的 AAC 吧」,但 Sink 只支持 44.1kHz,那 Sink 就会返回一个错误码,Source 就得重新选一个配置。

避坑指南: 我曾经遇到过一个 bug,Source 发过去的配置,Sink 总是拒绝。查了半天,发现是 Sink 的 codec_info 里,比特池范围写得太小了。Source 选了一个超出范围的比特率,Sink 自然不认。所以,一定要确保两边的能力描述是准确且匹配的

4.4 协商中的「潜规则」:SBC 是保底方案

蓝牙 SIG 规定,所有 A2DP 设备必须支持 SBC 编码。这意味着,就算你的耳机不支持 AAC、LDAC、aptX,它也一定能支持 SBC。

所以,协商的逻辑通常是这样的:

  • Source 先尝试最高优先级的编码(比如 LDAC)。
  • 如果 Sink 不支持,就降级到 AAC。
  • 再不行,就降级到 SBC。
  • 如果 SBC 都不支持?那这个设备就不符合 A2DP 规范,直接断开连接。

你想想看,这个机制保证了最基本的兼容性。哪怕你的耳机再便宜,也能用 SBC 听歌,只是音质差点而已。

我的建议: 在开发 Source 端时,一定要实现一个「降级策略」。不要一上来就选最高级的编码,万一 Sink 不支持,整个连接就断了。先发一个能力请求,看看 Sink 支持什么,再从中选一个最好的。这才是优雅的做法。

4.5 实战中的常见问题

最后,我总结几个我在项目中经常遇到的问题,供你参考:

问题 原因 解决方案
协商失败,连接断开 Source 和 Sink 的 codec_info 不匹配 抓取 HCI log,对比两边的能力描述
声音卡顿、断断续续 比特率设置过高,蓝牙带宽不够 降低比特率,或者切换到更高效的编码(如 AAC)
只有 SBC 能连上,其他编码都不行 Sink 端没有正确注册其他编码的 SEP 检查 Sink 端的 SEP 注册代码,确保 AAC/aptX 等编码的 SEP 已正确初始化
连接成功后没有声音 流状态没有正确切换到「开始流」 检查 AVDTP 的状态机,确保调用了 Start Stream 命令

好了,关于 A2DP 的角色、SEP 配置和协商流程,今天就聊到这里。说白了,A2DP 的协商就是一场「互相试探」的过程。你把自己的能力亮出来,我看看我能不能接住。接住了,咱们就愉快地开始传音乐;接不住,那就降级到 SBC,至少还能听个响。

下一章,我们会深入 A2DP 的数据传输过程,看看音频数据是怎么被打包、发送、接收和重组的。到时候见。