当前位置: 首页 > news >正文

【android bluetooth 协议分析 12】【A2DP详解 2】【开启ble扫描-蓝牙音乐卡顿分析】

1. 背景

实车报 蓝牙音乐卡顿的问题。 找到对应时刻,发现 在播放音乐的同时,在 ble 扫描。今天来分析一下,打开ble 扫描时,播放蓝牙音乐为何会出现蓝牙音乐卡顿现象。

在这里插入图片描述

这是一个非常典型的 蓝牙资源冲突问题:当同时进行 BLE 扫描A2DP 音乐播放 时,会出现音频卡顿。这种现象可以从 蓝牙协议栈的架构层次 来进行系统性分析,找到其根源。

车机是 A2DP Sink,即:

  • 负责接收手机的音频流(A2DP 音频);

2. 车机作为 A2DP Sink 的 BLE 并发卡顿根因:

我们从底层开始剖析:


1. BLE 与 A2DP 控制器资源竞争(物理层无法真正并发)

1. 现象:

A2DP Sink 模式下,车机通过 BR/EDR ACL 连接接收手机音频数据 → 使用 L2CAP → AVDTP → SBC/AAC 解码 → AudioTrack 播放

BLE 使用的是 独立的广播监听机制(advertising channel 37/38/39),但 控制器收发调度只能在 BLE 与 BR/EDR 之间切换

2.根因:

  • 绝大多数蓝牙控制器(尤其是 MTK/Qualcomm/瑞昱)不支持 BLE + BR/EDR 物理并发收发
  • BLE 扫描会强占控制器调度周期;
  • 车机在 BLE 扫描期间,接收 ACL 音频数据(来自手机)变得断续
  • 导致 SBC/AAC 音频帧断裂 → 卡顿。

2. ACL 数据接收窗口缩小 → L2CAP 报文丢失或乱序

1.背景:

车机在作为 A2DP Sink 时通过 ACL 逻辑通道接收音频数据。

BLE 扫描时:

  • 控制器调度窗口会周期性让位于 BLE;

  • 导致 ACL 数据接收延迟,甚至 SCO/Sniff 模式下无法维持正常带宽;

  • 某些控制器或固件(如 Realtek)直接在 BLE 窗口丢弃 ACL 数据。


3. 解码端(sink)缓冲不足 → AudioTrack underrun

1. 机制:

车机端对手机音频解码通常流程如下:

[ACL 收音频包] → [AVDTP 解封装] → [SBC/AAC 解码] → [AudioTrack 播放]

BLE 干扰导致音频包接收出现短时中断,解码层 buffer 耗尽 → AudioTrack 播放缓冲区 underrun → 卡顿或断音


4. 主线程/任务抢占问题(特别是在低端车载 SoC)

1. 情况:

  • BLE 扫描结果在 JNI 层通过 ScanCallback 回调;

  • 如果处理过慢(如 UI 层处理、广播事件)阻塞了主线程;

  • 影响音频链路中的 JNI/native 层数据流转(例如 Audio HAL、AudioTrack 写入);

  • 低端 SoC(A53 @ 1.0GHz)尤为明显 → 系统调度抖动影响播放流程


5. 音频 HAL 或蓝牙堆栈中 pipeline 堵塞

  • 一些车机厂商定制音频 HAL 时采用同步阻塞 I/O;

  • 若上层蓝牙数据接收因 BLE 干扰而延迟,会导致 HAL 层音频流暂停;

  • Android 中的 AudioFlinger 检测到 underrun → 短暂静音。


3.卡顿重现流程图(A2DP Sink + BLE Scan)

[BLE 扫描中][控制器轮询 BLE 信道,暂停 EDR ACL 收包][ACL 音频包接收中断 → 数据缺失][AVDTP 音频帧不完整或丢包][解码器无法持续输出 PCM → buffer 耗尽][AudioTrack underrun][播放卡顿/断音]

4.验证方法建议(面向车载调试)

1. 开启 verbose 蓝牙日志:


# 会在 /data/misc/bluedroid/output_sample.pcm 中保存 pcm 数据
setprop vendor.bluetooth.a2dp_sink.dump "true"setprop log.tag.bt_btif_avrcp_audio_track Vlogcat | grep bt_btif_avrcp_audio_track

2. 同时记录音频 underrun:

adb shell dumpsys media.audio_flinger # 检查 AudioTrack 的 grep Underrun 是否飙升

05-22 10:48:47.839746   798  1560 I AudioFlinger: track(103)  sessionid 537 usage 62: underrun, track state ACTIVE  framesReady(1623) < framesDesired(1768)

3. 控制 BLE 扫描策略:

尝试将 scanInterval, scanWindow 降低,看是否缓解卡顿。


5. aosp 源码分析

1. BtifAvrcpAudioTrackCreate 函数分析

作用:
用于创建一个音频播放轨道(Track),接收从 A2DP 传来的解码 PCM 数据,并通过 AAudio 播放到音频设备(如车机扬声器)。

// system/btif/src/btif_avrcp_audio_track.cc
void* BtifAvrcpAudioTrackCreate(int trackFreq, int bitsPerSample,int channelCount) {// 打印出调用此函数时传入的音频参数,包括采样率、位深、声道数。// btCreateTrack freq 44100 bps 16 channel 2LOG_INFO("%s Track.cpp: btCreateTrack freq %d bps %d channel %d ",__func__, trackFreq, bitsPerSample, channelCount);AAudioStreamBuilder* builder; // builder 是构造流用的构建器(Builder)AAudioStream* stream; // stream 是最终的音频流对象。//default is USAGE_MEDIAint32_t custom_usage  = osi_property_get_int32("persist.bluetooth.avrcp_play_usage",1);/* 初始化 AAudio 构建器,用于设置播放流的参数。SampleRate: 设置采样率(如 44100 Hz)Format: 使用 PCM_FLOAT 格式,32bit floatChannelCount: 设置声道数(如 2 表示立体声)SessionId: 让系统自动分配音频 session idUsage: 设置音频用途,决定 AudioFocus、输出设备等策略*/aaudio_result_t result = AAudio_createStreamBuilder(&builder);AAudioStreamBuilder_setSampleRate(builder, trackFreq);AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT);AAudioStreamBuilder_setChannelCount(builder, channelCount);AAudioStreamBuilder_setSessionId(builder, AAUDIO_SESSION_ID_ALLOCATE);AAudioStreamBuilder_setUsage(builder, custom_usage);/*设置性能模式(关键点)根据位深设置性能模式:如果是 24bit 及以上,说明可能是高保真音频(如 aptX HD),用 HD_APTX 模式(假设系统自定义了此模式常量)否则使用低延迟播放(常用于语音、响应快的场景)*/aaudio_performance_mode_t mode = (bitsPerSample >= 24) ?AAUDIO_PERFORMANCE_MODE_HD_APTX : AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;LOG_INFO("%s: mode:%d  custom_usage %d", __func__, mode, custom_usage);AAudioStreamBuilder_setPerformanceMode(builder, mode);/*打开流并验证成功:*/result = AAudioStreamBuilder_openStream(builder, &stream); // 尝试打开流CHECK(result == AAUDIO_OK); // CHECK() 宏用于强制断言成功,否则崩溃(调试时重要)AAudioStreamBuilder_delete(builder);/*构建自定义的 Track 封装结构体- 这是一个自定义结构,封装了 stream 和其他相关属性(类似 Android 中 AudioTrack 的封装)- 这里申请堆内存,最终通过 void* 返回给调用方使用(注意释放时要配对)*/BtifAvrcpAudioTrack* trackHolder = new BtifAvrcpAudioTrack;CHECK(trackHolder != NULL);/*初始化结构体字段stream:保存打开的音频流指针bitsPerSample:位深(例如 16/24)bufferLength:根据声道数和帧数计算每帧数据量buffer:分配一个 float 类型的播放 buffer,用于 PCM 数据中转*/trackHolder->stream = stream;trackHolder->bitsPerSample = bitsPerSample;trackHolder->channelCount = channelCount;trackHolder->bufferLength =trackHolder->channelCount * AAudioStream_getBufferSizeInFrames(stream);trackHolder->gain = kMaxTrackGain;trackHolder->buffer = new float[trackHolder->bufferLength]();/*PCM 数据调试保存(可选)如果编译时定义了 DUMP_PCM_DATA == TRUE,则打开 PCM 数据输出文件,用于调试(车厂分析音质、丢帧时常用)*/
#if (DUMP_PCM_DATA == TRUE)openPcmSampleFile();
#endif// 返回封装好的音频轨对象指针,供后续写入 PCM 数据使用(见 BtifAvrcpAudioTrackWrite 函数)return (void*)trackHolder;
}
阶段说明
参数初始化日志、读取系统属性(播放用途)
创建 Builder创建音频构建器,并配置基本参数
设置性能模式根据位深设置高保真或低延迟
打开流调用系统 API 打开实际音频通路
结构封装构建结构体封装流及缓冲区
返回 Track返回封装指针供上层写入 PCM
  • 车机作为 A2DP Sink 播放蓝牙音乐,这个函数就是其创建播放通路的核心之一

  • 使用 AAudio 是 Android 8.0+ 的高性能音频接口,相比 OpenSL ES 延迟更低、可靠性更高

  • 可以通过设置 persist.bluetooth.avrcp_play_usage 动态控制播放通路的行为(媒体 vs 导航)

