【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 结果处理阻塞音频相关线程 |