文章目录
- 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 plugslave {pcm "hw:0,0"format S32_LEchannels 2rate 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.cchar *pcm_name = "default"; // 缺省的播放设备,通常 "default" 代表 "hw:0,0"...// 解析命令行参数 -D plug:s16le_s32_lewhile ((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.csnd_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.sosprintf(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 的名称,如 slavecontinue;...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 @spcmerr = 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.cSND_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,0goto _err;continue;}for (k = 0; k < count; ++k) {// SND_PCM_HW_PARAM_FORMAT, SND_PCM_HW_PARAM_CHANNELS, SND_PCM_HW_PARAM_RATEunsigned int idx = fields[k].index;...if (strcmp(id, names[idx]) != 0)continue;switch (idx) { // format S32_LEcase 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_LEbreak;}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 @spcmerr = snd_pcm_plug_open(pcmp, name, sformat, schannels, srate, rate_converter, // alsa-lib-1.2.9/src/pcm/pcm_plug.croute_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)// 播放 WAVEpbrec_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) // 见下面分析elseerr = -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.csnd_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.csnd_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_LFLOATif (snd_pcm_format_float(clt->format))f = snd_pcm_lfloat_open;else#endiff = 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.cwhile (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.csnd_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() */elsereturn -ENOSYS;snd_pcm_plugin_mmap_commit() // alsa-lib-1.2.9/src/pcm/pcm-plugin.csnd_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;elseerr = 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