文章目录
- 1. 前言
- 2. 分析背景
- 3. Linux ALSA 框架
- 4. alsa 声卡设备
- 5. alsa-lib 简介
- 5.1 alsa-lib 插件
- 5.1.1 alsa-lib 插件概览
- 5.1.2 alsa-lib 插件工作细节
- 5.1.2.1 插件对象的创建和初始化
- 5.1.2.2 插件对象处理数据的过程
- 5.1.3 alsa-lib 内置插件代码组织
- 5.1.4 自定义 alsa-lib 插件
- 5.2 使用 alsa-lib API 编程
- 5.3 为 ARM 交叉编译 alsa-lib 和 alsa-utils
- 5.4 alsa-lib 配置
- 6. 参考资料
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 分析背景
本文基于 alsa-lib-1.2.9
源码进行分析。
3. Linux ALSA 框架
上图是 Linux ALSA 框架概览
,包括 用户空间
和 内核空间
的各个组成部分。ALSA
是 Advanced Linux Sound Architecture
的缩写。ASoC
是 ALSA System on Chip
的缩写,是针对片上系统引入的中间层:为了适应 Platform
和 Codec
硬件上的分离,对基础 ALSA
基础框架实现进行了解耦。
本文重点关注 用户空间
红色框选中的 alsa-lib
部分。
4. alsa 声卡设备
对声卡的操作,是通过 ALSA CORE
向用户空间导出的、声卡相关的字符设备节点来完成:
$ ls -l /dev/snd/
total 0
drwxr-xr-x 2 root root 60 10月 7 09:12 by-path
crw-rw----+ 1 root audio 116, 2 10月 7 09:12 controlC0
crw-rw----+ 1 root audio 116, 6 10月 7 09:12 midiC0D0
crw-rw----+ 1 root audio 116, 4 10月 7 09:13 pcmC0D0c
crw-rw----+ 1 root audio 116, 3 10月 7 09:13 pcmC0D0p
crw-rw----+ 1 root audio 116, 5 10月 7 09:12 pcmC0D1p
crw-rw----+ 1 root audio 116, 1 10月 7 09:12 seq
crw-rw----+ 1 root audio 116, 33 10月 7 09:12 timer
对上面输出的设备节点,只挑我们关注的几个进行说明。/dev/snd/controlC0
是声卡控制设备节点,可以选择通道、控制音量等;/dev/snd/pcmC0D0c
是声卡的录音节点,可以用来录音;/dev/snd/pcmC0D0p,/dev/snd/pcmC0D1p
是声卡的播放节点,可以用来播放音频数据。本文以 音频播放过程
为例,对 alsa-lib 插件
的加以介绍。用户空间应用播放音频的流程,可以简要的概括如下:
/* 打开播放设备 */
fd = open("/dev/snd/pcmC0D0p", O_RDWR);
/* 设置硬件参数 */
struct snd_pcm_hw_params hw_params;
// 初始化硬件参数 @hw_params ...
ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &hw_params);
/* 设置软件参数(可选) */
struct snd_pcm_sw_params sw_params;
// 初始化软件参数 @sw_params ...
ioctl(fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sw_params);
/* 准备好 PCM 数据 */
char *play_data;
// ...
/* 播放数据 */
ioctl(fd, SNDRV_PCM_IOCTL_PREPARE); // 设备准备工作
struct snd_xferi transfer;
// 设定 传输数据缓冲(@play_data) 和 大小(帧数)
ioctl(dev_fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &transfer); // 播放音频数据
/* 关闭设备 */
close(fd);
我们用下图来描述播放音频时的数据走向:
5. alsa-lib 简介
alsa-lib
是为了简化、便利用户空间对 ALSA
驱动框架声卡编程的开源库,和 ALSA
驱动框架一样,同属于 ALSA project
开源项目。
更多关于 alsa-lib
的细节,可以参考 ALSA project
的官方链接:https://www.alsa-project.org/alsa-doc/alsa-lib/index.html 。本文重点对 alsa-lib 插件
做一些简介。
5.1 alsa-lib 插件
5.1.1 alsa-lib 插件概览
alsa-lib 插件
起作用的地方,位于上图中 user space
与 DMA Buffer
之间。简单来讲,alsa-lib 插件
的作用就是:在进入内核空间将数据拷贝到 DMA buffer 之前,通过插件定义的行为(算法),对用户空间的原始数据进行一到多次(对应一到多个插件)加工,然后再拷贝到 DMA buffer
。我们对上图稍作修改,来描述 alsa-lib 插件
在整个播放流程中扮演的角色:
上图中红色框中的部分,每个 alsa-lib 插件
通过预定义的行为(算法),对输入数据进行处理后,输出给下一个插件。
到此,我们对插件的工作原理已经有了初步的了解。接下来看如何使用 alsa-lib 插件
来自定义对用户空间播放原始数据的处理。假设有一个 S16_LE,48KHz, 2通道
的音频文件 test.wav
,要在只支持 S32_LE,48KHz, 2通道
的声卡上播放,这样就需要将 S16_LE
的 test.wav
转换为 S32_LE
数据然后在声卡上播放。此时我们可以在 /etc/asound.conf
中定义可以将 S16_LE
转换为 S32_LE
的转换插件 s16le_s32le
:
pcm.s16le_s32le {
type plug
slave {
pcm "hw:0,0"
format S32_LE
channels 2
rate 48000
}
}
上面的 "hw:0,0"
代表第1片声卡。 播放的时候,调用插件 s16le_s32le
进行数据格式转换(S16_LE => S32_LE
):
$ aplay -D plug:s16le_s32le -f S16_LE -c2 -r48000 test.wav
aplay
会读取 /etc/asound.conf
中我们定义的 s16le_s32le
插件,然后按配置寻找匹配的 alsa-lib 插件
,然后调用插件的数据处理接口进行数据处理:
s16le_s32le 插件
user space (test.wav S16_LE 数据) ================> 经 s16le_s32le 插件转换后的 S32_LE 数据 => DMA Buffer => ......
5.1.2 alsa-lib 插件工作细节
5.1.2.1 插件对象的创建和初始化
前面对 aplay
调用插件播放音频数据的大概流程做了叙述,接下来看一看 aplay
读取 s16le_s32le
插件配置、以及按该配置寻找匹配插件、并最终调用匹配的插件转换数据的实现细节:
/* aplay -D plug:s16le_s32le -f dat test.wav */
main() // alsa-utils-1.2.9/aplay/aplay.c
char *pcm_name = "default"; // 缺省的播放设备,通常 "default" 代表 "hw:0,0"
...
// 解析命令行参数 -D plug:s16le_s32_le
while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) {
...
case 'D':
pcm_name = optarg; /* pcm_name = "plug:s16le_s32_le" */
break;
...
}
/*
* 解析配置文件,寻找匹配配置定义的插件,然后
* 创建 plug:s16le_s32_le 和 /dev/snd/pcmC0D0p 的 PCM 对象并初始化,
* 然后建立插件 plug:s16le_s32_le 和 /dev/snd/pcmC0D0p 的关联。
*/
err = snd_pcm_open(&handle, pcm_name, stream, open_mode); // alsa-lib-1.2.9/src/pcm/pcm.c
snd_config_t *top;
if (_snd_is_ucm_device(name)) { /* @name: _ucmXXX */
...
} else {
err = snd_config_update_ref(&top); /* @top: /usr/share/alsa/alsa.conf 配置对象 */
...
}
/*
* snd_pcm_open_noupdate() 的工作过程概述如下:
* 1. 解析 配置对象 @top 中名为 @name 的 插件 的配置,按 插件 或 设备
* 配置的 type 属性,找到匹配 type 的 内置 或 扩展 的 插件,然后调用 插件
* 或 设备的 open 接口 初始化 插件 或 设备。
* 2. 在 插件 或 设备 的 open 接口中,首先检查自身的配置是否包含 slave 属性:
* 如果包含 slave 属性,解析 slave 的配置,以解析的配置对象为第2个参数,
* 递归调用 snd_pcm_open_noupdate() ,进入步骤 1,在调用返回后,用返回的
* slave 的 PCM 对象,建立 当前 插件 和 slave 的连接,以此逐级建立 插件
* 和 设备 间的层级关联;
* 如果不包含 slave 属性,则为 插件 或 设备 创建 PCM 对象,初始化后返回。
*/
// 在这里我们只分析我们场景下的调用关系
err = snd_pcm_open_noupdate(pcmp, top, name, stream, mode, 0);
snd_config_t *pcm_conf;
err = snd_config_search_definition(root, "pcm", name, &pcm_conf);
...
if (snd_config_get_string(pcm_conf, &str) >= 0)
...
else {
snd_config_set_hop(pcm_conf, hop);
// 解析插件
err = snd_pcm_open_conf(pcmp, name, root, pcm_conf, stream, mode);
...
err = snd_config_search(pcm_conf, "type", &conf); // @conf => type plug
...
err = snd_config_get_id(conf, &id);
...
err = snd_config_get_string(conf, &str); /* @str: "plug" */
...
if (!open_name) { /* 设定插件 open 函数名 */
buf = malloc(strlen(str) + 32);
...
open_name = buf;
sprintf(buf, "_snd_pcm_%s_open", str); // @buf: "_snd_pcm_plug_open"
}
/*
* 设定插件 open 函数所在的 @lib:
* . 如果 @str 字串匹配内置插件名列表 build_in_pcms[] 中的某一个,
* 表示 @str 指向内置插件, 则使用 libasound.so.* 库查找插件 open
* 函数接口, @#lib 赋值为 NULL ;
* . 如果 @str 字串不能匹配插件名列表 build_in_pcms[] 中的任一个,
* 表示 @str 指向非内置、扩展的外部插件, @lib 赋值为扩展插件库名
* "libasound_module_pcm_%s.so"
*/
if (!lib) {
const char *const *build_in = build_in_pcms; /* 内置插件列表 */
while (*build_in) {
if (!strcmp(*build_in, str)) /* 看 @str 是否是内置插件 */
break;
build_in++;
}
if (*build_in == NULL) { /* 非内置插件: 外部扩展插件 libasound_module_pcm_%s.so */
buf1 = malloc(strlen(str) + 32);
...
lib = buf1;
// @str = "XXX" ==> libasound_module_pcm_XXX.so
sprintf(buf1, "libasound_module_pcm_%s.so", str);
}
}
...
/*
* . 如果是内置插件, 从 libasound.so.* 库中获取函数 @open_name 的地址;
* . 如果是扩展(非内置)插件, 从扩展插件库 libasound_module_pcm_XXX.so 中
* 获取函数 @open_name 的地址.
*/
open_func = snd_dlobj_cache_get(lib, open_name,
SND_DLSYM_VERSION(SND_PCM_DLSYM_VERSION), 1);
if (open_func) {
// 调用 插件 或 设备的 open 接口
err = open_func(pcmp, name, pcm_root, pcm_conf, stream, mode);
// 下接后面的 _snd_pcm_plug_open() 调用分析
_snd_pcm_plug_open(pcmp, name, pcm_root, pcm_conf, stream, mode) // alsa-lib-1.2.9/src/pcm/pcm_plug.c
}
}
snd_config_delete(pcm_conf);
snd_config_unref(top); /* 删除配置文件对象 */
来看具体插件的 open
接口调用过程:
// 本文示例中使用的插件有点特别,它的 type 为 "plug"
// pcm.s16le_s32le {
// type plug
// slave {
// pcm "hw:0,0"
// format S32_LE
// channels 2
// rate 48000
// }
// }
// 其它的一些插件,如定义为 type rate 的插件,很容易从它的名字知道是用来转换采样率的。
// 而从 type plug 中,我们无法分辨出,这个插件是做什么用的,alsa-lib 为 type plug 定
// 义了一个通用插件,alsa-lib 为该类型的插件设定了一些内置的规则,用来根据插件的配置,
// 自动决定改如何根据插件配置对数据进行处理,细节见后面 参数设置 和 数据处理 的分析代码。
_snd_pcm_plug_open(pcmp, name, pcm_root, pcm_conf, stream, mode) // alsa-lib-1.2.9/src/pcm/pcm_plug.c
...
snd_config_for_each(i, next, conf) { // 遍历 type plug 类型插件【第1层级】的所有属性
snd_config_t *n = snd_config_iterator_entry(i); // 插件属性配置项
const char *id;
if (snd_config_get_id(n, &id) < 0) // 获取属性 @n 的名称,如 slave
continue;
...
if (strcmp(id, "slave") == 0) { // 如果有 slave 节点,记录 slave 属性配置项
slave = n;
continue;
}
}
...
// 解析 plug 的 slave 配置:
// slave {
// pcm "hw:0,0"
// format S32_LE
// channels 2
// rate 48000
// }
// 后续在数据处理时,根据这些解析的配置信息,自动决定该如何对数据进行处理。
err = snd_pcm_slave_conf(root, slave, &sconf, 3,
SND_PCM_HW_PARAM_FORMAT, SCONF_UNCHANGED, &sformat,
SND_PCM_HW_PARAM_CHANNELS, SCONF_UNCHANGED, &schannels,
SND_PCM_HW_PARAM_RATE, SCONF_UNCHANGED, &srate);
...
// 打开 plug 的 slave 插件 或 设备。
// 这里的流程又会和前面的 snd_pcm_open() 处类似,流程会间接递归进入 snd_pcm_open_noupdate() ,
// 所以不再赘述。
// 在这条调用路径上,最终会打开一个声卡硬件设备,这个是我们用户空间音频数据进入的目标位置。
err = snd_pcm_open_slave(&spcm, root, sconf, stream, mode, conf);
snd_pcm_open_named_slave(pcmp, NULL, root, conf, stream, mode, parent_conf); // alsa-lib-1.2.9/src/pcm/pcm_local.h
...
if (snd_config_get_string(conf, &str) >= 0)
return snd_pcm_open_noupdate(pcmp, root, str, stream, mode, hop + 1);
return snd_pcm_open_conf(pcmp, name, root, conf, stream, mode);
snd_config_delete(sconf); // 删除 plug 的 slave 的配置对象
// 打开 plug 插件, 并关联 plug PCM @pcmp 和 其 slave 的 PCM @spcm
err = snd_pcm_plug_open(pcmp, name, sformat, schannels, srate, rate_converter,
route_policy, ttable, ssize, cused, sused, spcm, 1);
看看 snd_pcm_slave_conf()
是怎么自动决定 type plug
的插件类型的:
// 解析 type plug 插件 slave 的配置
err = snd_pcm_slave_conf(root, slave, &sconf, 3, // alsa-lib-1.2.9/src/pcm/pcm.c
SND_PCM_HW_PARAM_FORMAT, SCONF_UNCHANGED, &sformat,
SND_PCM_HW_PARAM_CHANNELS, SCONF_UNCHANGED, &schannels,
SND_PCM_HW_PARAM_RATE, SCONF_UNCHANGED, &srate)
...
snd_config_t *pcm_conf = NULL;
...
// fields[0]: {.index = SND_PCM_HW_PARAM_FORMAT, .flags = SCONF_UNCHANGED, .ptr = &sformat}
// fields[1]: {.index = SND_PCM_HW_PARAM_CHANNELS, .flags = SCONF_UNCHANGED, .ptr = &schannels}
// fields[2]: {.index = SND_PCM_HW_PARAM_RATE, .flags = SCONF_UNCHANGED, .ptr = &srate}
va_start(args, count);
for (k = 0; k < count; ++k) {
fields[k].index = va_arg(args, int);
fields[k].flags = va_arg(args, int);
fields[k].ptr = va_arg(args, void *);
fields[k].present = 0;
}
va_end(args);
...
/*
* @conf
* ||
* \/
* slave {
* pcm "hw:0,0"
* format S32_LE
* channels 2
* rate 48000
* }
*
* 注:这里的 pcm xxx_audio 指代一个实际的声卡设备,而不是一个 alsa-lib 的 plug-in 。
*/
snd_config_for_each(i, next, conf) {
snd_config_t *n = snd_config_iterator_entry(i);
const char *id;
if (snd_config_get_id(n, &id) < 0)
continue;
...
if (strcmp(id, "pcm") == 0) {
if (pcm_conf != NULL)
snd_config_delete(pcm_conf);
if ((err = snd_config_copy(&pcm_conf, n)) < 0) // @pcm_conf => hw:0,0
goto _err;
continue;
}
for (k = 0; k < count; ++k) {
// SND_PCM_HW_PARAM_FORMAT, SND_PCM_HW_PARAM_CHANNELS, SND_PCM_HW_PARAM_RATE
unsigned int idx = fields[k].index;
...
if (strcmp(id, names[idx]) != 0)
continue;
switch (idx) { // format S32_LE
case SND_PCM_HW_PARAM_FORMAT: {
snd_pcm_format_t f;
...
f = snd_pcm_format_value(str);
...
*(snd_pcm_format_t*)fields[k].ptr = f; // format S32_LE ==> SND_PCM_FORMAT_S32_LE
break;
}
default:
...
err = snd_config_get_integer(n, &v);
...
*(int*)fields[k].ptr = v;
break;
}
}
}
...
*_pcm_conf = pcm_conf; // 返回解析的配置对象
...
这里不仔细分析 type plug 插件 slave 声卡设备
的打开流程,主体无非就是 open("/dev/snd/pcmC0D0p", ...)
,感兴趣的读者可自行阅读相关代码。我们重点看一下 type plug 插件
的打开流程,因为这关系到后面的数据处理流程分析:
// 打开 plug 插件, 并关联 plug PCM @pcmp 和 其 slave 的 PCM @spcm
err = snd_pcm_plug_open(pcmp, name, sformat, schannels, srate, rate_converter, // alsa-lib-1.2.9/src/pcm/pcm_plug.c
route_policy, ttable, ssize, cused, sused, spcm, 1);
snd_pcm_t *pcm;
snd_pcm_plug_t *plug;
plug = calloc(1, sizeof(snd_pcm_plug_t));
...
plug->sformat = sformat;
plug->schannels = schannels;
plug->srate = srate;
// 关联 plug 插件的从设 PCM 。
// 我们的场景是 /dev/snd/pcmC0D0p ,后面设置参数时(见 set_params()),
// 会被修改为 S16_LE 转 S32_LE 的 linear 插件。
plug->gen.slave = plug->req_slave = slave;
...
// 新建 plug 插件的 PCM 对象
err = snd_pcm_new(&pcm, SND_PCM_TYPE_PLUG, name, slave->stream, slave->mode);
...
pcm->ops = &snd_pcm_plug_ops;
// pcm->fast_ops = slave->fast_ops = &snd_pcm_hw_fast_ops
// 我们的场景是 /dev/snd/pcmC0D0p 的 fast_ops ,后面设置参数时(见 set_params()),
// 会被修改为 S16_LE 转 S32_LE 的 linear 插件的接口 。
pcm->fast_ops = slave->fast_ops;
pcm->fast_op_arg = slave->fast_op_arg;
...
pcm->private_data = plug;
...
*pcmp = pcm; // 返回 plug 插件的 PCM 对象
return 0;
5.1.2.2 插件对象处理数据的过程
main() // alsa-utils-1.2.9/aplay/aplay.c
/*
* 解析配置文件,寻找匹配配置定义的插件,然后
* 创建 plug:s16le_s32_le 和 /dev/snd/pcmC0D0p 的 PCM 对象并初始化,
* 然后建立插件 plug:s16le_s32_le 和 /dev/snd/pcmC0D0p 的关联。
*/
err = snd_pcm_open(&handle, pcm_name, stream, open_mode); @ alsa-lib-1.2.9/src/pcm/pcm.c
...
/*
* 经插件 plug 处理 test.wav 音频数据,然后传递给声卡设备播放。
*/
playback(argv[optind++]); /* @argv[optind]: "test.wav" */
...
playback_wave(name, &loaded);
// WAVE 文件解析
read_header(loaded, sizeof(WaveHeader))
dtawave = test_wavefile(fd, audiobuf, *loaded)
// 播放 WAVE
pbrec_count = calc_count(); /* 计算 1 秒内所有通道播放的数据总量 */
playback_go(fd, dtawave, pbrec_count, FORMAT_WAVE, name)
header(rtype, name);
// 设置参数: 通道数、采样率、数据格式等等
// 在设置参数的过程中,会
set_params();
...
err = snd_pcm_hw_params(handle, params);
...
err = _snd_pcm_hw_params_internal(pcm, params);
...
if (pcm->ops->hw_params)
err = pcm->ops->hw_params(pcm->op_arg, params);
snd_pcm_plug_hw_params(pcm->op_arg, params) // 见下面分析
else
err = -ENOSYS;
...
...
err = snd_pcm_prepare(pcm);
...
...
// 播放音频数据: 经 插件 处理后传递给 声卡 播放
while (loaded > chunk_bytes && written < count && !in_aborting) {
if (pcm_write(audiobuf + written, chunk_size) <= 0) // 见下面的分析
return;
written += chunk_bytes;
loaded -= chunk_bytes;
}
...
上面的代码分析给出了 aplay
播放音频文件的主体轮廓:先通过 set_params()
配置参数,然后通过 pcm_write()
播放音频数据。先来看 set_params()
配置参数的流程,过程中很关键的一点是插入了一个新的、为将 S16_LE
转换为 S32_LE
的 linear
插件。
// 参数设置。
// 过程中,会添加一个用来将 S16_LE 转换为 S32_LE 的 linear 插件,层级拓扑变化:
// ---------------------- -----------------------------------
// | slave | | slave slave |
// | plug -----> 声卡设备 | ==> | plug -----> linear -----> 设卡设备 |
// | | | |
// --------------------- ------------------------------------
snd_pcm_plug_hw_params(pcm->op_arg, params) // alsa-lib-1.2.9/src/pcm/pcm-plug.c
snd_pcm_plug_t *plug = pcm->private_data;
snd_pcm_t *slave = plug->req_slave; // plug 当前的 slave 为 声卡设备
...
INTERNAL(snd_pcm_hw_params_get_access)(params, &clt_params.access);
INTERNAL(snd_pcm_hw_params_get_format)(params, &clt_params.format);
INTERNAL(snd_pcm_hw_params_get_channels)(params, &clt_params.channels);
INTERNAL(snd_pcm_hw_params_get_rate)(params, &clt_params.rate, 0);
...
// 关键的来了,比较 plug 和 其 slave 的格式、通道、采样率,
// 如果这些参数有不同,则创建一个新的转换插件,做 plug 新的 slave,
// 而 plug 原来的 slave ,作为新的转换插件的 slave .
if (!(clt_params.format == slv_params.format &&
clt_params.channels == slv_params.channels &&
clt_params.rate == slv_params.rate &&
!plug->ttable &&
snd_pcm_hw_params_test_access(slave, &sparams, clt_params.access) >= 0)) {
INTERNAL(snd_pcm_hw_params_set_access_first)(slave, &sparams, &slv_params.access);
err = snd_pcm_plug_insert_plugins(pcm, &clt_params, &slv_params); // 见后面分析
...
}
...
// 更新操作接口
pcm->fast_ops = slave->fast_ops; /* &snd_pcm_hw_fast_ops -> &snd1_pcm_plugin_fast_ops */
pcm->fast_op_arg = slave->fast_op_arg;
...
err = snd_pcm_plug_insert_plugins(pcm, &clt_params, &slv_params); // alsa-lib-1.2.9/src/pcm/pcm-plug.c
snd_pcm_plug_t *plug = pcm->private_data;
static int (*const funcs[])(snd_pcm_t *_pcm, snd_pcm_t **new,
snd_pcm_plug_params_t *s, snd_pcm_plug_params_t *d) = { // 函数指针表
...
snd_pcm_plug_change_format,
...
};
snd_pcm_plug_params_t p = *slave;
unsigned int k = 0;
...
while (client->format != p.format || client->channels != p.channels ||
client->rate != p.rate || client->access != p.access ||
(plug->ttable && !plug->ttable_ok)) {
snd_pcm_t *new;
...
err = funcs[k](pcm, &new, client, &p);
snd_pcm_plug_change_format(pcm, &new, client, &p) // 见下面分析
...
if (err < 0) { // 出错
snd_pcm_plug_clear(pcm);
return err;
}
if (err) { // snd_pcm_plug_change_format() 新建插件 PCM 对象 @new 成功
plug->gen.slave = new; // plug 的 slave 更新为新的 linear 插件 PCM 对象
}
k++;
}
snd_pcm_plug_change_format(pcm, &new, client, &p) // ala-lib-1.2.9/src/pcm/pcm-plug.c
...
if (snd_pcm_format_linear(slv->format)) {
...
cfmt = clt->format;
switch (clt->format) {
...
default:
#ifdef BUILD_PCM_PLUGIN_LFLOAT
if (snd_pcm_format_float(clt->format))
f = snd_pcm_lfloat_open;
else
#endif
f = snd_pcm_linear_open; // plug 和 其当前 slave 格式不兼容,需做线性转换
}
} else if (snd_pcm_format_float(slv->format)) {
...
} else {
...
}
err = f(new, NULL, slv->format, plug->gen.slave, plug->gen.slave != plug->req_slave);
snd_pcm_linear_open(new, NULL, slv->format, plug->gen.slave, plug->gen.slave != plug->req_slave) // 见后面分析
...
slv->format = cfmt;
slv->access = clt->access;
return 1;
// 新建 linear 插件
// alsa-lib-1.2.9/src/pcm/pcm-linear.c
snd_pcm_linear_open(new, NULL, slv->format, plug->gen.slave, plug->gen.slave != plug->req_slave)
snd_pcm_t *pcm;
snd_pcm_linear_t *linear;
...
linear = calloc(1, sizeof(snd_pcm_linear_t));
...
snd_pcm_plugin_init(&linear->plug);
linear->sformat = sformat;
linear->plug.read = snd_pcm_linear_read_areas;
linear->plug.write = snd_pcm_linear_write_areas;
linear->plug.undo_read = snd_pcm_plugin_undo_read_generic;
linear->plug.undo_write = snd_pcm_plugin_undo_write_generic;
linear->plug.gen.slave = slave; // 新的 linear 插件的 slave 设为 plug 当前的 slave (即声卡设备)
linear->plug.gen.close_slave = close_slave;
// 创建新的插件 PCM 对象
err = snd_pcm_new(&pcm, SND_PCM_TYPE_LINEAR, name, slave->stream, slave->mode);
...
// 设置插件 接口
pcm->ops = &snd_pcm_linear_ops;
pcm->fast_ops = &snd_pcm_plugin_fast_ops;
pcm->private_data = linear;
...
*pcmp = pcm; // 返回新建的 linear 插件 PCM 对象
return 0;
到此,参数设置完毕。接下来看数据经插件处理,并最终流向声卡设备的过程:
// 写数据到声卡设备:数据先流经各插件处理,最终到达声卡设备
pcm_write(audiobuf + written, chunk_size)
...
while (count > 0 && !in_aborting) {
...
r = writei_func(handle, data, count) = snd_pcm_writei() // alsa-lib-1.2.9/src/pcm/pcm.c
_snd_pcm_writei(pcm, buffer, size)
// alsa-lib-1.2.9/src/pcm/pcm-plug.c
// 首先是 plug 插件对数据进行处理
pcm->fast_ops->writei(pcm->fast_op_arg, buffer, size) = snd_pcm_plugin_writei()
...
}
...
// alsa-lib-1.2.9/src/pcm/pcm-plug.c
// 首先是 plug 插件对数据进行处理
snd_pcm_plugin_writei(pcm->fast_op_arg, buffer, size)
snd_pcm_channel_area_t areas[pcm->channels];
snd_pcm_areas_from_buf(pcm, areas, (void*)buffer);
return snd_pcm_write_areas(pcm, areas, 0, size, snd_pcm_plugin_write_areas); // alsa-lib-1.2.9/src/pcm/pcm.c
while (size > 0) {
snd_pcm_uframes_t frames;
snd_pcm_sframes_t avail;
...
avail = __snd_pcm_avail_update(pcm);
...
frames = size;
if (frames > (snd_pcm_uframes_t) avail)
frames = avail;
if (! frames) // 本次数据处理播放完毕
break;
err = func(pcm, areas, offset, frames)
snd_pcm_plugin_write_areas(pcm, areas, offset, frames) // 见后续
...
offset += frames;
size -= frames;
xfer += frames;
}
// alsa-lib-1.2.9/src/pcm/pcm-plug.c
snd_pcm_plugin_write_areas(pcm, areas, offset, frames)
snd_pcm_plugin_t *plugin = pcm->private_data;
snd_pcm_t *slave = plugin->gen.slave; // linear 插件的 PCM 对象
snd_pcm_uframes_t xfer = 0;
snd_pcm_sframes_t result;
...
/*
* 1. 数据先经插件 @plugin 处理
* 2. 再将经插件 @plugin 处理过的数据, 丢给插件 @plug_in 的 @slave 继续处理
* 重复 1, 2 直到 @slave 不再有 slave 为止.
* 如果是播放, 通常是数据到达了硬件.
*/
while (size > 0) {
snd_pcm_uframes_t frames = size;
const snd_pcm_channel_area_t *slave_areas;
snd_pcm_uframes_t slave_offset;
snd_pcm_uframes_t slave_frames = ULONG_MAX;
result = snd_pcm_mmap_begin(slave, &slave_areas, &slave_offset, &slave_frames);
...
if (slave_frames == 0)
break;
/* 1. 数据先经插件 @plugin 处理: @areas => @slave_areas */
frames = plugin->write(pcm, areas, offset, frames,
slave_areas, slave_offset, &slave_frames);
snd_pcm_linear_write_areas()
...
/* 2. 再将经插件 @plugin 处理过的数据, 丢给插件 @plug_in 的 @slave 继续处理 */
result = snd_pcm_mmap_commit(slave, slave_offset, slave_frames);
...
snd_pcm_mmap_appl_forward(pcm, frames);
offset += frames;
xfer += frames;
size -= frames;
}
return (snd_pcm_sframes_t)xfer; // 返回已经播放的帧数
...
// 数据先经 liear 处理
snd_pcm_linear_write_areas() // alsa-lib-1.2.9/src/pcm/pcm-linear.c
snd_pcm_linear_t *linear = pcm->private_data;
...
if (linear->use_getput)
...
else
/* 做数据转换(@slave_areas <- @areas), 如 S16_LE -> S32_LE */
snd_pcm_linear_convert(slave_areas, slave_offset,
areas, offset, pcm->channels, size, linear->conv_idx);
*slave_sizep = size;
return size;
// 经 liear 处理后的数据,提交给声卡圣杯
result = snd_pcm_mmap_commit(slave, slave_offset, slave_frames);
result = __snd_pcm_mmap_commit(pcm, offset, frames);
if (pcm->fast_ops->mmap_commit)
return pcm->fast_ops->mmap_commit(pcm->fast_op_arg, offset, frames); /* snd_pcm_plugin_mmap_commit() */
else
return -ENOSYS;
snd_pcm_plugin_mmap_commit() // alsa-lib-1.2.9/src/pcm/pcm-plugin.c
snd_pcm_plugin_t *plugin = pcm->private_data; /* 当前级插件 */
snd_pcm_t *slave = plugin->gen.slave; /* 当前级插件的下一级 slave (我们的场景是声卡设备) */
...
// 1. 数据经当前级插件 @plugin 处理
// 2. 将经当前级插件 @plugin 处理后的数据, 转发给
// 当前级插件 @plugin 的 @slave 处理
// 重复 1, 2 直到再没有 slave 为止.
while (size > 0 && slave_size > 0) {
...
// 1. 数据经当前级插件 @plugin 处理
frames = plugin->write(pcm, areas, appl_offset, frames,
slave_areas, slave_offset, &slave_frames); /* snd_pcm_hw_writei() */
// 2. 将经当前级插件 @plugin 处理后的数据, 转发给
// 当前级插件 @plugin 的 @slave 处理
result = snd_pcm_mmap_commit(slave, slave_offset, slave_frames); // 我们的场景,不再有下一级的 slave
...
}
snd_pcm_hw_writei() // alsa-lib-1.2.9/src/pcm/pcm-hw.c
...
struct snd_xferi xferi;
xferi.buf = (char*) buffer;
xferi.frames = size;
xferi.result = 0; /* make valgrind happy */
if (ioctl(fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &xferi) < 0) // 将数据写入声卡设备
err = -errno;
else
err = query_status_and_control_data(hw);
...
到此,数据终于到达声卡设备。前面我们分析的是播放流程中,插件对数据的处理过程。事实上,在录音过程中,插件对数据的处理类似,只不过方向与播放流程正好相反:
内核空间 | 用户空间
Mic -> CODEC -> I2S RX FIFO -> DMA Buffer -|-> alsa-lib 插件 -> 处理后的最终数据
5.1.3 alsa-lib 内置插件代码组织
alsa-lib 插件代码组织在目录 alsa-lib-1.2.9/src/pcm
下,命名为 pcm_插件名.c
:
alsa-lib-1.2.9/src/pcm/pcm_adpcm.c // adpcm 插件
alsa-lib-1.2.9/src/pcm/pcm_alaw.c // alaw 插件
...
alsa-lib-1.2.9/src/pcm/pcm_dmix.c // dmix 插件
...
alsa-lib-1.2.9/src/pcm/pcm_plug.c // 通用 plug 插件 (本文示例所用插件)
...
alsa-lib-1.2.9/src/pcm/pcm_rate.c // rate 插件
...
alsa-lib-1.2.9/src/pcm/pcm_softvol.c // softvol 插件
各类型插件的功能可参考 ALSA 官方链接:https://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html 。
5.1.4 自定义 alsa-lib 插件
假设我们要自定义一名为 test
的插件,从前面的代码分析中知道(细节见前面对 snd_pcm_open_noupdate()
的分析):
o 该插件必须编译成名为 `libasound_module_pcm_test.so` 的共享库文件。
o 该插件必须包含一个名为 `_snd_pcm_test_open()` 的接口,且该接口完成
为插件接卸 slave 配置、创建 slave 以及自身 PCM 对象、绑定操作接口等
功能。
o 该插件实现本身功能、以及 fast_ops, ops 等接口。
使用该插件时,在定义中用 type test
关联插件配置和插件功能。
5.2 使用 alsa-lib API 编程
snd_pcm_t *handle;
snd_pcm_hw_params_t *hw_params;
// 加载解析 alsa 配置,并创建声卡 PCM 对象
snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
// 声卡参数配置
snd_pcm_hw_params_malloc(&hw_params);
snd_pcm_hw_params_any(handle, hw_params);
snd_pcm_hw_params_set_access(handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(handle, hw_params, pcm_format);
snd_pcm_hw_params_set_channels(handle, hw_params, 2);
snd_pcm_hw_params_set_rate_near(handle, hw_params, &val, &dir);
snd_pcm_hw_params_set_buffer_size_near(handle, hw_params, &period_size);
snd_pcm_hw_params_set_period_size_near(handle, hw_params, &period_size, 0);
snd_pcm_hw_params(handle, hw_params);
// 播放
snd_pcm_hw_params_get_period_size(hw_params, &frames, &dir);
snd_pcm_writei(handle, buffer, frames);
工作细节和前面使用 aplay
播放类型。
5.3 为 ARM 交叉编译 alsa-lib 和 alsa-utils
# 交叉编译 alsa-lib ,生成的文件位于 _install 目录。
#
# 完成后需要同时拷贝 libasound.so.* 和 *.conf 到目录平台。
# so 和 .conf 应该来自同一份源码,不同版本源码的生成 .conf 是不同的。
# .so 可拷贝到默认的系统库目录,而 .conf 默认位于 /usr/share/alsa 目录,
# 使用不同的配置目录,可在编译时指定,或通过环境变量 ALSA_CONFIG_PATH 指定。
CC=arm-linux-gnueabihf-gcc \
./configure --host=arm-linux-gnueabihf \
--prefix=$PWD/_install
make -j8
make install
# 将 alsa-utils 源码和库代码放在同一级目录下,然后进入 alsa-utils 源码目录编译。
CC=arm-linux-gnueabihf-gcc \
./configure --prefix=$PWD/_install \
--host=arm-linux-gnueabihf \
--with-alsa-inc-prefix=$PWD/../alsa-lib-1.2.9/_install/include \
--with-alsa-prefix=$PWD/../alsa-lib-1.2.9/_install/lib \
--disable-alsamixer --disable-xmlto --disable-nls
make -j8
make install
5.4 alsa-lib 配置
alsa-lib
配置的组织大概如下:
/usr/share/alsa/alsa.conf
[/alsa.conf.d/]
[/etc/asound.conf]
[~/.asoundrc]
[/cards/aliases.conf]
[/cards/.conf]
/usr/share/alsa/alsa.conf
绝大多数情形下都不应该被修改,用户通常是自定义配置文件 /etc/asound.conf
。
6. 参考资料
https://www.codenong.com/cs106472281/
https://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html