Linux: alsa-lib 插件简介

文章目录

  • 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 框架图
上图是 Linux ALSA 框架概览,包括 用户空间内核空间 的各个组成部分。ALSAAdvanced Linux Sound Architecture 的缩写。ASoCALSA System on Chip 的缩写,是针对片上系统引入的中间层:为了适应 PlatformCodec 硬件上的分离,对基础 ALSA 基础框架实现进行了解耦。
本文重点关注 用户空间 红色框选中的 alsa-lib 部分。

4. alsa 声卡设备

对声卡的操作,是通过 ALSA CORE 向用户空间导出的、声卡相关的字符设备节点来完成:

$ ls -l /dev/snd/
total 0
drwxr-xr-x  2 root root       60 107 09:12 by-path
crw-rw----+ 1 root audio 116,  2 107 09:12 controlC0
crw-rw----+ 1 root audio 116,  6 107 09:12 midiC0D0
crw-rw----+ 1 root audio 116,  4 107 09:13 pcmC0D0c
crw-rw----+ 1 root audio 116,  3 107 09:13 pcmC0D0p
crw-rw----+ 1 root audio 116,  5 107 09:12 pcmC0D1p
crw-rw----+ 1 root audio 116,  1 107 09:12 seq
crw-rw----+ 1 root audio 116, 33 107 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 spaceDMA Buffer 之间。简单来讲,alsa-lib 插件 的作用就是:在进入内核空间将数据拷贝到 DMA buffer 之前,通过插件定义的行为(算法),对用户空间的原始数据进行一到多次(对应一到多个插件)加工,然后再拷贝到 DMA buffer。我们对上图稍作修改,来描述 alsa-lib 插件 在整个播放流程中扮演的角色:
在这里插入图片描述上图中红色框中的部分,每个 alsa-lib 插件 通过预定义的行为(算法),对输入数据进行处理后,输出给下一个插件。
到此,我们对插件的工作原理已经有了初步的了解。接下来看如何使用 alsa-lib 插件 来自定义对用户空间播放原始数据的处理。假设有一个 S16_LE,48KHz, 2通道 的音频文件 test.wav ,要在只支持 S32_LE,48KHz, 2通道 的声卡上播放,这样就需要将 S16_LEtest.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_LElinear 插件。

// 参数设置。
// 过程中,会添加一个用来将 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/153017.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js中的原型链

编写思路&#xff1a; 简单介绍构造函数介绍原型对象原型对象、实例的关系&#xff0c;从而引出原型链的基本概念 原型链基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。 1. 什么是构造函数 构造函数本身跟普通函数一样&#xff0c;也不存在定义构造函数…

图神经网络 GNN

之前经常看到图神经网络的内容&#xff0c;但是一直都觉得很难&#xff0c;就没有继续了解&#xff0c;现在抽空学习了一下&#xff0c;简单了解GNN是个什么东西&#xff0c;还没有进行代码实践&#xff0c;随着后续的学习&#xff0c;会继续更新代码的内容&#xff0c;这里先记…

Linux动态链接库.so文件

一、动态库和静态库的区别 库是一个二进制文件&#xff0c;包含的代码可以被程序调用&#xff0c;如标准库、线程库。Windows 和 Linux下的库文件格式不兼容。 Windows环境&#xff1a;静态库是 .lib 文件&#xff0c;共享库是 .dll 文件 Linux环境&#xff1a;静态库是 .a 文…

数据结构与算法(八):排序算法

参考引用 Hello 算法 Github&#xff1a;hello-algo 1. 选择排序 选择排序的工作原理非常直接&#xff1a;开启一个循环&#xff0c;每轮从未排序区间选择最小的元素&#xff0c;将其放到已排序区间的末尾&#xff0c;设数组的长度为 n 初始状态下&#xff0c;所有元素未排序&…

HTTP协议的请求协议和响应协议的组成,HTTP常见的状态信息

HTTP协议 什么是协议 协议实际上是某些人或组织提前制定好的一套规范,大家只要都按照这个规范来就可以做到沟通无障碍 HTTP协议是W3C(万维网联盟组织)制定的一种超文本传输通信协议(发送消息的模板和数据的格式),除了传送字符串,还有声音、视频、图片等流媒体等超文本信息 …

伦敦银最新走势不利怎么办

跟其他的投资品种一样&#xff0c;伦敦银的价格走势在不停的变化&#xff0c;而且由于本身产品具有较高的资金杠杆&#xff0c;所以万一行情走势变得不利&#xff0c;在很短的时间之内就会对投资者的账户造成严重损失&#xff0c;所以投资者应该对此作好充分的准备。 伦敦银的最…

LabVIEW利用以太网开发智能液位检测仪

LabVIEW利用以太网开发智能液位检测仪 目前&#xff0c;工业以太网接口在国内外的发展已经达到了相当深入的程度&#xff0c;特别是在自动化控制和工业控制领域有着非常广泛的应用。在工业生产过程中&#xff0c;钢厂的连铸机是前后的连接环节&#xff0c;其中钢水从大钢包进入…

Spring Boot如何配置CORS支持

Spring Boot如何配置CORS支持 CORS&#xff08;跨源资源共享&#xff09;是一种Web浏览器的安全性功能&#xff0c;用于控制网页上的脚本文件从不同的源加载其他网页资源。在开发现代Web应用程序时&#xff0c;通常需要跨域请求不同的资源&#xff0c;如API服务或其他Web应用程…

一个tomcat下如何部署多个项目?

1、不修改端口&#xff0c;部署多个项目 清楚tomcat目录结构的应该都知道&#xff0c;项目包是放在webapps目录下的&#xff0c;那能否在同一个tomcat的webapps目录下运行多个不同项目呢&#xff1f; 答案是可以的。 1、将多个项目包放入webapps文件夹下 2、修改conf下的serv…

reactjs开发环境搭建

Reactjs是一个前端web页面应用开发框架工具集&#xff0c;其支持前端构建页面以及后端构建页面两种常用的开发场景&#xff0c;其中&#xff0c;支持reactjs的开发框架包括next.js、remix、gatsby以及其他&#xff0c;本文主要描述next.js开发环境的搭建&#xff0c;next.js是一…

Verilog HDL阻塞赋值和非阻塞赋值笔记

1. module test( input wire clk, input wire b, output reg a, output reg c ); always(posedge clk) begin ab; ca; end endmodule 上面的代码在vivado中综合后的电路为&#xff1a; 2. module test( input wire clk, input wire b, outp…

springcloud之项目实战环境准备

写在前面 为了更好的学习springcloud&#xff0c;我们来一起开发一个实战项目&#xff0c;加深理解。 1&#xff1a;项目介绍 在开始项目实战之前先来做一个整体的项目介绍&#xff0c;从而能够让对项目的整体架构和模板有一个比较清晰的认知。 大家都知道双11&#xff0c;…

JS进阶-原型

原型 原型就是一个对象&#xff0c;也称为原型对象 构造函数通过原型分配的函数是所有对象所共享的 JavaScript规定&#xff0c;每一个构造函数都有一个prototype属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象 这个对象可以挂载函数&#xff0c;对象实…

64.最小路径和

法&#xff1a;动态规划 第一行的元素&#xff0c;只有通过左侧右移才能到达&#xff1b;第一列的元素&#xff0c;只有通过上方的下移才能到达。其他位置元素&#xff1a;比较从上方元素向下移动的路径和和左侧元素向右移动的路径和的较小值dp[i][j]&#xff1a;到达(i,j)位置…

中国移动咪咕、阿里云、华为“秀肌肉”,这届亚运会的“高光”不止比赛

文 | 智能相对论 作者 | 青月 竞技体育的发展&#xff0c;其实也可以看作是一部“技术进化史”。 在1924年的巴黎&#xff0c;广播首次进入奥运会&#xff0c;人们第一次可以通过报纸以外的方式了解奥运会。 1928年&#xff0c;在荷兰申办的阿姆斯特丹奥运会&#xff0c;高…

【jvm--方法区】

文章目录 1. 栈、堆、方法区的交互关系2. 方法区的内部结构3. 运行时常量池4. 方法区的演进细节5. 方法区的垃圾回收 1. 栈、堆、方法区的交互关系 方法区的基本理解&#xff1a; 方法区&#xff08;Method Area&#xff09;与 Java 堆一样&#xff0c;是各个线程共享的内存区…

【力扣】单调栈:901. 股票价格跨度

【力扣】单调栈&#xff1a;901. 股票价格跨度 文章目录 【力扣】单调栈&#xff1a;901. 股票价格跨度1. 题目介绍2. 思路3. 解题代码参考 1. 题目介绍 设计一个算法收集某些股票的每日报价&#xff0c;并返回该股票当日价格的 跨度 。 当日股票价格的 跨度 被定义为股票价格…

75.颜色分类

原地排序&#xff1a;空间复杂度为1 class Solution { public:void sortColors(vector<int>& nums) {if(0){//法一&#xff1a;单指针两个遍历int nnums.size();int ptr0;for(int i0;i<n;i){if(nums[i]0){swap(nums[i],nums[ptr]);ptr;}}for(int iptr;i<n;i){…

【Proteus仿真】【STM32单片机】汽车倒车报警系统设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真STM32单片机控制器&#xff0c;使用LCD1602液晶、按键、继电器电机模块、DS18B20温度传感器、蜂鸣器LED、HCSR04超声波等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显…

2021年03月 Python(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python编程&#xff08;1~6级&#xff09;全部真题・点这里 一、单选题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 第1题 对于字典infor {“name”:“tom”, “age”:13, “sex”:“male”}&#xff0c;删除"age":13键值对的操作正确的…