Linux: alsa-lib 插件简介

news2024/11/16 12:02:06

文章目录

  • 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 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

家政服务行业做开发微信小程序可以实现什么功能

家政服务行业开发微信小程序可以实现多种功能&#xff0c;从而提升服务品质和效率&#xff0c;下面我们来详细介绍一些可能实现的功能。 一、展示服务信息 家政服务微信小程序可以展示各种服务信息&#xff0c;包括各类家政服务项目、价格、服务流程、服务人员信息等。用户可以…

Android多线程学习:线程池(二)

一、线程池运行流程 具体执行流程如下&#xff1a; 1、首先检测线程池运行状态&#xff0c;如果不是RUNNING&#xff0c;则直接拒绝&#xff0c;线程池要保证在RUNNING的状态下执行任务&#xff1b; 2、如果workerCount < corePoolSize&#xff0c;则创建并启动一个线程来…

12. Java异常及异常处理处理

Java —— 异常及处理 1. 异常2. 异常体系3. 常见Exception4. 异常处理4.1 try finally catch关键字4.2 throws和throw 自定义异常4.3 finally&#xff0c;final&#xff0c;finalize三者的区别 1. 异常 异常&#xff1a;在程序执行过程中发生的意外状况&#xff0c;可能导致程…

使用Pritunl OpenVPN远程连接,实现安全高效的远程访问

文章目录 前言1.环境安装2.开始安装3.访问测试4.创建连接5.局域网测试连接6.安装cpolar7.配置固定公网访问地址8.远程连接测试 前言 Pritunl是一款免费开源的 VPN 平台软件&#xff08;但使用的不是标准的开源许可证&#xff0c;用户受到很多限制&#xff09;。这是一种简单有…

印度网络安全:威胁与应对

随着今年过半&#xff0c;我们需要评估并了解不断崛起的网络威胁复杂性&#xff0c;这些威胁正在改变我们的数字景观。 从破坏性的网络钓鱼攻击到利用人工智能的威胁&#xff0c;印度的网络犯罪正在升级。然而&#xff0c;在高调的数据泄露事件风暴中&#xff0c;我们看到了政…

禁用Chrome自动更新

chrome浏览器会强制用户自动更新&#xff0c;每次点击关于google时&#xff0c;会自动检测更新并下载&#xff0c;非常不好 1. 进入%userprofile%\AppData\Local\Google文件夹 2. 找到其中的Update文件夹&#xff0c;右键属性-安全&#xff0c;将所有组/用户的权限设置为拒绝…

8年测试老鸟,性能测试-数据库连接池问题定位/分析,一篇打通...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、环境准备 1&a…

JProfiler14.0(Java开发分析)

JProfiler是一款专业的Java应用程序性能分析工具&#xff0c;可帮助开发人员识别和解决Java应用程序中的性能问题。JProfiler支持Java SE、Java EE和Android平台&#xff0c;提供了多种分析选项&#xff0c;包括CPU分析、内存分析和线程分析等。 使用JProfiler&#xff0c;开发…

【手写数字识别】数据挖掘实验二

文章目录 Ⅰ、项目任务要求任务描述&#xff1a;主要任务要求(必须完成以下内容但不限于这些内容)&#xff1a; II、方法思想及实现原理陈述&#xff08;20分&#xff09;算法思想和实现原理数据集描述实验运行环境描述不同方法对MNIST数据集分类识别结果分析(不同方法识别对比…

拍摄的照片怎么做二维码?一分钟在线生成二维码

​手机拍摄的照片怎么做成二维码呢&#xff1f;用二维码来查看图片的方式现在很多人都在使用&#xff0c;其优点在于不占用自身空间&#xff0c;还可以拥有更快速度让他人查看图片内容&#xff0c;常见的图片二维码类型一般有信息展示、照片展示、商品海报、表情包等等。图片二…

2023性能测试入门,其实很简单,看看这篇,好好学习

注&#xff1a;性能测试&#xff0c;入门简单&#xff0c;深入难。经常有同学问&#xff0c;建议看看这篇 。 为了帮助大家快速的入门性能测试&#xff0c;接下来文章将从以下几个方面进行展开&#xff1a; 一、赶鸭子上架要我搞性能测试&#xff0c;怎么办&#xff1f; 二、想…

最新外卖点餐小程序开源源码 支持单店+多店双模式 含完整前后端代码包和搭建教程

随着移动互联网的普及&#xff0c;外卖点餐已成为人们日常生活中不可或缺的一部分。给大家分享一个全新的外卖点餐小程序开源源码&#xff01;该程序支持单店及多店模式&#xff0c;含完整的前后端代码和详细的搭建教程&#xff0c;让您轻松开启外卖点餐业务&#xff01; 一、…

企业数字化之库存管理篇

一、前言 接上一篇 《企业数字化之采购篇》&#xff0c;这一篇我们来了解一下如何做好库存管理&#xff0c;主要还是讲销售型企业成品库存的管理&#xff0c;对于生产制造型企业库存因涉及到物料、半成品、各种消耗品、成品&#xff0c;其存在一定依赖的相关性&#xff0c;会复…

简单强大的时序图绘制工具

今天分享一个简单强大的时序图绘制工具——WaveDrom。 WaveDrom Digital Timing Diagram everywhere WaveDrom draws your Timing Diagram or Waveform from simple textual description. It comes with description language, rendering engine and the editor. WaveDrom edi…

vue-7-vuex

一、Vuex 概述 目标&#xff1a;明确Vuex是什么&#xff0c;应用场景以及优势 1.是什么 Vuex 是一个 Vue 的 状态管理工具&#xff0c;状态就是数据。 大白话&#xff1a;Vuex 是一个插件&#xff0c;可以帮我们管理 Vue 通用的数据 (多组件共享的数据)。例如&#xff1a;购…

浅谈内存函数以及模拟实现

1.memcpy void * memcpy ( void * destination, const void * source, size_t num ); 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。 这个函数在遇到 \0 的时候并不会停下来。 如果source和destination有任何的重叠&#xff0c;复制的结果都…

小白学习笔记—网络安全/黑客技术

作为一个合格的网络安全工程师&#xff0c;应该做到攻守兼备&#xff0c;毕竟知己知彼&#xff0c;才能百战百胜。 谈起黑客&#xff0c;可能各位都会想到&#xff1a;盗号&#xff0c;其实不尽然&#xff1b;黑客是一群喜爱研究技术的群体&#xff0c;在黑客圈中&#xff0c;一…

【HomeKit】HAT User Manual教程

前言&#xff1a;这篇文章是对于苹果协议文件《HomeKit Accessory Tester (HAT) User Manual》的学习&#xff0c;即 HomeKit配件测试仪(HAT) 用户手册&#xff0c;该版本是第11次修订 第一章 概述 本文档介绍了Apple HomeKit配件测试仪(HAT)的配置和使用方法。HAT是一个Mac应…

家政系统开发,家政保洁维修预约小程序开发;

家政系统是家政行业的专业管理系统软件&#xff0c;功能涉及到家政公司运营的方方面面&#xff0c;包括&#xff1a;推广、营销、管理、培训、周边服务等等&#xff1b; 家政系统功能介绍&#xff1a; 系统集成分销客户裂变、微信推广、团购引流、热文海报推广、短视频引流、搜…

Camera metadata

目录 背景 CameraMetadata基本概念 Google Metadata Google—Metadata结构 官方注释 Aandroid API cameraMetadata头部 : struct camera_metadata camera_metadata_buffer_entry struct camera_metadata_entry data区为什么是一个联合体&#xff1f; camera metadat…