2. BtifAvrcpAudioTrackWriteData

它是 蓝牙 A2DP Sink 模式下音频播放的核心 PCM 写入函数,负责将解码后的音频数据送入 AAudio 播放通道。

/*handle:由 BtifAvrcpAudioTrackCreate() 返回的指针,表示当前音频流的封装对象audioBuffer:音频数据原始缓冲区,类型通常是 uint8_t*bufferLength:缓冲区大小,单位是字节
*/int BtifAvrcpAudioTrackWriteData(void* handle, void* audioBuffer,int bufferLength) {// 将 void* 转换为具体类型 BtifAvrcpAudioTrack*BtifAvrcpAudioTrack* trackHolder = static_cast<BtifAvrcpAudioTrack*>(handle);CHECK(trackHolder != NULL);CHECK(trackHolder->stream != NULL);// 初始化状态aaudio_result_t retval = -1; // 用于保存 AAudioStream_write() 返回值, 默认为失败状态/*可选:PCM 数据调试保存如果开启 DUMP_PCM_DATA 编译宏,将原始输入数据写入文件车厂常用于 音质调试、卡顿分析、声音畸变排查*/
#if (DUMP_PCM_DATA == TRUE)writePcmSampleFile(audioBuffer, bufferLength);
#endif/*计算单个样本大小(字节)根据 trackHolder->bitsPerSample 返回每个样本的大小(例如 16bit 就是 2 字节)*/size_t sampleSize = sampleSizeFor(trackHolder);/*初始化计数变量记录已处理的总字节数(用于追踪写入进度)*/int transcodedCount = 0;do {/*主循环:写入 PCM 数据到 AAudiotranscodeToPcmFloat(...)是什么?- 将原始 PCM(int16、int24 等)数据转换为 float 格式([-1.0, 1.0])- 转换后的数据写入 trackHolder->buffer 中- 返回转换了多少字节例如:- 输入为 int16_t:0x0000 ~ 0x7FFF → 0.0 ~ 1.0f- 输入为 int24_t:需拼接后做有符号扩展再转 float*/transcodedCount +=transcodeToPcmFloat(((uint8_t*)audioBuffer) + transcodedCount,bufferLength - transcodedCount, trackHolder);/*写入 AAudio 播放器- stream:目标音频流- buffer:float 格式 PCM 数据- frameCount:- 要写入的帧数 = 字节数 / (每样本大小 × 声道数)- 注意 AAudio 写入单位是“帧”(frame),非字节- timeout:阻塞写入时的超时(kTimeoutNanos 是一个预定义的纳秒值)*/retval = AAudioStream_write(trackHolder->stream, trackHolder->buffer,transcodedCount / (sampleSize * trackHolder->channelCount),kTimeoutNanos);LOG_VERBOSE("%s Track.cpp: btWriteData len = %d ret = %d", __func__,bufferLength, retval);} while (transcodedCount < bufferLength); // 如果还没处理完,就继续循环转换并写入,确保整个 audioBuffer 被完全写入// 返回写入总字节数return transcodedCount;
}

