Android AudioTrack 在车载平台上的原理、问题与调试

> 适用平台:Android Automotive OS (AAOS)

> 适用版本:Android 14 / 15 / 16

> 注:Android 17 源码尚未公开,暂不涉及

一、AudioTrack 在车载音频架构中的位置

1.1 整体架构

车载 Android 的音频路径比普通 Android 复杂得多,新增了 CarAudioService 作为中间层:


┌─────────────────────────────────────────────────────────────┐
│                        应用层 (App)                         │
│              AudioTrack.write() / AAudio                   │
└────────────────────────────┬────────────────────────────────┘
                             │  JNI
┌────────────────────────────▼────────────────────────────────┐
│                 Native AudioTrack (C++)                    │
│            frameworks/av/media/libmedia/                   │
└────────────────────────────┬────────────────────────────────┘
                             │  Binder IPC
┌────────────────────────────▼────────────────────────────────┐
│                    CarAudioService                          │
│         (CarAudioManager / AudioZone / AudioFocus)         │
│              frameworks/av/services/car/                     │
└────────────────────────────┬────────────────────────────────┘
                             │
┌────────────────────────────▼────────────────────────────────┐
│                     AudioFlinger                            │
│              (PlaybackThread / MixerThread)                │
└────────────────────────────┬────────────────────────────────┘
                             │
┌────────────────────────────▼────────────────────────────────┐
│                      CarAudioHAL                            │
│          (车载功放 / DSP / 多区域路由管理)                   │
└────────────────────────────┬────────────────────────────────┘
                             │
                    车载硬件(扬声器 / 功放 / DSP)

关键区别

  • 普通 Android:App → AudioFlinger → HAL → 硬件
  • 车载 Android:App → CarAudioService → AudioFlinger → CarAudioHAL → 硬件

1.2 共享内存机制(与普通 Android 相同)

AudioTrack 与 AudioFlinger 之间通过 mmap 共享内存传输数据,不拷贝:


// audio_track_cblk_t — 核心控制结构
struct audio_track_cblk_t {
    volatile uint32_t server;    // AudioFlinger 已消费到的帧位置
    volatile uint32_t user;      // App 已写入的帧位置
    uint32_t bufferEnd;          // 缓冲区末尾帧索引
    void*    buffers;           // 共享内存数据区
};

App 写数据时更新 user,AudioFlinger 消费时更新 serveruser - server 即为缓冲区中尚未被消费的数据量。

二、车载多区域音频(Audio Zone)

2.1 为什么需要分区

车是一个多人共处的空间,不同座位的乘客需要独立的音频控制:


├── 驾驶座区域(Driver Zone, Zone 0)
│   ├── 扬声器:通常位于前门 + 仪表台中央
│   ├── 典型用途:导航播报、仪表盘提示音
│   └── 特点:安全相关优先级最高
│
├── 副驾区域(Front Passenger Zone, Zone 1)
│   ├── 扬声器:前门
│   └── 典型用途:副驾娱乐
│
├── 后排左侧 / 右侧(rear left / right)
│   └── 各有独立音量控制
│
└── 中央通道(Center Channel)
    └── 导航语音助手、ADAS 警示音专属

2.2 声音类型与优先级

车机上同时存在多个音频流竞争播放,AudioFlinger 的 Mixer 按以下优先级混合:

优先级 声音类型 说明

|——–|———|——|

**最高** ADAS 警示音(碰撞预警、车道偏离) 必须立即抢占,FLAG_AUDIBILITY_ENFORCED
**高** 导航播报 次高优先级
**高** 免提电话(下行) 通话时压制媒体
**中** 语音助手 / 唤醒词 需要持续监听
**低** 媒体音乐 可被高优先级声音打断

// 正确的车载 AudioTrack 创建方式
AudioAttributes attrs = new AudioAttributes.Builder()
    .setUsage(AudioUsage.Voice)          // 语音类用途
    .setSource(AudioSource.VoiceCall)    // 来源是通话
    .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) // 安全警示音必加
    .build();

