AudioReach Plugin 机制深度解析:从 AudioHAL 到 DSP 的数据之路
主题: tinyalsa PCM plugin、AGM、ALSA/ASoC bypass 机制
源码路径:
platform/external/tinyalsa_new/,platform/vendor/qcom/opensource/audioreach/
在 Qualcomm AudioReach 音频架构中,tinyalsa 的 PCM plugin 机制扮演了核心角色。它让 AudioHAL 可以通过统一的 PCM API 与 AGM(Audio Graph Manager)交互,而底层数据最终流向 DSP 完成信号处理。
很多开发者初学时会困惑几个问题:plugin 会不会调用 hw PCM?多路 Stream 同时播放时如何区分?数据究竟怎样从 AudioHAL 一步步到达 DSP?
本文基于源码逐一解答。
一、libagm_pcm_plugin.so 会调用 hw PCM 设备吗?
结论:不会。在标准 AudioReach 构建下,ALSA/ASoC 内核驱动被完全绕过,plugin 直接与 DSP 交互。
1.1 plugin operations 不涉及任何 /dev/snd/*
libagm_pcm_plugin.so 的源码位于 audioreach-graphmgr/plugins/tinyalsa/src/agm_pcm_plugin.c。我们逐一检查其关键操作:
open --- 不打开设备节点,只创建 AGM Session
c
// agm_pcm_plugin.c:923-1024
int agm_pcm_open(struct pcm_plugin **plugin, unsigned int card,
unsigned int device, unsigned int mode)
{
// 从 card-defs.xml 读取虚拟 PCM 定义,不碰 /dev/snd/*
pcm_node = snd_card_def_get_node(card_node, device, SND_NODE_TYPE_PCM);
// 通过 IPC 向 AGM Service 请求创建 Session
session_id = device;
ret = agm_session_open(session_id, sess_mode, &handle);
}
write / read --- 数据走 AGM IPC,不走内核
c
// agm_pcm_plugin.c:568-591
static int agm_pcm_writei_frames(struct pcm_plugin *plugin, struct snd_xferi *x)
{
// 数据直接发给 AGM Service,不经过任何 hw fd
ret = agm_session_write(handle, buff, &count);
}
mmap --- 映射 GSL 共享内存,非内核 ring buffer
c
// agm_pcm_plugin.c:811-884
static void *agm_pcm_mmap(struct pcm_plugin *plugin, ...)
{
// 从 AGM 获取 dma-buf fd(由 GSL/SPF 管理)
agm_session_get_buf_info(priv->session_id, buf_info, flag);
// buf_info->data_buf_fd 和 buf_info->pos_buf_fd 来自 DSP 侧共享内存
mmap(NULL, size, PROT_WRITE | PROT_READ, MAP_SHARED, buf_info->data_buf_fd, 0);
}
ioctl --- 仅处理 RESET,其余静默忽略
c
// agm_pcm_plugin.c:902-921
static int agm_pcm_ioctl(struct pcm_plugin *plugin, int cmd, void *arg)
{
switch (cmd) {
case SNDRV_PCM_IOCTL_RESET:
ret = agm_pcm_plugin_reset(plugin);
break;
default:
// 其他 ioctl 全部忽略,不转发给 hw 设备
break;
}
}
1.2 AGM Service 中的 ALSA 代码也被编译掉了
AGM service 的 device.c 中确实存在 snd_pcm_open("hw:X,Y") 等代码,但在标准构建下被 BYPASS_ALSA_HW 宏切除:
makefile
# audioreach-graphmgr/service/Android.mk:62
ifeq ($(ENABLE_HYP), true)
LOCAL_CFLAGS += -DBYPASS_ALSA_HW
c
// audioreach-graphmgr/service/src/device.c:249-280
int device_open(struct dev_obj *obj)
{
#ifndef BYPASS_ALSA_HW
snd_pcm_open(&pcm, "hw:%u,%u", obj->card_id, obj->pcm_id, ...);
snd_pcm_hw_params(pcm, hwparams);
// ...
#else
// 仅设置 obj->state = DEV_OPENED,不碰内核
#endif
obj->state = DEV_OPENED;
}
device_prepare()、device_stop()、device_close() 同样被 #ifndef BYPASS_ALSA_HW 包裹。
1.3 AGM 读取 /proc/asound/pcm 的唯一目的
AGM 会 fopen("/proc/asound/pcm") 枚举后端 AIF 设备,但仅获取名称、card ID、pcm ID 等元数据 ,用于向 DSP 配置 graph 时使用,绝不用于打开设备进行数据传输。
1.4 本质:虚拟 PCM 被 Plugin 旁路
总结来说,AudioReach 通过 plugin 机制实现了一个精妙的旁路:
- 上层(PAL / AudioHAL)看到的是一个标准的 PCM 设备:
card=100, device=100,对应/dev/snd/pcmC100D100p - 该设备节点在内核中不存在 ,
hw_ops.open()必然失败 - tinyalsa 自动回退到
plug_ops,加载libagm_pcm_plugin.so - 后续所有 PCM 操作(write、mmap、ioctl、poll)全部在用户态完成,不进入内核
对 PAL 而言,它只是在调用 pcm_open()、pcm_write()、pcm_mmap_write() ------ plugin 机制对它完全透明。
二、多路 Stream 同时打开虚拟 PCM,如何区分和管理?
2.1 核心机制:不同 device ID → 独立 AGM Session
Card-defs.xml 中,虚拟卡(card 100)定义了多个 PCM 设备:
| Device ID | 名称 | 方向 | 用途 |
|---|---|---|---|
| 100 | PCM100 | playback | deep_buffer(音乐等) |
| 101 | PCM101 | capture | primary capture |
| 102 | PCM102 | playback | low-latency(按键音等) |
| 103 | PCM103 | playback | hostless playback |
| 106 | VOICEMMODE1p | playback | 语音 modem 1 |
| 107 | VOICEMMODE2p | playback | 语音 modem 2 |
| 108 | VOICEMMODE1c | capture | 语音 modem 1 |
AudioHAL 根据 usecase 选择对应 device ID:
c
// hal/msm8974/platform.c:369
static int pcm_device_table[AUDIO_USECASE_MAX][2] = {
[USECASE_AUDIO_PLAYBACK_DEEP_BUFFER] = {DEEP_BUFFER_PCM_DEVICE, ...},
[USECASE_AUDIO_PLAYBACK_LOW_LATENCY] = {LOWLATENCY_PCM_DEVICE, ...},
// ...
};
2.2 并发场景:音乐 + 按键音
当 deep_out(音乐)和 fast_out(按键音)同时播放时:
scss
deep_out (音乐)
└─ pcm_open(card=100, device=100)
└─ agm_session_open(session_id=100, ...) → AGM Session A
└─ DSP Graph A(含 format convert → mix 输入)
fast_out (按键音)
└─ pcm_open(card=100, device=102)
└─ agm_session_open(session_id=102, ...) → AGM Session B
└─ DSP Graph B(含 format convert → mix 输入)
↓
DSP Graph Mixer 汇合
↓
AIF → Codec → 扬声器
每个 Session 是独立的:
- 拥有自己的 graph 子树(DSP 侧的 signal processing graph)
- 拥有自己的 buffer 配置(period size、channels、format 等)
- 拥有自己的 AGM 状态机(OPEN → SETUP → PREPARED → RUNNING)
DSP 侧的 Hexagon 调度器(SLPI/CDSP)负责多个并发 graph 模块的调度和执行。
三、数据从 AudioHAL 到 DSP 的完整路径
3.1 完整数据流
scss
┌──────────────────────────────────────────────────────────────┐
│ 1. AudioFramework → AudioHAL (HAL-ABL) │
│ PAL::SessionAlsaPcm::open() │
│ pcm_open(rm->getVirtualSndCard(), pcmDevId, │
│ PCM_OUT | PCM_MMAP | PCM_NOIRQ, &config) │
│ ── card=100 为虚拟卡,内核中不存在 ────────────── │
└──────────────┬───────────────────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────────────────┐
│ 2. Tinyalsa PCM Open Dispatch (pcm.c:pcm_open) │
│ ├─ hw_ops.open() → 失败(/dev/snd/pcmC100D100p 不存在) │
│ └─ plug_ops.open() │
│ ├─ dlopen("libsndcardparser.so") │
│ │ 解析 card-defs.xml → so_name="libagm_pcm_plugin.so" │
│ ├─ dlopen("libagm_pcm_plugin.so") │
│ │ dlsym("pcm_plugin_ops") 获取 vtable │
│ └─ agm_pcm_open() → agm_session_open() │
│ ── 通过 AIDL Binder IPC 连接到 AGM Service ── │
│ ── AGM 创建 Session + DSP Graph + 共享内存 ── │
└──────────────┬───────────────────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────────────────┐
│ 3. 数据写入 (MMAP/NOIRQ 模式) │
│ HAL 填充数据 → pcm_mmap_write() │
│ → plug_ops.mmap_write() │
│ → 数据写入已 mmap 的共享缓冲区 │
│ → agm_session_write() → Binder IPC → AGM Service │
│ → gsl_write() → 数据进入 DSP 侧共享缓冲区 │
└──────────────┬───────────────────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────────────────┐
│ 4. DSP 侧 (Hexagon ADSP) │
│ SPF Framework 执行 Signal Processing Graph: │
│ ├─ Format Conversion │
│ ├─ Mixing(多路 Stream 汇合) │
│ ├─ Effects / Voice Processing │
│ └─ AIF (Audio Interface) → 硬件 DMA │
│ ── DSP 直接驱动 SLIMbus/MI2S/TDM,不经 CPU ASoC ─ │
└──────────────┬───────────────────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────────────────┐
│ 5. Position Feedback │
│ DSP 写 hw_ptr 到共享位置缓冲区 (pos_buf_fd) │
│ → agm_pcm_plugin_update_hw_ptr() 读取共享内存 │
│ → HAL 获取当前位置,计算 avail / underrun │
└──────────────────────────────────────────────────────────────┘
3.2 与传统 ALSA/ASoC 路径的对比
| 维度 | 传统 Linux 音频 | AudioReach Plugin 路径 |
|---|---|---|
| 数据路径 | user → ALSA → ASoC DMA → Codec | user → plugin → AGM → GSL → DSP SPF → AIF → Codec |
| PCM 设备 | /dev/snd/pcmCxDx 字符设备 |
card 100 虚拟卡,无内核节点 |
| 内核驱动 | ASoC codec + platform + machine driver | 不参与数据传输(BYPASS_ALSA_HW) |
| 混音位置 | CPU 侧(alsa-lib plug / Android mixer) | DSP 侧(SPF graph mixer module) |
| 数据拷贝 | user → kernel DMA buffer | user → dma-buf → DSP shared memory |
| 后端驱动 | CPU 侧 ASoC DAI 驱动 | DSP 硬件控制器直接驱动 |
3.3 关于 DMA 的常见误解
AudioReach 并不是"不需要 DMA",而是把 DMA 从 CPU 侧转移到了 DSP 侧:
| 传统 ALSA/ASoC | AudioReach | |
|---|---|---|
| DMA 执行者 | CPU 侧 ASoC DAI driver | DSP 硬件控制器直接驱动 |
| DMA buffer | 内核 ring buffer(snd_pcm_lib_alloc_pages) |
GSL shared memory(dma-buf / ion) |
| CPU 参与 | CPU DMA engine 搬运数据 | CPU 只写共享内存,后续不参与 |
| 后端接口 | CPU DAI → Codec | DSP AIF → Codec(SLIMbus/MI2S/TDM) |
对 CPU 而言,确实"不需要 DMA"了------pcm_mmap_write() 把数据写入共享内存后就可以休眠(NOIRQ 模式),后续的数据搬运、混音、编码、后端传输全部由 DSP 独立完成。
3.4 Stream Open 的关键观察
播放音乐(wav/mp3)时,上层拿到的 PCM 设备并非真实硬件,而是 PAL 根据 use case 返回的虚拟 PCM:
cpp
// audioreach-pal/session/src/SessionAlsaPcm.cpp:1003
pcm = pcm_open(rm->getVirtualSndCard(), pcmDevIds.at(0), PCM_OUT|PCM_MMAP|PCM_NOIRQ, &config);
rm->getVirtualSndCard()固定返回 card 100(虚拟卡)pcmDevIds由 ResourceManager 根据 use case(deep_buffer / low_latency / voice)分配对应 device ID- Android Framework 调用
openOutputStream()→ HAL-ABL → PAL →pcm_open(card=100, device=N) - 返回的
struct pcm*完全不知道自己操作的是虚拟设备,plugin 机制对上层透明
四、关键源码索引
| 组件 | 文件路径 |
|---|---|
| tinyalsa pcm_open 分发 | external/tinyalsa_new/src/pcm.c:1050-1133 |
| tinyalsa hw_ops | external/tinyalsa_new/src/pcm_hw.c |
| tinyalsa plug_ops | external/tinyalsa_new/src/pcm_plugin.c |
| snd_card_parser | external/tinyalsa_new/src/snd_card_plugin.c |
| libsndcardparser.so 源码 | audioreach-graphmgr/snd_parser/src/snd-card-parser.c |
| AGM PCM plugin | audioreach-graphmgr/plugins/tinyalsa/src/agm_pcm_plugin.c |
| AGM service 主入口 | audioreach-graphmgr/service/src/agm.c |
| AGM device 层(含 BYPASS_ALSA_HW) | audioreach-graphmgr/service/src/device.c |
| AGM graph 层(GSL 交互) | audioreach-graphmgr/service/src/graph.c |
| AGM IPC client (AIDL Binder) | audioreach-graphmgr/ipc/aidl/client/AgmClientWrapper.cpp |
| PAL PCM open | audioreach-pal/session/src/SessionAlsaPcm.cpp:1003 |
| 虚拟卡 XML 配置 | audioreach-graphmgr/snd_parser/configs/sdxpinn/card-defs.xml |
| AGM Android.mk(BYPASS_ALSA_HW) | audioreach-graphmgr/service/Android.mk:62 |