audioBuffer (原始PCM)↓ transcodeToPcmFloat()
trackHolder->buffer (float PCM)↓
AAudioStream_write()↓
系统音频播放

在车机 A2DP Sink 中的作用:

  • 每次从 AVDTP media 解码出的 PCM 数据,就通过此函数送入硬件播放
  • BLE 扫描时造成卡顿,大概率是:
    • 系统调度资源冲突,AAudio 写入阻塞(如 AAudioStream_write() 卡顿)
    • 音频线程调度变慢,或播放 buffer 填充不及时导致断续

6.车机 A2DP Sink 并发 BLE 扫描优化建议

层级优化建议
控制器若芯片支持,开启 BLE + EDR concurrency(部分 CSR/QCA 芯片支持)
BLE 策略使用 low duty cycle 扫描:scanInterval=2000ms, scanWindow=50ms;避免长时间连续扫描
蓝牙堆栈降低 BLE 回调频率;处理放在子线程
Audio HAL使用大 buffer size + underrun 保护逻辑
AOSP 层音频配置提高 a2dp_sink_buffer_size, buffer_count,如:256KB>4 buffer
控制器固件升级蓝牙控制器 firmware(部分 vendor 固件会对 BLE 扫描做优先级误判)

7. 总结:车机作为 A2DP Sink 时卡顿的根因关键词

分类原因
PHY 资源抢占BLE 与 A2DP 共享控制器,不能并发通信
ACL 数据接收延迟控制器丢失或延迟接收手机推送的音频帧
解码器 buffer 耗尽SBC/AAC 解码失败或播放 pipeline 停顿
AudioTrack underrun无法及时提供音频帧
BLE 回调干扰主线程BLE 结果处理阻塞音频相关线程
http://www.xdnf.cn/news/867457.html

相关文章:

  • 光伏防逆流控制方案
  • .NET Core接口IServiceProvider
  • Spring Boot MVC自动配置与Web应用开发详解
  • Asp.net Core 通过依赖注入的方式获取用户
  • 全志A40i android7.1 调试信息打印串口由uart0改为uart3
  • 六种高阶微分方程的特解(原创:daode3056)
  • Java观察者模式深度解析:构建松耦合事件驱动系统的艺术
  • NC28 最小覆盖子串【牛客网】
  • 基于Axure+墨刀设计的电梯管理系统云台ERP的中保真原型图
  • Apache APISIX
  • CMake入门:3、变量操作 set 和 list
  • 深度学习项目之RT-DETR训练自己数据集
  • 通过模型文件估算模型参数量大小
  • Flask框架详解:轻量高效的Python Web开发利器
  • 深入解析Oracle SQL调优健康检查工具(SQLHC):从原理到实战优化
  • intense-rp-api开源程序是一个具有直观可视化界面的 API,可以将 DeepSeek 非正式地集成到 SillyTavern 中
  • Windows系统工具:WinToolsPlus 之 SQL Server Suspect/质疑/置疑/可疑/单用户等 修复
  • stress 服务器压力测试的工具学习
  • linux操作系统---网络协议
  • LeetCode 3370.仅含置位位的最小整数
  • 二维 根据矩阵变换计算镜像旋转角度
  • 短剧+小说网盘搜索系统(支持全网网盘转存拉新)
  • 《T/CI 404-2024 医疗大数据智能采集及管理技术规范》全面解读与实施分析
  • [ Qt ] | 与系统相关的操作(二):键盘、定时器、窗口移动和大小
  • 虚拟机CentOS 7 网络连接显示“以太网(ens33,被拔出)“、有线已拔出、CentOS7不显示网络图标
  • 【Unity】R3 CSharp 响应式编程 - 使用篇(集合)(三)
  • Async-profiler 内存采样机制解析:从原理到实现
  • Elasticsearch中什么是分析器(Analyzer)?它由哪些组件组成?
  • 2025年- H68-Lc176--46.全排列(回溯,组合)--Java版
  • 通光散基因组-文献精读139