AudioReach:Tinyalsa PCM Plugin 机制

在 Android 音频栈中,tinyalsa 作为轻量级 ALSA 实现,其 pcm_open() 调用的默认路径是打开 /dev/snd/pcmC*D*p 字符设备。当该设备节点不存在时,tinyalsa 并不直接报错,而是回退到 plugin 路径,从用户态动态库加载虚拟 PCM 设备的实现。本文基于 tinyalsa 源码,完整剖析这一机制的架构与运行原理。

源码路径:https://git.codelinaro.org/clo/la/platform/external/tinyalsa_new/src/

Qcom在 tinyalsa_new 中贡献了Plug-In机制,应用在其开源架构AudioReach上。


一、hw 到 plugin 的回退路由

pcm_open() 的入口逻辑集中在 pcm.c:1050-1133。打开流程分为两阶段:

c 复制代码
// pcm.c:1063-1079
/* Default to hw_ops, attemp plugin open only if hw (/dev/snd/pcm*) open fails */
pcm->ops = &hw_ops;
pcm->fd = pcm->ops->open(card, device, flags, &pcm->data, NULL);

#ifdef TINYALSA_USES_PLUGINS
    if (pcm->fd < 0) {
        int pcm_type;
        pcm->snd_node = snd_utils_open_pcm(card, device);
        pcm_type = snd_utils_get_node_type(pcm->snd_node);
        if (!pcm->snd_node || pcm_type != SND_NODE_TYPE_PLUGIN) {
            oops(&bad_pcm, ENODEV, "no device (hw/plugin) for card(%u), device(%u)",
                 card, device);
            goto fail_close_dev_node;
        }
        pcm->ops = &plug_ops;
        pcm->fd = pcm->ops->open(card, device, flags, &pcm->data, pcm->snd_node);
    }
#endif

回退流程:

  1. 默认绑定 hw_ops,尝试打开 /dev/snd/pcmC*D*p/c(pcm_hw.c:103-144)
  2. 若返回负值,调用 snd_utils_open_pcm() 查询 sound card 配置
  3. snd_utils_open_pcm() 内部执行 dlopen("libsndcardparser.so", RTLD_NOW)(snd_card_plugin.c:105),通过 snd_card_ops 符号(snd_card_plugin.c:90)获取设备定义中的 type 属性
  4. snd_utils_get_node_type() 读取设备 type 字段(snd_card_plugin.c:74-84);若为 SND_NODE_TYPE_PLUGIN(snd_card_plugin.h:53),则将 pcm->ops 切换为 plug_ops 并重新调用 open
  5. 若既无 hw 设备又无 plugin 配置,返回 ENODEV

pcm_params_get()(pcm.c:697-756)遵循完全相同的回退逻辑,确保参数查询路径与打开路径一致。


二、pcm_ops 多态分发

所有 PCM 操作的统一抽象是 struct pcm(pcm.c:298-328):

c 复制代码
struct pcm {
    int fd;
    unsigned int flags;
    // ... 运行态字段 ...
    struct pcm_config config;
    // ... mmap/status 字段 ...
    /** Pointer to the pcm ops, either hw or plugin */
    const struct pcm_ops *ops;
    /** Private data for pcm_hw or pcm_plugin */
    void *data;
    struct snd_node *snd_node;
};

关键字段 ops 和 data 构成多态分发核心。ops 指向 pcm_ops 虚函数表,data 指向该虚函数表的私有上下文。无论底层走 hw 还是 plugin,上层 API(pcm_writei、pcm_start、pcm_close 等)统一通过 pcm->ops->xxx(pcm->data, ...) 分发。

pcm_ops 接口定义在 pcm_io.h:38-47:

c 复制代码
struct pcm_ops {
    int (*open)  (unsigned int card, unsigned int device,
                  unsigned int flags, void **data, struct snd_node *node);
    void (*close) (void *data);
    int (*ioctl) (void *data, unsigned int cmd, ...);
    void *(*mmap) (void *data, void *addr, size_t length, int prot, int flags,
                   off_t offset);
    int (*munmap) (void *data, void *addr, size_t length);
    int (*poll)  (void *data, struct pollfd *pfd, nfds_t nfds, int timeout);
};

该接口定义了两条独立实现:

c 复制代码
// pcm_hw.c:146-153
const struct pcm_ops hw_ops = {
    .open = pcm_hw_open, .close = pcm_hw_close,
    .ioctl = pcm_hw_ioctl, .mmap = pcm_hw_mmap,
    .munmap = pcm_hw_munmap, .poll = pcm_hw_poll,
};

// pcm_plugin.c:788-795
const struct pcm_ops plug_ops = {
    .open = pcm_plug_open, .close = pcm_plug_close,
    .ioctl = pcm_plug_ioctl, .mmap = pcm_plug_mmap,
    .munmap = pcm_plug_munmap, .poll = pcm_plug_poll,
};

以 pcm_set_config()(pcm.c:432-539)为例,其对 hw_params 和 sw_params 的配置全部通过 pcm->ops->ioctl() 分发,不区分设备类型:

c 复制代码
// pcm.c:478
if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_HW_PARAMS, &params)) {
    // ...
}

三、私有数据结构对比

hw 路径:pcm_hw_data

c 复制代码
// pcm_hw.c:50-59
struct pcm_hw_data {
    unsigned int card;
    unsigned int device;
    int fd;                       // open("/dev/snd/pcmC%D%u%c") 的返回值
    struct snd_node *node;        // 通常为 NULL
};

hw 路径是对文件描述符的最薄封装。pcm_hw_open()(pcm_hw.c:103-144)构造设备节点路径并调用系统 open(),pcm_hw_ioctl() 直接转发到内核(pcm_hw.c:71-82),pcm_hw_mmap() 直接调用系统 mmap()(pcm_hw.c:90-96)。所有参数协商、状态机管理、缓冲区分配均由内核 ALSA 驱动完成。

plugin 路径:pcm_plug_data

c 复制代码
// pcm_plugin.c:72-83
struct pcm_plug_data {
    unsigned int card;
    unsigned int device;
    unsigned int fd;
    unsigned int flags;

    void                        *dl_hdl;      // dlopen(so_name) 返回值
    const struct pcm_plugin_ops *ops;         // dlsym("pcm_plugin_ops") 返回值
    struct pcm_plugin           *plugin;      // plugin .so 创建的实例
    void                        *dev_node;    // 配置节点句柄
};

额外引入的 dl_hdl、ops、plugin 三字段是 plugin 机制的核心。pcm_plug_open()(pcm_plugin.c:724-786)从配置节点读取 so-name 属性,经 dlopen() 加载动态库,再通过 dlsym() 获取 pcm_plugin_ops 符号,最终调用 .so 导出的 open 回调创建 pcm_plugin 实例。


四、用户态 ioctl 模拟

这是 hw 与 plugin 最本质的差异。hw 路径的 pcm_hw_ioctl 仅一行转发:

c 复制代码
// pcm_hw.c:71-82
static int pcm_hw_ioctl(void *data, unsigned int cmd, ...) {
    struct pcm_hw_data *hw_data = data;
    // 可变参数解析...
    return ioctl(hw_data->fd, cmd, arg);
}

plugin 路径无内核设备节点,pcm_plug_ioctl()(pcm_plugin.c:635-690)充当用户态 ioctl 模拟器:

c 复制代码
switch (cmd) {
case SNDRV_PCM_IOCTL_INFO:
    ret = pcm_plug_info(plug_data, arg);
    break;
case SNDRV_PCM_IOCTL_TTSTAMP:
    ret = pcm_plug_ttstamp(plug_data, arg);
    break;
case SNDRV_PCM_IOCTL_HW_REFINE:
    ret = pcm_plug_hrefine(plug_data, arg);
    break;
case SNDRV_PCM_IOCTL_HW_PARAMS:
    ret = pcm_plug_hparams(plug_data, arg);
    break;
case SNDRV_PCM_IOCTL_SW_PARAMS:
    ret = pcm_plug_sparams(plug_data, arg);
    break;
case SNDRV_PCM_IOCTL_SYNC_PTR:
    ret = pcm_plug_sync_ptr(plug_data, arg);
    break;
case SNDRV_PCM_IOCTL_PREPARE:
    ret = pcm_plug_prepare(plug_data);
    break;
case SNDRV_PCM_IOCTL_START:
    ret = pcm_plug_start(plug_data);
    break;
case SNDRV_PCM_IOCTL_DRAIN:
    ret = pcm_plug_drain(plug_data);
    break;
case SNDRV_PCM_IOCTL_DROP:
    ret = pcm_plug_drop(plug_data);
    break;
case SNDRV_PCM_IOCTL_WRITEI_FRAMES:
    ret = pcm_plug_writei_frames(plug_data, arg);
    break;
case SNDRV_PCM_IOCTL_READI_FRAMES:
    ret = pcm_plug_readi_frames(plug_data, arg);
    break;
default:
    ret = plug_data->ops->ioctl(plugin, cmd, arg);
    break;
}

