音频编解码与 A2DP 媒体
本文档介绍 fw-AC63_BT_SDK 中 A2DP(Advanced Audio Distribution Profile,高级音频分发规范)媒体链路的完整实现:从手机端通过 AVDTP 信令协商 SBC 编解码能力,到 A2DP 媒体通道接收音频数据包,再到 audio_dec.c 中的解码器(a2dp_dec_hdl)完成 SBC 解码、采样率检测、丢包隐藏(PLC)、TWS 时钟同步并最终混音输出的全过程。
Purpose and Scope
本页覆盖"音频编解码与 A2DP 媒体"这一能力域的以下内容:
- A2DP 协议栈分层与 AVDTP 信令能力协商(SBC codec capability)
- 媒体数据通道 API(
a2dp_media_codec.h声明的媒体包缓冲与取包接口) - A2DP 解码器实现(
cpu/br30/audio_dec.c中的a2dp_dec_hdl及其状态机) - 采样率检测(
a2dp_sample_detect.h)、丢包隐藏(PLC)、TWS 编解码同步机制 - A2DP 模式下的系统时钟档位配置(
audio_codec_clock.c)
以下相关主题属于兄弟页面,不在本页展开:通话音频(HFP/mSBC/CVSD)、AVRCP 控制命令、TWS 无线对箱整体架构、EQ/音效处理细节。需要时请参阅对应目录页面。
概述
A2DP 是经典蓝牙(BR/EDR)上承载高质量单向立体声音乐流的规范。AC63 系列芯片的 SDK 采用 BTstack 协议栈 + 自研媒体管线的组合:
- 信令面(AVDTP):手机与耳机通过 AVDTP 发现、协商并配置流端点(SEID),SBC 是 A2DP 强制支持的编解码器。协商结果通过
btstack_event.h中的AVDTP_SUBEVENT_SIGNALING_MEDIA_CODEC_SBC_CAPABILITY事件上报。 - 数据面(Media Transport):音频数据以 RTP 封装(payload 为 SBC 帧)经 ACL 通道到达;SDK 侧由
a2dp_media_codec模块缓冲为包队列,供解码线程拉取。 - 解码与输出:
audio_dec.c实现解码器a2dp_dec_hdl,它从媒体队列取包、剥离 RTP/帧头、执行 SBC 解码(硬件/软件解码器)、逐帧做采样率检测,并在丢包时通过 PLC(Packet Loss Concealment)填补,最终将 PCM 送入混音器(audio_mixer_ch)输出到 DAC。
关键设计意图:媒体包缓冲与解码消费解耦。协议栈把收到的媒体包放入队列,解码器按自己的节奏取包,并通过序号(seqn)比较、剩余播放时间等指标做抗抖动与重同步,从而在蓝牙射频波动下保持音频连续。
架构
flowchart TD
subgraph sg_Phone["对端设备(手机/发射端)"]
SRC["A2DP Source"]
end
subgraph sg_BT["蓝牙协议栈(BTstack)"]
AVDTP["AVDTP 信令<br/>SBC 能力协商"]
MEDIA["a2dp_media_codec<br/>媒体包缓冲队列"]
end
subgraph sg_DEC["解码层(cpu/br30/audio_dec.c)"]
DEC["a2dp_dec_hdl<br/>A2DP 解码器"]
DET["a2dp_sample_detect<br/>采样率检测"]
PLC["LFaudio_plc<br/>丢包隐藏"]
SYNC["audio_syncts / bt_audio_timestamp<br/>TWS 同步"]
end
subgraph sg_OUT["输出链路"]
MIX["audio_mixer_ch 混音"]
DAC["DAC"]
end
subgraph sg_CLK["时钟管理"]
CLK["audio_codec_clock<br/>A2DP_MODE 档位"]
end
SRC -->|"ACL 媒体数据 (RTP/SBC)"| MEDIA
SRC -->|"AVDTP 信令"| AVDTP
AVDTP -->|"配置 SBC 流端点"| MEDIA
MEDIA -->|"a2dp_media_fetch_packet"| DEC
DEC --> DET
DEC --> PLC
DEC --> SYNC
DEC --> MIX
MIX --> DAC
CLK -->|"SYS_64M 等档位"| DEC
各组件职责
| 组件 | 文件 | 职责 |
|---|---|---|
| AVDTP 信令 | include_lib/btstack/btstack_event.h | 上报 SBC 能力协商事件(cid、SEID、采样率位图) |
| 媒体通道 | include_lib/btstack/a2dp_media_codec.h | 媒体包队列:取包、剩余缓冲/播放时间、按序号清包 |
| A2DP 解码器 | cpu/br30/audio_dec.c | a2dp_dec_hdl 状态机:取帧、解码、修复、输出 |
| 采样率检测 | include_lib/media/a2dp_sample_detect.h | 逐帧统计样本数,估算真实采样率,驱动时钟微调 |
| 丢包隐藏 | cpu/br30/audio_dec.c(A2DP_AUDIO_PLC_ENABLE,调用 media/tech_lib/LFaudio_plc_api.h) | 对丢失/损坏的帧做波形预测填补 |
| TWS 同步 | media/audio_syncts.h、media/bt_audio_timestamp.h | 主/从箱播放时钟对齐(AUDIO_CODEC_SUPPORT_SYNC) |
| 时钟管理 | cpu/br30/audio_codec_clock.c | 按功能组合选择 SYS_64M/SYS_96M 等时钟档位 |
这种分层把"协议栈收包"与"音频消费"隔离:即使射频突发丢包或解码瞬时繁忙,队列仍能提供缓冲,解码器通过序号与时间戳判断是否需要清空或等待,保证音画(声像)同步与播放连续性。
AVDTP 信令与 SBC 能力协商
A2DP 会话的建立走 AVDTP(Audio/Video Distribution Transport Protocol)信令。协议栈在协商阶段产生 SBC capability 相关事件,include_lib/btstack/btstack_event.h 提供了一组内联访问函数来读取事件字段,例如:
* @brief Get field avdtp_cid from event AVDTP_SUBEVENT_SIGNALING_MEDIA_CODEC_SBC_CAPABILITY
static inline uint16_t avdtp_subevent_signaling_media_codec_sbc_capability_get_avdtp_cid(const uint8_t *event)
...
* @brief Get field sampling_frequency_bitmap from event AVDTP_SUBEVENT_SIGNALING_MEDIA_CODEC_SBC_CAPABILITY
Source: btstack_event.h
该事件携带 avdtp_cid、local_seid、remote_seid、media_type 与 sampling_frequency_bitmap 等字段,用于确认对端支持的采样率与声道配置。SBC 作为 A2DP 的强制编解码器,协商结果决定后续媒体包的 payload 格式。
控制命令面(avctp_user)
include_lib/btstack/avctp_user.h 还暴露了 A2DP 媒体相关的用户控制命令与辅助接口:
USER_CTRL_TWS_SYNC_SBC_CDM,
USER_CTRL_TWS_RESTART_SBC_CDM,
/*能量检测之后有一些判断流程要走,非蓝牙开发不用*/
extern int sbc_energy_check(u8 *packet, u16 size);
/*配置通话使用16k的msbc还是8k的cvsd*/
extern void __set_support_msbc_flag(bool flag);
Source: avctp_user.h(另见 L549、L594-L595)
USER_CTRL_TWS_SYNC_SBC_CDM/USER_CTRL_TWS_RESTART_SBC_CDM:TWS 场景下主从箱同步/重启 SBC 解码的命令字。sbc_energy_check():对 SBC 包做能量检测(用于防伪/事件识别等判断流程)。__set_support_msbc_flag():切换通话编码(16k mSBC 或 8k CVSD),属于 HFP 侧能力,此处仅说明其在 avctp_user 层与编解码配置共存。
媒体数据通道(a2dp_media_codec)
媒体包到达后由协议栈写入缓冲队列,解码侧通过 include_lib/btstack/a2dp_media_codec.h 声明的接口访问。该头文件还定义了蓝牙序号比较宏:
#define seqn_after(a, b) ((s16)((s16)(b) - (s16)(a)) < 0)
#define seqn_before(a, b) seqn_after(b, a)
extern int a2dp_media_get_packet(u8 **frame);
extern int a2dp_media_try_get_packet(u8 **frame);
extern int a2dp_media_get_remain_buffer_size();
extern int a2dp_media_get_remain_play_time(u8 include_tws);
extern int a2dp_media_get_total_data_len();
extern int a2dp_media_get_packet_num();
extern int a2dp_media_clear_packet_before_seqn(u16 seqn_number);
extern void *a2dp_media_fetch_packet(int *len, void *prev_packet);
extern void *a2dp_media_fetch_packet_and_wait(int *len, void *prev_packet, int msec);
extern void a2dp_media_free_packet(void *_packet);
extern int a2dp_media_channel_exist(void);
extern int a2dp_media_is_clearing_frame(void);
extern int a2dp_media_get_codec_type();
Source: a2dp_media_codec.h
设计意图:
seqn_after/before使用s16有符号回绕比较,正确处理 16 位 RTP 序号翻转(sequence wrap-around)问题,这是音频连续性判断的基础。fetch_packet/fetch_packet_and_wait返回解码器可消费的帧指针(区别于get_packet的原生包),并支持传入prev_packet以迭代遍历队列;and_wait变体在队列空时最多阻塞msec毫秒。clear_packet_before_seqn(seqn)用于在重同步时将指定序号之前的包全部丢弃,配合is_clearing_frame查询当前是否处于清帧状态。get_remain_play_time(include_tws)返回剩余可播时间(可计入 TWS 对端缓存),是上抛到应用层做缓冲水位提示的指标。
A2DP 解码器实现(a2dp_dec_hdl)
cpu/br30/audio_dec.c 是 A2DP 媒体消费的核心文件。文件顶部通过宏开关配置特性:
#define AUDIO_CODEC_SUPPORT_SYNC 1
#define A2DP_AUDIO_PLC_ENABLE 1
#if A2DP_AUDIO_PLC_ENABLE
#include "media/tech_lib/LFaudio_plc_api.h"
#endif
#define A2DP_RX_AND_AUDIO_DELAY 1
Source: audio_dec.c
AUDIO_CODEC_SUPPORT_SYNC:使能编解码时钟同步(TWS 主从对齐、timestamp 驱动)。A2DP_AUDIO_PLC_ENABLE:使能丢包隐藏,调用杰理自研LFaudio_plc_api.h的 PLC 算法。A2DP_RX_AND_AUDIO_DELAY:使能"接收即延迟"策略,即收到数据后引入固定音频延迟,为抗抖动与 PLC 留出处理窗口。
解码器句柄 struct a2dp_dec_hdl 是理解整个状态机的钥匙:
struct a2dp_dec_hdl {
struct audio_decoder decoder;
struct audio_res_wait wait;
struct audio_mixer_ch mix_ch;
enum audio_channel channel;
u8 start;
u8 ch;
s16 header_len;
u8 remain;
u8 fetch_lock;
u8 preempt;
u8 stream_error;
u8 new_frame;
u8 repair;
void *sample_detect;
void *syncts;
void *repair_pkt;
s16 repair_pkt_len;
u16 missed_num;
u16 repair_frames;
u16 overrun_seqn;
u16 slience_frames;
#if AUDIO_CODEC_SUPPORT_SYNC
...
};
Source: audio_dec.c
字段语义(设计意图):
| 字段 | 含义 |
|---|---|
decoder / mix_ch / channel | 标准音频解码器基类、混音通道与声道信息,接入统一音频框架 |
start / ch | 播放启动标志与声道数 |
header_len / remain | RTP/帧头长度与剩余帧数据,用于跨包拼接 SBC 帧 |
fetch_lock / preempt | 取包锁与抢占标志,保护队列访问 |
stream_error / new_frame | 流错误标志与新帧到达标志 |
repair / repair_pkt / repair_pkt_len | PLC 修复包缓存,保存用于填补的参考帧 |
missed_num / repair_frames | 累计丢帧数与已修复帧数 |
overrun_seqn / slience_frames | 溢出时的参考序号与静音填充帧计数 |
sample_detect | a2dp_sample_detect_open() 返回的采样率检测句柄 |
syncts | TWS/播放时钟同步句柄(AUDIO_CODEC_SUPPORT_SYNC 时启用) |
解码主循环的控制流
解码器按"取包 → 拆帧 → 检测采样率 → 解码 → 修复/重同步 → 混音输出"循环工作:
flowchart TD
Start([解码线程启动]) --> Fetch{"a2dp_media_fetch_packet<br/>取到帧?"}
Fetch -->|"否(超时)"| Check{"stream_error 或<br/>队列空?"}
Check -->|"是"| Repair["PLC 修复 /<br/>slience 填充"]
Check -->|"否"| Wait["等待新帧<br/>(fetch_lock)"]
Wait --> Fetch
Fetch -->|"是"| Seq{"序号连续?<br/>(seqn_after)"}
Seq -->|"丢帧"| Repair
Seq -->|"乱序/溢出"| Clear["clear_packet_before_seqn<br/>重同步"]
Repair --> Decode["SBC 解码"]
Clear --> Decode
Decode --> Detect["a2dp_frame_sample_detect<br/>采样率检测"]
Detect --> Sync["syncts 时钟对齐<br/>(TWS)"]
Sync --> Mix["混音输出到 DAC"]
Mix --> Fetch
核心思路:以序号驱动状态。正常时逐帧解码;检测到跳号(missed_num 累加)时用 PLC 填补;检测到序号倒退或溢出(overrun_seqn)时清空旧包并重同步,防止旧数据污染新流。
采样率检测(a2dp_sample_detect)
include_lib/media/a2dp_sample_detect.h 提供逐帧采样率检测接口,用于校准本地播放时钟:
void *a2dp_sample_detect_open(int sample_rate, u32 coding_type);
int a2dp_frame_sample_detect_start(void *hdl, u32 time);
int a2dp_frame_sample_detect(void *hdl, void *data, int len, u32 time);
void a2dp_sample_detect_close(void *hdl);
Source: a2dp_sample_detect.h
open:按期望采样率与编码类型(coding_type)创建检测句柄;hdl保存在a2dp_dec_hdl.sample_detect。frame_sample_detect_start/frame_sample_detect:在每帧解码前后调用,传入解码数据与硬件时间戳time,模块统计实际样本数与时间差,从而得出真实采样率漂移。close:释放句柄。
该模块的输出喂给 audio_codec_clock 或播放时钟微调逻辑,以补偿 SBC 时钟与本地晶振之间的偏差——这是长时间播放不逐渐加速/减速的关键。
核心流程:从手机播放到耳机出声
sequenceDiagram
participant P as 手机 (A2DP Source)
participant S as BTstack AVDTP 信令
participant M as a2dp_media_codec
participant D as a2dp_dec_hdl
participant T as sample_detect / syncts
participant O as 混音 + DAC
P->>S: 发现/能力交换(含 SBC capability)
S-->>P: 本地 SEID + 采样率位图
P->>S: 配置流端点 / 打开 / 启动流
S-->>D: 通知解码器启动 (start)
P->>M: RTP/SBC 媒体包(ACL)
M->>D: a2dp_media_fetch_packet_and_wait()
D->>D: 拆 RTP 头 (header_len),拼接 SBC 帧
D->>T: a2dp_frame_sample_detect_start(time)
D->>D: SBC 解码 → PCM
D->>T: a2dp_frame_sample_detect(data,len,time)
alt 序号跳变(丢包)
D->>D: PLC 修复 / 静音填充
end
T->>O: 播放时钟校准(TWS 对齐)
D->>O: PCM 混音输出
O-->>D: 采样回调(能量检测等)
流程要点:
- 协商阶段:AVDTP 事件
AVDTP_SUBEVENT_SIGNALING_MEDIA_CODEC_SBC_CAPABILITY上报对端 SBC 能力,本地按能力位图选择采样率/声道并配置 SEID。 - 流启动:
start置位后解码器进入取包循环;fetch_lock保证同一时刻只有一个消费方在队列上操作。 - 帧消费:
fetch_packet返回的帧可能横跨多个 RTP 包(SBC 帧可小于包长),由header_len/remain完成跨包拼接。 - 逐帧检测:每帧解码前后调用
a2dp_frame_sample_detect*,统计样本与时间戳,修正采样率漂移。 - 异常路径:丢帧 → PLC 修复;序号倒退/溢出 →
clear_packet_before_seqn清旧帧重同步;长时间无数据 → 静音填充并上报stream_error。 - 输出:PCM 进入
audio_mixer_ch混音,与提示音/通话等其它音源混合后送 DAC。
A2DP 模式下的系统时钟配置
cpu/br30/audio_codec_clock.c 根据功能宏组合为 A2DP 播放选择 CPU/音频时钟档位:
//A2DP_MODE
{
#if A2DP_AUDIO_PLC_ENABLE
SYS_64M,
#elif (TCFG_BT_MUSIC_EQ_ENABLE && ((EQ_SECTION_MAX >= 10) || AUDIO_OUT_EFFECT_ENABLE))
SYS_64M,
...
Source: audio_codec_clock.c(
cpu/bd19/audio_codec_clock.c结构相同)
设计意图:按需升频。纯 A2DP 播放且开启 PLC 时使用 SYS_64M 以给 PLC 算法留出算力;若叠加 EQ(段数 ≥ 10)或输出音效(AUDIO_OUT_EFFECT_ENABLE),同样需要更高档位。时钟档位的选择直接决定解码线程能否在帧间隔(约 23ms/帧 @ 44.1kHz)内完成"取包+解码+PLC+检测"全部工作,是音频不卡顿的底层保障。
用法示例
以下示例均直接取自仓库源码,展示各层接口的实际调用形态。
示例 1:媒体包消费(解码线程侧)
解码线程通过 a2dp_media_fetch_packet_and_wait 阻塞取帧,取不到时进入等待:
extern void *a2dp_media_fetch_packet(int *len, void *prev_packet);
extern void *a2dp_media_fetch_packet_and_wait(int *len, void *prev_packet, int msec);
extern void a2dp_media_free_packet(void *_packet);
extern int a2dp_media_channel_exist(void);
extern int a2dp_media_is_clearing_frame(void);
extern int a2dp_media_get_codec_type();
Source: a2dp_media_codec.h
调用方先检查 a2dp_media_channel_exist() 确认媒体通道存在,再取包;prev_packet 非空时表示迭代继续,解码完成后必须 a2dp_media_free_packet() 归还,否则队列会被耗尽。
示例 2:采样率检测句柄的生命周期
解码器打开时创建检测句柄,每帧检测,关闭时释放(接口由 a2dp_dec_hdl.sample_detect 持有):
void *a2dp_sample_detect_open(int sample_rate, u32 coding_type);
int a2dp_frame_sample_detect_start(void *hdl, u32 time);
int a2dp_frame_sample_detect(void *hdl, void *data, int len, u32 time);
void a2dp_sample_detect_close(void *hdl);
Source: a2dp_sample_detect.h
示例 3:序号比较宏(抗回绕)
RTP 序号为 16 位,比较时须按有符号回绕处理,这正是 seqn_after/before 宏的用途:
#define seqn_after(a, b) ((s16)((s16)(b) - (s16)(a)) < 0)
#define seqn_before(a, b) seqn_after(b, a)
Source: a2dp_media_codec.h
例如判断 seqn_b 是否早于 seqn_a(用于决定 a2dp_media_clear_packet_before_seqn(seqn) 是否触发)时,直接调用宏即可,无需手工处理 0xFFFF→0x0000 翻转。
示例 4:音量联动(应用层调用)
app_audio.c 在系统音量为数字音量类型时,将音量直接下发到 A2DP 数字音量模块:
#if (SYS_VOL_TYPE == VOL_TYPE_DIGITAL)
extern void a2dp_digital_vol_set(u8 vol);
a2dp_digital_vol_set(volume);
#endif
Source: app_audio.c(另见 L674-L675)
配置选项
以下宏/选项在 audio_dec.c、audio_codec_clock.c 及配置头文件中定义,控制 A2DP 媒体链路行为:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
AUDIO_CODEC_SUPPORT_SYNC | 宏 | 1 | 使能编解码时钟同步(TWS timestamp 对齐) |
A2DP_AUDIO_PLC_ENABLE | 宏 | 1 | 使能丢包隐藏(调用 LFaudio_plc_api.h),并影响时钟档位选择 |
A2DP_RX_AND_AUDIO_DELAY | 宏 | 1 | 使能接收延迟策略,为抗抖动/PLC 预留处理窗口 |
ESCO_DRC_EN | 宏 | 0 | 通话下行限幅器(依赖 TCFG_DRC_ENABLE 与 TCFG_PHONE_EQ_ENABLE),默认关闭 |
TCFG_BT_MUSIC_EQ_ENABLE | 配置 | 依项目 | 音乐 EQ 开关;与 EQ_SECTION_MAX >= 10 或 AUDIO_OUT_EFFECT_ENABLE 组合决定 A2DP 时钟档位 |
TCFG_USER_TWS_ENABLE | 配置 | 依项目 | TWS 使能;开启时编译 bt_tws.h 并参与 a2dp_media_get_remain_play_time 统计 |
SYS_VOL_TYPE == VOL_TYPE_DIGITAL | 配置 | 依项目 | 数字音量类型;决定 a2dp_digital_vol_set 是否被调用 |
依据:
audio_dec.c顶部宏定义(L59-L67)、audio_codec_clock.c档位分支(L36-L43)、app_audio.c音量分支(L543-L545)。
API 参考
媒体通道(include_lib/btstack/a2dp_media_codec.h)
| 函数 | 参数 | 返回 | 说明 |
|---|---|---|---|
a2dp_media_get_packet | u8 **frame | int | 取一个原生媒体包 |
a2dp_media_try_get_packet | u8 **frame | int | 非阻塞取包(队列空立即返回) |
a2dp_media_get_remain_buffer_size | — | int | 队列剩余缓冲字节数 |
a2dp_media_get_remain_play_time | u8 include_tws | int | 剩余可播时间(ms),可含 TWS 对端缓存 |
a2dp_media_get_total_data_len | — | int | 队列内数据总长度 |
a2dp_media_get_packet_num | — | int | 队列内包数 |
a2dp_media_clear_packet_before_seqn | u16 seqn_number | int | 丢弃指定序号之前的所有包(重同步用) |
a2dp_media_fetch_packet | int *len, void *prev_packet | void * | 取可解码帧;prev_packet 支持迭代遍历 |
a2dp_media_fetch_packet_and_wait | int *len, void *prev_packet, int msec | void * | 同上,队列空时最多等待 msec 毫秒 |
a2dp_media_free_packet | void *_packet | void | 归还取出的包 |
a2dp_media_channel_exist | — | int | 媒体通道是否已建立 |
a2dp_media_is_clearing_frame | — | int | 当前是否处于清帧状态 |
a2dp_media_get_codec_type | — | int | 当前协商的编解码类型 |
采样率检测(include_lib/media/a2dp_sample_detect.h)
| 函数 | 参数 | 返回 | 说明 |
|---|---|---|---|
a2dp_sample_detect_open | int sample_rate, u32 coding_type | void * | 创建检测句柄 |
a2dp_frame_sample_detect_start | void *hdl, u32 time | int | 帧解码前标记起始时间戳 |
a2dp_frame_sample_detect | void *hdl, void *data, int len, u32 time | int | 帧解码后统计样本数/时间差 |
a2dp_sample_detect_close | void *hdl | void | 释放句柄 |
故障模式、边界情况与并发
丢包与序号跳变
蓝牙射频拥塞或对端调度抖动会导致 RTP 包丢失。解码器通过 seqn_after 检测跳号,missed_num 累加、repair_frames 计数;启用 PLC 时用 repair_pkt 中保存的参考帧做波形预测填补,未启用或修复失败则回退为静音填充(slience_frames)。边界:连续大量丢包时修复效果有限,应同时依赖上层缓冲水位(get_remain_play_time)触发清帧而非无限填补。
序号回绕(sequence wrap-around)
RTP 序号为 16 位,0xFFFF 后回到 0x0000。seqn_after/before 宏通过 s16 有符号差值比较,天然处理回绕;注意:宏有效前提是两次比较的序号跨度小于 32768,队列深度设计必须满足该约束。
流切换/旧数据污染
切换歌曲或对端重启流时,队列中残留旧帧。a2dp_media_clear_packet_before_seqn(seqn) 按新流的起始序号清空旧包,a2dp_media_is_clearing_frame() 供上层查询清帧状态;解码器侧 overrun_seqn 记录溢出参考序号,配合 stream_error 标志进入重同步路径。
并发访问
- 媒体队列由协议栈(生产者)与解码线程(消费者)共享:取包侧用
fetch_lock互斥,preempt支持高优先级抢占(例如切歌立即放弃当前帧)。 sample_detect与syncts句柄仅由解码线程触达,无需额外加锁。- TWS 场景下主从箱各自解码,
a2dp_media_get_remain_play_time(include_tws=1)需要统计对端缓存,由 TWS 链路同步播放时钟(USER_CTRL_TWS_SYNC_SBC_CDM/USER_CTRL_TWS_RESTART_SBC_CDM控制启停)。
时钟漂移
SBC 源时钟与本地晶振存在 ppm 级偏差。a2dp_sample_detect 每帧统计"实际样本数 vs 时间戳差",输出漂移量用于微调播放时钟/audio_codec_clock 档位;长期不校准会导致缓冲逐渐耗尽或溢出,表现为周期性卡顿。
性能与运维要点
- 时钟档位即算力预算:
A2DP_MODE下A2DP_AUDIO_PLC_ENABLE、EQ 段数(≥10)、输出音效都会把档位抬到SYS_64M及以上(audio_codec_clock.c分支)。改功能组合时须同步评估解码线程是否能在帧间隔内完成全部处理。 - 缓冲水位是核心运维指标:
get_remain_buffer_size/get_remain_play_time/get_packet_num可用于日志或上抛 UI(如缓冲提示),用于排查"卡顿是收包不足还是解码超时"。 - PLC 是 CPU 大头:
LFaudio_plc为自研算法,开启后功耗与算力上升;低功耗产品可权衡关闭(A2DP_AUDIO_PLC_ENABLE=0),此时时钟档位可下调。 - 数字音量:
SYS_VOL_TYPE == VOL_TYPE_DIGITAL时音量经a2dp_digital_vol_set在解码链路内生效,避免额外 DSP 通路开销(app_audio.c多处分发点)。
扩展点
- 编解码类型扩展:
a2dp_media_get_codec_type()暴露当前 codec 类型,SDK 的audio_dec.c按类型分发到对应解码器;新增 codec(如 AAC)时在此处增加分支即可接入现有取包/同步/输出框架。 - PLC 算法替换:
A2DP_AUDIO_PLC_ENABLE后调用LFaudio_plc_api.h;若自研算法,只需保持repair_pkt/repair_frames语义并替换实现。 - 同步策略:
AUDIO_CODEC_SUPPORT_SYNC与syncts(audio_syncts.h/bt_audio_timestamp.h)为 TWS/多设备对齐预留钩子;a2dp_media_get_remain_play_time(include_tws)已是多端感知接口。 - 上层控制:
avctp_user.h的用户控制命令(USER_CTRL_*)与sbc_energy_check提供应用层介入点(如能量检测后的特殊处理流程,注释明确"非蓝牙开发不用")。
相关链接
- a2dp_media_codec.h(媒体通道 API)
- a2dp_sample_detect.h(采样率检测)
- audio_dec.c(A2DP 解码器实现)
- audio_codec_clock.c(A2DP_MODE 时钟配置)
- avctp_user.h(AVDTP/用户控制命令)
- btstack_event.h(AVDTP SBC 能力协商事件)
- 兄弟页面:通话音频(HFP/mSBC/CVSD)、AVRCP 控制、TWS 对箱同步、EQ/音效处理