AudioTrack track = new AudioTrack.Builder()
    .setAudioAttributes(attrs)
    .setAudioZoneId(0)                   // 绑定到驾驶区
    .setBufferSizeInBytes(8192)
    .build();

三、缓冲区管理与 Underrun / Overrun

3.1 什么是 Underrun(欠载)

Underrun 发生在 AudioFlinger 消费数据的速度大于 App 生产数据的速度时:


时间轴 ──────────────────────────────────────────►

时刻A: [##############........]  75% 满,正常播放
时刻B: [--------------------]  0% 空,AudioFlinger 无数据 → 静音
                                      ↑
                                  这里产生静音
                                  恢复瞬间 → Pop 音

根本原因

1. App 层 write() 太慢(主线程被 GC、CPU 抢占等)

2. 缓冲区设置太小

3. AudioFlinger 的 PlaybackThread 被高优先级线程抢占

4. HAL 层缓冲过大,系统整体”生产-消费”节奏被打乱

3.2 什么是 Overrun(过载)

Overrun 与 underrun 相反,App 写入速度过快,write() 长时间阻塞在缓冲区满的状态:


adb logcat | grep "AudioTrack"
# 看到 "write() blocked for > 50ms" 这类 log,说明正在 Overrun

3.3 Pop 音(爆破音)的来源

来源 触发场景 根因

|——|———|——|

**Underrun 恢复** 缓冲区清空后第一次恢复播放 共享内存未初始化 / DAC 没有 fade-in
**HAL 第一帧** 每次播放线程启动时 HAL 层驱动没有在第一帧做清零
**打断-恢复** 导航播报打断 → 恢复媒体 恢复瞬间缓冲区状态与停止前不一致
**启停(Start-Stop)** 发动机启停系统熄火/重启时 DAC 电源序列不对,I2S 重新初始化
**采样率切换** 从 44.1kHz 切换到 48kHz 重采样器初始阶段输出突变

Android 16 对启停场景做了修复,要求 HAL 实现必须做 fade-in/fade-out,避免电源波动导致的 Pop。

3.4 缓冲区大小参考

以 48kHz 双声道 16bit PCM 为例:

bufferSizeInBytes 帧数 缓冲时长 适用场景

|——————|——|———|———|

2048 512 帧 ~10ms 超低延迟,但 underrun 风险高
4096 1024 帧 ~21ms 低延迟场景
8192 2048 帧 ~42ms 平衡之选,适合大多数场景
16384 4096 帧 ~85ms 高稳定性,延迟不敏感场景

四、车载音频延迟分析

4.1 端到端延迟构成


总延迟 = T_app + T_buffer + T_mixer + T_hal + T_dac

阶段 说明 典型延迟

|——|——|———|

**T_app** App 的 write() 调用返回时间 1-5ms
**T_buffer** 数据在 AudioTrack 缓冲区和 Mixer 缓冲区的排队时间 变量,10-100ms
**T_mixer** PlaybackThread 混音/重采样 5-20ms

| T_hal | CarAudioHAL 内部缓冲 | 5-30ms(取决于厂商实现)|

**T_dac** I2S/PCM 到扬声器 1-5ms

T_buffer 是最大的不确定因素,也是车载音频延迟的主要瓶颈。

4.2 车载场景的延迟要求

场景 可接受延迟 说明

|——|———–|——|

**普通音乐播放** 50-150ms 体验不受影响
**导航播报** < 100ms 再高会有明显滞后感
**卡拉OK / 实时监听** < 30ms 人耳能感知 20ms 以上的滞后
**ADAS 警示音** < 50ms 安全相关,但不需要极低延迟

4.3 降低延迟的方法

#### 方法一:使用低延迟标志


AudioAttributes attrs = new AudioAttributes.Builder()
    .setUsage(AudioUsage.Game)  // 或 Greeting / Sonification
    .setFlags(AudioAttributes.FLAG_LOW_LATENCY)
    .build();

FLAG_LOW_LATENCY 会让 CarAudioService 请求 AudioFlinger 使用更小的缓冲区。但实际效果取决于 HAL 厂商实现,很多车载 HAL 内部缓冲区无法进一步缩小。

#### 方法二:绕过 Mixer(直连 HAL,Android 15+)

Android 15 引入了直连 HAL 路径,绕过 MixerThread,减少调度开销:


// 请求独占输出路径
track.setPreferredAudioClientUid(Process.SYSTEM_UID);

#### 方法三:使用 AAudio

AAudio 是比 AudioTrack 更轻量的 API,设计目标就是低延迟:


AAudioStreamBuilder builder;
AAudioStream *stream;
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_openStream(builder, &stream);

#### 方法四:硬件选型

方案 延迟 说明

|——|——|——|

蓝牙 A2DP 100-200ms **不适合卡拉OK / 实时监听**
3.5mm 模拟输出 中等 内置 Codec 通常有额外缓冲
USB 声卡 推荐用于卡拉OK等实时场景
车载 DSP 直解 极低 代价是架构复杂,需厂商配合

五、车载特有的问题场景

5.1 导航播报打断媒体 → Pop 音

场景:车主正在听音乐,导航说”前方500米右转”——导航播报抢占焦点后,媒体恢复播放时出现 Pop 音。

时序分析


T1: 导航 AudioTrack 抢占焦点 → 媒体 track 被强制 pause()
T2: 导航播报结束 → 媒体 track 恢复 play()
T3: 缓冲区残留 T1 时刻的旧数据 + HAL 可能已重新初始化
    ↓
    Pop 音出现

调试方法


adb shell dumpsys media.audio_flinger | grep -A 20 "Media"
# 看 underrun 计数是否在导航打断后飙升

adb logcat | grep -E "CarAudioService|AudioFocus|Navigation"

5.2 免提通话与媒体混音问题

场景:通话时音乐应该暂停或降混(ducking),但某些 HAL 实现不正确,导致:

  • 对方听到你车里的音乐声
  • 通话结束后媒体恢复时出现 Pop

根因:Audio Focus 机制和 HAL 的路由策略没有正确联动。

5.3 启停(Start-Stop)造成的 Pop

场景:等红灯时发动机启停系统熄火,然后重新启动——音频系统随之重启,DAC 的 I2S 总线重新初始化,产生 Pop。

修复情况

  • Android 16 已改进了 Suspend/Resume 的保护机制
  • 要求 HAL 实现必须做 fade-in/fade-out
  • 如果仍有问题,大概率是 HAL 驱动层没有按规范实现

5.4 多区域音量控制失效

场景:司机调大导航音量,后排乘客的音乐音量也跟着变,或者反之。

根因:App 的 AudioTrack 没有正确绑定到对应的 AudioZone,导致音量被错误地路由到了全局控制链路上。

正确做法


// 每个区域的 App 应该绑定到对应 Zone
audioZone = CarAudioManager.getAudioZone(CarAudioManager.ZONE_ID_DRIVER);
track = audioZone.createTrack(audioAttributes, config, bufferSize, ...);

六、调试方法

6.1 确认问题边界


第一步:确认是系统问题还是 App 问题
  ↓
用系统自带应用测试(音乐播放器 / 导航)
  系统自带也有问题 → 系统层问题(HAL / CarAudioService)
  只有我的 App 有问题 → App 层问题

6.2 CarAudioService 和 AudioFlinger Dump


# 查看车载音频服务全状态(最重要)
adb shell dumpsys car_audio_service

# 查看 AudioFlinger(PlaybackThread / underrun 计数)
adb shell dumpsys media.audio_flinger

# 输出关键字段解读:
#   Underruns: N       → N > 0 = 发生过 underrun
#   Buffer size: X    → 缓冲区总大小
#   Latency: Y ms     → 端到端延迟
#   Sample rate: Z    → 当前采样率

6.3 Logcat 过滤


# 过滤 AudioTrack 相关
adb logcat | grep -E "AudioTrack|AudioFlinger|PlaybackThread"

# 过滤 underrun / buffer 相关
adb logcat | grep -i "underrun|buffer|pop"

# 过滤 CarAudioService(车载专用)
adb logcat | grep -E "CarAudioService|AudioZone|AudioFocus|CarAudio"

# 过滤 HAL 层(确认问题在哪一层)
adb logcat | grep -E "audio_hw|StreamOut|output_stream_write"

6.4 代码层加 dump

#### 在 App 的 write() 前后加时间戳:


long t0 = System.nanoTime();
int written = track.write(buffer, buffer.length);
long t1 = System.nanoTime();
Log.d("AudioDebug", "write " + written + " bytes, cost=" + (t1-t0)/1_000_000 + "ms");

// 如果 write() 返回耗时 > 20ms,说明 App 层生产速度是瓶颈

#### 获取缓冲区消费进度(Android 14+):


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    AudioTrack.Tracker tracker = track.getTracker();
    // 通过 tracker 监控缓冲区填满程度
}