明确处理的 ioctl 命令覆盖 ALSA PCM 参数协商(HW_REFINE、HW_PARAMS、SW_PARAMS)、指针同步(SYNC_PTR)、生命周期管理(PREPARE、START、DRAIN、DROP)以及数据读写(WRITEI_FRAMES、READI_FRAMES)。未命中的命令通过 default 分支转发给 plugin .so 的 ioctl 回调。


五、hw_params 参数协商

hw 路径中,内核 ALSA 驱动在 SNDRV_PCM_IOCTL_HW_PARAMS ioctl 中完成参数 refine:接收应用请求的参数范围,与硬件约束求交集,返回精炼后的参数。

plugin 路径在用户态复现这一过程,调用链为:

scss 复制代码
pcm_plug_hparams()          // pcm_plugin.c:496-521
  → __pcm_plug_hrefine()    // pcm_plugin.c:402-418
  → pcm_plug_hw_params_set()// pcm_plugin.c:462-494
  → ops->hw_params()        // 通知 plugin .so

5.1 refine 阶段

__pcm_plug_hrefine()(pcm_plugin.c:402-418)分两步:先从 plugin->constraints 构建 plugin 侧的 snd_pcm_hw_params,再调用 pcm_plug_hw_params_refine()(pcm_plugin.c:379-400)对应用请求与 plugin 约束求交集。

plugin 的能力声明通过 pcm_plugin_hw_constraints 结构体(plugin.h:155-171)定义:

c 复制代码
struct pcm_plugin_hw_constraints {
    uint64_t access;                     // ACCESS mask
    uint64_t format;                     // FORMAT mask(64-bit 覆盖 52 个 ALSA format)
    struct pcm_plugin_min_max bit_width; // SAMPLE_BITS 范围
    struct pcm_plugin_min_max channels;  // CHANNELS 范围
    struct pcm_plugin_min_max rate;      // RATE 范围
    struct pcm_plugin_min_max periods;   // PERIODS 范围
    struct pcm_plugin_min_max period_bytes; // PERIOD_BYTES 范围
};

pcm_plug_get_params()(pcm_plugin.c:209-281)将 constraints 填充为标准 snd_pcm_hw_params,其中 FRAME_BITS、PERIOD_SIZE、BUFFER_BYTES、BUFFER_SIZE 等派生参数由基础约束计算得出。

pcm_plug_hw_params_refine() 依次调用 pcm_plug_masks_refine()(pcm_plugin.c:283-313)和 pcm_plug_interval_refine()(pcm_plugin.c:315-376):

  • masks_refine:对每个请求的 mask 与约束 mask 做按位与,交集结果写回请求参数,若发生修改则置位 cmask
  • interval_refine:收紧请求区间的 min/max 使其不低于约束区间,处理 open/close 边界标志和 integer 标志

这一逻辑与内核 ALSA 的 hw_params refine 语义完全一致。

5.2 set 阶段

pcm_plug_hw_params_set()(pcm_plugin.c:462-494)在 refine 后的参数空间内做出具体选择:

对 param_list 中声明的五个基础参数(SAMPLE_BITS、CHANNELS、RATE、PERIOD_SIZE、PERIODS)统一选取最小值,然后据此计算派生参数(FRAME_BITS、PERIOD_BYTES、BUFFER_SIZE、BUFFER_BYTES)的确切值。

5.3 通知 plugin

完成 refine 和 set 后,pcm_plug_hparams() 调用 plug_data->ops->hw_params(plugin, params) 将最终参数传递给 plugin .so,随后将状态推进至 PCM_PLUG_STATE_SETUP。


六、状态机

hw PCM 的状态机由内核 ALSA 驱动维护,用户态通过 sync_ptr 或 mmap 读取。plugin PCM 无内核支撑,状态机完全在用户态维护。

6.1 状态定义

c 复制代码
// pcm_plugin.c:65-70
enum {
    PCM_PLUG_STATE_OPEN,      // pcm_plug_open 成功后的初始状态
    PCM_PLUG_STATE_SETUP,     // hw_params 成功后
    PCM_PLUG_STATE_PREPARED,  // prepare 成功后
    PCM_PLUG_STATE_RUNNING,   // start 成功后
};

