当前位置: 首页 > 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 结果处理阻塞音频相关线程

相关文章:

  • 【知识点】第6章:组合数据类型
  • 时序替换实时?是否必要
  • C++算法训练营 Day7 哈希表及双指针
  • 《汇编语言》第14章 端口——实验14 访问CMOS RAM
  • OpenCV C++ 心形雨动画
  • 灰狼优化算法MATLAB实现,包含种群初始化和29种基准函数测试
  • 从零开始:用Tkinter打造你的第一个Python桌面应用
  • JVMTI 在安卓逆向工程中的应用
  • 解决 WebAssembly 错误:Incorrect response MIME type (Expected ‘application/wasm‘)
  • 【已解决】电脑端 划词时出现腾讯元宝弹窗问题
  • SQL 中 NOT IN 的陷阱?
  • 固定ip和非固定ip的区别是什么?如何固定ip地址
  • AI助力Java开发:减少70%重复编码,实战效能提升解析
  • Python多线程与多进程
  • 那些Java 线程中断的实现方式
  • Git的使用技巧
  • qt的智能指针
  • MuLogin浏览器如何使用Loongproxy?
  • 深入解析 Java ClassLoader:揭开 JVM 动态加载的神秘面纱
  • 海康网络摄像头实时取帧转Opencv数组格式(h,w,3),已实现python、C#
  • 做期货浏览哪些网站/推荐几个靠谱的网站
  • 昌乐网站制作价格/谷歌官网网址
  • 上传的网站打不开/宁波网络推广软件
  • 做网站买Java什么书/网络营销推广策略
  • 做网站哪家强/seo搜索优化是什么
  • 淄博网站建设 优易科技/制作一个网站大概需要多少钱