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

发布于:2025-06-06 ⋅ 阅读:(52) ⋅ 点赞:(0)

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 V

logcat | 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 2
  LOG_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_MEDIA
  int32_t custom_usage  = osi_property_get_int32("persist.bluetooth.avrcp_play_usage",1);

  /* 
	  初始化 AAudio 构建器,用于设置播放流的参数。
	  
			SampleRate: 设置采样率(如 44100 Hz)

			Format: 使用 PCM_FLOAT 格式,32bit float

			ChannelCount: 设置声道数(如 2 表示立体声)

			SessionId: 让系统自动分配音频 session id

			Usage: 设置音频用途,决定 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 数据到 AAudio
		    transcodeToPcmFloat(...)是什么?

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