在 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
回退流程:
- 默认绑定
hw_ops,尝试打开/dev/snd/pcmC*D*p/c(pcm_hw.c:103-144) - 若返回负值,调用
snd_utils_open_pcm()查询 sound card 配置 snd_utils_open_pcm()内部执行dlopen("libsndcardparser.so", RTLD_NOW)(snd_card_plugin.c:105),通过snd_card_ops符号(snd_card_plugin.c:90)获取设备定义中的type属性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- 若既无 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, ¶ms)) {
// ...
}
三、私有数据结构对比
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 回调中,实现方需完成:
- 分配并初始化
struct pcm_plugin(plugin.h:173-186),其中card、device、mode由调用方传入 - 设置
constraints指向pcm_plugin_hw_constraints,声明该 plugin 支持的格式、采样率、声道数、period 大小等能力边界 - 设置
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 内置中间层统一完成。