6.2 状态转换与守卫

每次操作前进行状态守卫,违反时序返回 -EBADFD:

操作 前置状态 后置状态 源码位置
hw_params OPEN SETUP pcm_plugin.c:502
sw_params SETUP SETUP pcm_plugin.c:526-529
prepare SETUP PREPARED pcm_plugin.c:583-596
start PREPARED RUNNING pcm_plugin.c:598-611
writei_frames PREPARED / RUNNING 不变 pcm_plugin.c:549-558
readi_frames RUNNING 不变 pcm_plugin.c:561-569
drain RUNNING 不变 pcm_plugin.c:625-633
drop 无前置检查 SETUP pcm_plugin.c:613-623
mmap / munmap SETUP 不变 pcm_plugin.c:701-722

sync_ptr 操作要求状态不低于 SETUP(pcm_plugin.c:534-547),并通过 convert_plugin_to_pcm_state()(pcm_plugin.c:93-109)将内部状态映射为上层可见的 ALSA 标准状态:

c 复制代码
static int convert_plugin_to_pcm_state(int plugin_state)
{
    switch (plugin_state) {
    case PCM_PLUG_STATE_SETUP:    return PCM_STATE_SETUP;
    case PCM_PLUG_STATE_RUNNING:  return PCM_STATE_RUNNING;
    case PCM_PLUG_STATE_PREPARED: return PCM_STATE_PREPARED;
    case PCM_PLUG_STATE_OPEN:     return PCM_STATE_OPEN;
    default: break;
    }
    return PCM_STATE_OPEN;
}

这确保了上层通过 pcm_state()(pcm.c:620-628)读取到的状态值与 hw 路径完全一致。


七、plugin .so 接口规范

plugin 的实现方编译为动态库 .so,需导出一个名为 pcm_plugin_ops 的全局符号,其类型为 struct pcm_plugin_ops(plugin.h:99-140):

c 复制代码
struct pcm_plugin_ops {
    int (*open)        (struct pcm_plugin **plugin, unsigned int card,
                        unsigned int device, unsigned int flags);
    int (*close)       (struct pcm_plugin *plugin);
    int (*hw_params)   (struct pcm_plugin *plugin, struct snd_pcm_hw_params *params);
    int (*sw_params)   (struct pcm_plugin *plugin, struct snd_pcm_sw_params *params);
    int (*sync_ptr)    (struct pcm_plugin *plugin, struct snd_pcm_sync_ptr *sync_ptr);
    int (*writei_frames)(struct pcm_plugin *plugin, struct snd_xferi *x);
    int (*readi_frames)(struct pcm_plugin *plugin, struct snd_xferi *x);
    int (*ttstamp)     (struct pcm_plugin *plugin, int *tstamp);
    int (*prepare)     (struct pcm_plugin *plugin);
    int (*start)       (struct pcm_plugin *plugin);
    int (*drain)       (struct pcm_plugin *plugin);
    int (*drop)        (struct pcm_plugin *plugin);
    int (*ioctl)       (struct pcm_plugin *plugin, int cmd, void *arg);
    void *(*mmap)      (struct pcm_plugin *plugin, void *addr, size_t length,
                        int prot, int flags, off_t offset);
    int (*munmap)      (struct pcm_plugin *plugin, void *addr, size_t length);
    int (*poll)        (struct pcm_plugin *plugin, struct pollfd *pfd, nfds_t nfds,
                        int timeout);
};

在 open 回调中,实现方需完成:

  1. 分配并初始化 struct pcm_plugin(plugin.h:173-186),其中 card、device、mode 由调用方传入
  2. 设置 constraints 指向 pcm_plugin_hw_constraints,声明该 plugin 支持的格式、采样率、声道数、period 大小等能力边界
  3. 设置 priv 指向私有数据

参考实现 sample_pcm_plugin.c:271-332 展示了完整模式:open 回调创建 pcm_plugin 实例并绑定 constraints,各回调函数从 plugin->priv 获取私有上下文执行实际操作。

pcm_plug_open() 中的加载序列(pcm_plugin.c:751-764):

c 复制代码
plug_data->ops = dlsym(dl_hdl, "pcm_plugin_ops");
// ...
rc = plug_data->ops->open(&plug_data->plugin, card, device, flags);

