• 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
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    对上面输出的设备节点,只挑我们关注的几个进行说明。/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);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    我们用下图来描述播放音频时的数据走向:
    在这里插入图片描述

    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
    	}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面的 "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 => ......
      • 1

      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); /* 删除配置文件对象 */
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105

      来看具体插件的 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);
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56

      看看 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; // 返回解析的配置对象
      	...
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73

      这里不仔细分析 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;
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      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;
      				}
      		...
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49

      上面的代码分析给出了 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;
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124

      到此,参数设置完毕。接下来看数据经插件处理,并最终流向声卡设备的过程:

      // 写数据到声卡设备:数据先流经各插件处理,最终到达声卡设备
      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);
      	...
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131

      到此,数据终于到达声卡设备。前面我们分析的是播放流程中,插件对数据的处理过程。事实上,在录音过程中,插件对数据的处理类似,只不过方向与播放流程正好相反:

                       内核空间                   |             用户空间
      Mic -> CODEC -> I2S RX FIFO -> DMA Buffer -|-> alsa-lib 插件 -> 处理后的最终数据
      • 1

      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 插件
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      各类型插件的功能可参考 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 等接口。
      • 1
      • 2
      • 3
      • 4

      使用该插件时,在定义中用 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);
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

      工作细节和前面使用 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
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      # 将 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
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      5.4 alsa-lib 配置

      alsa-lib 配置的组织大概如下:

      /usr/share/alsa/alsa.conf
       	[/alsa.conf.d/]
      	[/etc/asound.conf]
      	[~/.asoundrc]
      	[/cards/aliases.conf]
      	[/cards/.conf]
      • 1
      • 2
      • 3
      • 4
      • 5

      /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

    • 相关阅读:
      【C语言进阶】动态内存管理
      mysql数据库连接后缀的作用
      Python中向量的表示
      ElasticSearch实操入门(四)
      GaussDB数据库SQL系列-定义重载函数
      22-07-05 七牛云存储图片、用户头像上传
      六分科技CEO李阳:精准定位助力汽车智能化普及
      LeetCode 0155. 最小栈
      配电网重构|基于新颖的启发式算法SOE的随机(SDNR)配电网重构(Matlab代码实现)【算例33节点、84节点、119节点、136节点、417节点】
      对于程序员来说,怎样才算是在写有“技术含量”的代码?
    • 原文地址:https://blog.csdn.net/JiMoKuangXiangQu/article/details/133013698