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;
};

关键字段 opsdata 构成多态分发核心。ops 指向 pcm_ops 虚函数表,data 指向该虚函数表的私有上下文。无论底层走 hw 还是 plugin,上层 API(pcm_writeipcm_startpcm_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_hdlopsplugin 三字段是 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 .soioctl 回调。


五、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_ptrmmap 读取。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),其中 carddevicemode 由调用方传入
  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 内置中间层统一完成。

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