AudioReach Plugin 机制问题解答

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
相关推荐
大辉狼_音频架构6 小时前
AudioReach:Tinyalsa PCM Plugin 机制
嵌入式·音视频开发
老孙讲技术9 小时前
凌晨三点报警全乱了:一个 callback 地址,如何把设备托管消息拆成多客户流水线?
物联网·音视频开发
feasibility.13 小时前
从数字智能到物理智能体:深度解构具身智能时代的边缘计算、嵌入式实时系统与多形态智能体
人工智能·机器人·自动驾驶·嵌入式·无人机·具身智能·智能体
ajassi20001 天前
AI语音智能体开发日记(一)如何为“小智”服务器启用并调试 License 功能
笔记·ai·嵌入式·ai编程
ARM+FPGA+AI工业主板定制专家2 天前
国产化RK3576+FPGA架构|晶圆传输机器人高速定位+AI瑕疵检测一体化方案
fpga开发·架构·机器人·嵌入式·fpga·工控·机器人运控
Discipline~Hai2 天前
ARM03-蜂鸣器和中断
c语言·arm开发·单片机·嵌入式硬件·嵌入式
一杯原谅绿茶3 天前
安卓烧录工具PhonixCard-4.2.8下载
嵌入式
青衫嵌入式3 天前
FreeRTOS中断优先级配置不对也会HardFault?这次用一个血的教训讲明白
嵌入式
零涂毕业设计4 天前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled