Linux: alsa-lib 插件简介

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 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_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.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

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux ALSA-Lib库是用于读取和处理音频的开源库。它提供了一套API,可以让开发者通过 C/C++ 编程语言访问 Linux 系统中的音频设备。 ALSA-Lib 可以实现多种音频设备的读写,包括内置音频硬件,外部 USB 音频设备以及蓝牙音频。 ALSA-Lib 提供了一个叫做alsa-lib.h的头文件,这个头文件包含了常用的 ALSA-Lib API 函数。开发者可以根据具体需求来选择合适的函数,最常用的是snd_pcm_open()、snd_pcm_hw_params_set_xxx()、snd_pcm_writei()和snd_pcm_close(),这些函数分别用于打开、设置参数、写数据和关闭音频设备。 ALSA-Lib 提供的多种API函数使得开发者可以对音频进行多种高级操作。比如,开发者可以通过snd_pcm_drop()中止当前播放操作,通过snd_pcm_pause()暂停播放,通过snd_pcm_prepare()准备播放,还可以通过调用snd_pcm_avail_update()获取当前音频设备的缓冲区状态。 读取音频数据可以通过snd_pcm_readi()函数实现,这个函数会一次性从设备中读取指定数量的音频采样,并将其存储在一个指定的缓冲区中。开发者还可以选择使用snd_pcm_mmap_readi()和snd_pcm_mmap_begin()来读取音频采样,这两个函数可以实现更高效的读取。 在开发 Linux 音频应用程序时,ALSA-Lib 是非常重要的组件。通过掌握 ALSA-Lib 的 API 函数,开发者可以实现快速、高效地读取和处理音频数据。因此,熟悉 ALSA-LibLinux 音频开发工程师的必备技能之一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值