tinyalsa 中间层通过 dlsym 定位 pcm_plugin_ops 符号,再调用其 open 回调完成 plugin 实例化。open 回调通过输出参数 struct pcm_plugin **plugin 返回创建好的实例指针。


八、三层架构总结

scss 复制代码
┌─────────────────────────────────────┐
│  应用层                               │
│  pcm_open() / pcm_set_config()      │
│  pcm_writei() / pcm_start() / ...   │
└──────────────┬──────────────────────┘
               │  pcm->ops->xxx() 分发
┌──────────────▼──────────────────────┐
│  tinyalsa 内置层 (plug_ops)          │
│  • ioctl 转译与状态守卫              │
│  • hw_params refine + set           │
│  • 状态机管理 (OPEN→SETUP→...)      │
│  • mmap/poll 代理                   │
└──────────────┬──────────────────────┘
               │  dlopen(so-name) → dlsym("pcm_plugin_ops")
┌──────────────▼──────────────────────┐
│  用户态 plugin .so                    │
│  • 实际音频数据处理                   │
│  • 格式转换 / 路由 / 混音 / 写文件   │
└─────────────────────────────────────┘

配置解析由 libsndcardparser.so 独立承担:snd_utils_open_pcm() 通过 dlopen("libsndcardparser.so") 加载(snd_card_plugin.c:105),解析 sound card 定义文件中的设备类型(hw/plugin)、plugin 库名(so-name)、设备能力(playback/capture)等元信息。其接口通过 snd_node_ops(plugin.h:255-270)抽象,由 libsndcardparser.so 导出的 snd_card_ops 符号实现。


九、hw PCM 与 plugin PCM 对比

维度 hw PCM plugin PCM
设备节点 /dev/snd/pcmCxDy[p/c] 字符设备 无内核节点,纯用户态
私有数据 pcm_hw_data(pcm_hw.c:50)--- 包装 fd pcm_plug_data(pcm_plugin.c:72)--- 含 .so 句柄、plugin 实例
open 实现 open() 系统调用(pcm_hw.c:119) dlopen() + dlsym() + ops->open()(pcm_plugin.c:743-760)
ioctl 处理 直接 ioctl(fd, cmd, arg)(pcm_hw.c:81) switch-case 分发 + 状态守卫(pcm_plugin.c:647-687)
参数协商 内核 ALSA 驱动 __pcm_plug_hrefine() + pcm_plug_hw_params_set()(pcm_plugin.c:402-494)
状态机 内核维护,用户态通过 mmap/sync_ptr 读取 用户态 plugin->state 字段维护(pcm_plugin.c:65-70)
mmap 支持 原生,系统 mmap 到内核环形缓冲区 代理至 plugin .so 的 mmap 回调(pcm_plugin.c:701-711)
数据读写 WRITEI/READI ioctl 转发至内核 代理至 plugin .so 的 writei/readi 回调
扩展方式 需更新内核 ALSA 驱动 替换 .so + 更新 sound card 配置,无需重编译 tinyalsa

plugin 机制使 tinyalsa 在无需修改应用层代码的前提下,支持格式转换、设备路由、软件混音等用户态处理能力,同时将参数协商与状态管理的负担从 plugin 实现方剥离,由 tinyalsa 内置中间层统一完成。

相关推荐
La Pulga11 小时前
【FreeRTOS源码学习笔记】任务延时列表
笔记·mcu·学习·嵌入式·freertos·实时操作系统
2035去旅行11 小时前
bootcmd 详细讲解
linux·嵌入式·arm·soc
蓝天居士11 小时前
EtherCAT从入门到精通(7)
机器人·嵌入式·ethercat
蓝天居士13 小时前
EtherCAT从入门到精通(10)
机器人·嵌入式·ethercat
不脱发的程序猿17 小时前
GD32串口数据收进内存之后DMA如何处理?
单片机·嵌入式硬件·dma·嵌入式·串口通信·gd32
lishen21 天前
06 · 我把开发流程拆成了 AI 能执行的技能
嵌入式
天天不在5512 天前
从拉代码到编出库,WebRTC 上的几道坎
音视频开发
§ꦿCFོ༉2 天前
嵌入式八股文—C语言入门学习(二)
c语言·学习·嵌入式·编程语言
Zwawa3 天前
两根线怎么挂住一堆设备:I2C 协议原理
c语言·单片机·嵌入式
欧西3 天前
零成本做一颗芯片:我用 OpenLane 把 SHA-256 从 Verilog 做到了 GDS
开源·嵌入式