6.5 HAL 层 PCM dump(定位问题在哪一层)

需要 root 权限,在 HAL 层加 dump(临时修改):


// 修改 hardware/interfaces/audio/core/all-versions/default/StreamOut.cpp
// 在 write() 函数中:
FILE* fp = fopen("/data/vendor/audio_hal_dump.pcm", "ab");
fwrite(buffer, 1, size, fp);
fclose(fp);

判断逻辑

  • HAL dump 数据正确 + App dump 数据正确 → 问题在驱动 / 硬件
  • HAL dump 全是 0 或错误 → 问题在 AudioFlinger 或更上层
  • App dump 本身错误 → App 代码的问题

6.6 systrace 分析


python systrace.py -a com.example.yourapp \
    -o trace.html \
    sched freq idle am wm audio binder_driver

# 在 chrome://tracing 打开 trace 文件
# 关注:
#   - AudioTrack write() 行是否有长时间空白(underrun)
#   - PlaybackThread 是否频繁被高优先级线程抢占
#   - 主线程是否在 write() 上长时间阻塞(CPU 竞争)

6.7 完整调试清单


① 用系统 App 测试,确认是系统还是 App 问题

② adb shell dumpsys car_audio_service
   → 确认 Zone 配置是否正确
   → 确认 Audio Focus 持有者

③ adb shell dumpsys media.audio_flinger
   → 找 Underruns 计数 > 0 的 Track
   → 找 Latency 过大的 Track

④ adb logcat 过滤
   → 搜 underrun / pop / AudioTrack 相关 tag

⑤ 确认 bufferSizeInBytes
   → < 4096 容易 underrun,建议 ≥ 4096

⑥ 确认采样率匹配
   → track.getSampleRate() vs 音频源采样率
   → 不一致时看 log 有无重采样警告

⑦ 抓 HAL dump
   → 对比 Framework 层和 HAL 层的数据是否一致

⑧ systrace
   → 分析线程调度是否抢占了 AudioFlinger

⑨ 测试启停场景
   → 发动机启停后音频是否正常恢复,有无 Pop

⑩ 多 Zone 测试
   → 确认音量控制是否在正确的 Zone 生效

七、总结

维度 说明

|------|------|

**架构** 车载比普通 Android 多 CarAudioService 中间层,负责多 Zone 和 Audio Focus
**延迟** 正常场景 50-150ms 可接受;卡拉OK < 30ms 需要 AAudio + FLAG_LOW_LATENCY + USB 声卡
**Pop 音根因** 最常见的是 underrun 恢复 + HAL 层没有 fade-in,其次是启停场景 HAL 重初始化
**多 Zone** App 必须通过 `setAudioZoneId()` 绑定到正确 Zone,否则音量控制失效
**调试入口** `dumpsys car_audio_service` + `dumpsys media.audio_flinger` + HAL 层 dump 配合使用

如果遇到具体问题,可以按以下格式描述:

1. 出现的具体场景(播放音乐 / 导航播报 / 免提通话 / 启停后)

2. 具体表现(Pop 音 / 无声 / 延迟大 / 某个 Zone 无声)

3. 所有 App 都有问题还是只有你的 App 有问题

4. Android 版本(14 / 15 / 16)

5. 能否提供 adb shell dumpsys car_audio_serviceadb shell dumpsys media.audio_flinger 的完整输出

发表评论