【Android Audio 入门 二】--- /dev/snd下的pcm节点 创建 及 open 过程代码分析


接着前面我们写的文章《【Android Audio 入门 一】— Audio ALSA Driver

关于Audio 发现一个官方的网站 https://www.kernel.org/doc/html/v4.10/sound/soc/index.html ,英文好的兄弟可以参考下。


二、audio 节点 介绍

1. /dev/snd下的pcm设备节点介绍

我们 adb shell, 进入手机中,ls -al /dev/snd 看下,可以看到很多设备节点。
简化如下:

$ cd /dev/snd 
$ ls –l 
crw-rw----+ 1 root audio 116, 8 2011-02-23 21:38 controlC0  ---> 用于声卡的控制,例如通道选择,混音,麦克风的控制等 
crw-rw----+ 1 root audio 116, 4 2011-02-23 21:38 midiC0D0 	---> 用于播放midi 音频 
crw-rw----+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D0c 	---> 用于录音的pcm 设备 1
crw-rw----+ 1 root audio 116, 6 2011-02-23 21:56 pcmC0D0p 	---> 用于播放的pcm 设备 1
crw-rw----+ 1 root audio 116, 5 2011-02-23 21:38 pcmC0D1p 	---> 用于播放的pcm 设备 2
crw-rw----+ 1 root audio 116, 3 2011-02-23 21:38 seq 		---> 音序器 
crw-rw----+ 1 root audio 116, 2 2011-02-23 21:38 timer 		---> 定时器 

其中,
C0D0 代表的是声卡0 中的设备0,
pcmC0D0c 最后一个c 代表capture,
pcmC0D0p 最后一个p 代表 playback,
这些都是alsa-driver 中的命名规则。

2. /dev/snd下的pcm设备节点 创建过程分析

另外,还有一个发现,就,/dev/snd 下面所有的节点的主设备号 都是 116 ,面次设备号各不相同。

原因是因为,
在alsa 中所有的节点都是同一个主设备号,到时访问open 节点的时候就会先调用同一个主设备号的open 函数,
接着,在主设备号的open 函数中,再来分发调用,各个不同次设备号的open 函数。

2.1 CONFIG_SND_MAJOR 主设备号 116

代码可以参考 sound.c 中的代码:
主设备号注册

@\kernel\msm-3.18\include\sound\core.h
#define CONFIG_SND_MAJOR	116	/* standard configuration */  定义主设备号


@ \kernel\msm-3.18\sound\core\sound.c
static int major = CONFIG_SND_MAJOR; // 主设备号
module_param(major, int, 0444);
MODULE_PARM_DESC(major, "Major # for sound driver.");

static const struct file_operations snd_fops =
{
	.owner =	THIS_MODULE,
	.open =		snd_open,
	.llseek =	noop_llseek,
};

static int __init alsa_sound_init(void)
{
	snd_major = major;
	snd_ecards_limit = cards_limit;
	if (register_chrdev(major, "alsa", &snd_fops)) {  // 主册一个主设备号
		pr_err("ALSA core: unable to register native major device number %d\n", major);
		return -EIO;
	}

	snd_info_minor_register();
	return 0;
}

2.2 snd_minors 数组分析

在 snd_register_device_for_dev 函数中,主要作用就是创建不同的次设备号节点,保存在 snd_minors[] 数组中。

其主要是在pcm.c 创建节点时被调用的。
可以发现在代码中,会根据声卡号和设备索引号,依次创建 pcmC%iD%ip 和 pcmC%iD%ic 两个设备节点的名字。
接着,调用 snd_register_device_for_dev 来创建设备节点,传入换参数就是 设备节点的名字。

@\kernel\msm-3.18\sound\core\pcm.c

static int snd_pcm_dev_register(struct snd_device *device)
{
	pcm = device->device_data;
	err = snd_pcm_add(pcm);
	
	for (cidx = 0; cidx < 2; cidx++) {
		int devtype = -1;
		switch (cidx) {
		case SNDRV_PCM_STREAM_PLAYBACK:
			sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->device);
			devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK;
			break;
		case SNDRV_PCM_STREAM_CAPTURE:
			sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->device);
			devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE;
			break;
		}
		/* device pointer to use, pcm->dev takes precedence if
		 * it is assigned, otherwise fall back to card's device
		 * if possible */
		dev = pcm->dev;

		/* register pcm */
		err = snd_register_device_for_dev(devtype, pcm->card,
						  pcm->device,
						  &snd_pcm_f_ops[cidx],
						  pcm, str, dev);

		dev = snd_get_device(devtype, pcm->card, pcm->device);
		if (dev) {
			err = sysfs_create_groups(&dev->kobj,
						  pcm_dev_attr_groups);
			put_device(dev);
		}

		for (substream = pcm->streams[cidx].substream; substream; substream = substream->next)
			snd_pcm_timer_init(substream);
	}

	list_for_each_entry(notify, &snd_pcm_notify_list, list)
		notify->n_register(pcm);

	return 0;
}

在snd_register_device_for_dev() 中,会根据传入的字符串名字,创建不同的设备节点。
看 snd_register_device_for_dev() 代码前,我们来看一下snd_minors[] 这个数组。

static struct snd_minor *snd_minors[SNDRV_OS_MINORS];

其结构体描述如下:
@ \kernel\msm-3.18\include\sound\core.h

struct snd_minor {
	int type;			/* SNDRV_DEVICE_TYPE_XXX */  
		// 声卡类型: SNDRV_DEVICE_TYPE_PCM_PLAYBACK 和  SNDRV_DEVICE_TYPE_PCM_CAPTURE 两种
		
	int card;			/* card number */ 							//声卡号
	int device;			/* device number */							// 设备号
	const struct file_operations *f_ops;	/* file operations */ 	// 该节点换操作节构体
	void *private_data;		/* private data for f_ops->open */		// 私有参数
	struct device *dev;		/* device for sysfs */					// sys 设备节点描述符
	struct snd_card *card_ptr;	/* assigned card instance */		//声卡结构体
};

从上面的结构体可以看出,snd_minors中 主要是包含了 card声卡下 device设备的操作方法 f_ops。
这样就很清楚了。

通过snd_minors[] 这个 数组,我人能够找到任意一个 声卡下的设备 的操作方法。

2.3 pcm设备节点创建代码

接下来,我们来分析snd_register_device_for_dev() 这个函数,

这个函数主要工作 如下:
step 1. 使用 snd_minor 指针将 要创建的声卡设备的信息保存下来
step 2. 给声卡设备分配 次设备号,如果定义了动态分配,则分配次设备号
step 3. 以次设备号为索引,将声卡设备的信息保存在 snd_minors[minor]数组中。
step 4. 通过 device_create 创建一个 主设备号 majore=116, 次设备号minor 的设备节点,节点名字就是字符串 pcmC%iD%ip 或 pcmC%iD%ic


@ \kernel\msm-3.18\sound\core\sound.c
/**
 * snd_register_device_for_dev - Register the ALSA device file for the card
 * @type: the device type, SNDRV_DEVICE_TYPE_XXX
 * @card: the card instance
 * @dev: the device index
 * @f_ops: the file operations
 * @private_data: user pointer for f_ops->open()
 * @name: the device file name
 * @device: the &struct device to link this new device to
 *
 * Registers an ALSA device file for the given card.
 * The operators have to be set in reg parameter.
 *
 * Return: Zero if successful, or a negative error code on failure.
 */
int snd_register_device_for_dev(int type, struct snd_card *card, int dev,
				const struct file_operations *f_ops,
				void *private_data,
				const char *name, struct device *device)
{
	int minor;
	struct snd_minor *preg;
	preg = kmalloc(sizeof *preg, GFP_KERNEL);

	// step 1. 使用 snd_minor  指针将 要创建的声卡设备的信息保存下来
	preg->type = type;
	preg->card = card ? card->number : -1;
	preg->device = dev;
	preg->f_ops = f_ops;
	preg->private_data = private_data;
	preg->card_ptr = card;
	
	// step 2. 给声卡设备分配 次设备号,如果定义了动态分配,则分配次设备号
#ifdef CONFIG_SND_DYNAMIC_MINORS
	minor = snd_find_free_minor(type);
#else
	minor = snd_kernel_minor(type, card, dev);
	if (minor >= 0 && snd_minors[minor])
		minor = -EBUSY;
#endif

	// step 3. 以次设备号为索引,将声卡设备的信息保存在 snd_minors[minor]数组中。
	snd_minors[minor] = preg;
	
	// step 4. 通过 device_create 创建一个 主设备号 majore=116, 次设备号minor 的设备节点,节点名字就是字符串 pcmC%iD%ip 或 pcmC%iD%ic
	preg->dev = device_create(sound_class, device, MKDEV(major, minor),
				  private_data, "%s", name);

	return 0;
}

2.4 pcm设备节点创建open 过程分析

前面讲了pcm设备节点的创建过程,接下来我们来看下如何打开的。

先看下如下代码,在 snd_fops 文件操作节构全中,包含了 snd_open方法 。
在init 代码中,是通过 register_chrdev(major, “alsa”, &snd_fops) 来将 major=116 的主设备号 和 snd_fops绑定在一起。

也就是说,凡是打开 设备节点major 为 116 的节点时,都会调用该 snd_fop 的open方法 snd_open()。

@ \kernel\msm-3.18\sound\core\sound.c

static const struct file_operations snd_fops =
{
	.owner =	THIS_MODULE,
	.open =		snd_open,
	.llseek =	noop_llseek,
};
static int __init alsa_sound_init(void)
{
	snd_major = major;
	snd_ecards_limit = cards_limit;
	if (register_chrdev(major, "alsa", &snd_fops)) {
		pr_err("ALSA core: unable to register native major device number %d\n", major);
		return -EIO;
	}
	snd_info_minor_register();
	return 0;
}

在 snd_open() 方法中,整个过程为:
step 1:获取次设备号
step 2:初始化一个 snd_minor 类型的指针, 和file_operations 类型的操作方法指针
step 3:根据设备的次设备号,从 snd_minors[minor]数组中获取对应设备的snd_minor 结构体信息
step 4:解析出该设备的 操作方法
step 5:替换文件的操作方法
step 6:调用open 方法

@ \kernel\msm-3.18\sound\core\sound.c
static int snd_open(struct inode *inode, struct file *file)
{
	// step 1: 获取次设备号
	unsigned int minor = iminor(inode);
	// step 2:初始化一个 snd_minor 类型的指针, 和file_operations 类型的操作方法指针
	struct snd_minor *mptr = NULL;
	const struct file_operations *new_fops;

	// step 3:根据设备的次设备号,从 snd_minors[minor]数组中获取对应设备的snd_minor 结构体信息。
	mptr = snd_minors[minor];
	// step 4:解析出该设备的 操作方法
	new_fops = fops_get(mptr->f_ops);
	// step 5: 替换文件的操作方法
	replace_fops(file, new_fops);
	// step 6: 调用open 方法
	if (file->f_op->open)
		err = file->f_op->open(inode, file);
	return err;
}

2.5 pcm设备节点 file_operations 介绍

前面,我们说了pcm设备节点的 open() 方法的调用流程,
不知道你有没有好奇心,是否想进去看下它做了啥呢? 哈哈。

在前面的代码中,fops 是在 snd_pcm_f_ops[cidx] 中传递过来的。

/* register pcm */
		err = snd_register_device_for_dev(devtype, pcm->card,
						  pcm->device,
						  &snd_pcm_f_ops[cidx],
						  pcm, str, dev);

我们看下 snd_pcm_f_ops[cidx] 中的定义:

@ \kernel\msm-3.18\sound\core\pcm_native.c

const struct file_operations snd_pcm_f_ops[2] = {
	{	// SNDRV_PCM_STREAM_PLAYBACK
		.owner =		THIS_MODULE,
		.write =		snd_pcm_write,
		.aio_write =		snd_pcm_aio_write,
		.open =			snd_pcm_playback_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_playback_poll,
		.unlocked_ioctl =	snd_pcm_playback_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	},
	{	// SNDRV_PCM_STREAM_CAPTURE
		.owner =		THIS_MODULE,
		.read =			snd_pcm_read,
		.aio_read =		snd_pcm_aio_read,
		.open =			snd_pcm_capture_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_capture_poll,
		.unlocked_ioctl =	snd_pcm_capture_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	}
};

可以看出,在 snd_pcm_f_ops 数组中,主要就是定义了 playback 和 capture 的各个操作方法。

2.6 pcm设备节点 snd_pcm_playback_open() 代码分析

我们以 playback 来分析下 其open 方法: snd_pcm_playback_open()

其主要工作 为:
step 1: 通过nonseekable_open函数,告诉内核,当前文件open 时,是不可 llseek 定位的
step 2: 获得 snd_minor 结构体中的 privdata 私有数据,其中保存了声卡的相关信息
step 3: 调用 snd_pcm_open() open 函数,传参为 pcm 和 SNDRV_PCM_STREAM_CAPTURE;

static int snd_pcm_playback_open(struct inode *inode, struct file *file)
{
	struct snd_pcm *pcm;
	// step 1: 通过nonseekable_open函数,告诉内核,当前文件open 时,是不可 llseek 定位的
	int err = nonseekable_open(inode, file);
	
	// step 2: 获得 snd_minor 结构体中的 privdata 私有数据,其中保存了声卡的相关信息
	pcm = snd_lookup_minor_data(iminor(inode),
				    SNDRV_DEVICE_TYPE_PCM_PLAYBACK);
		-------->
		+	private_data = mreg->private_data;
		+	return private_data;
		<-------
	
	// step 3: 调用 snd_pcm_open() open 函数,
	err = snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_PLAYBACK);

	return err;
}

2.7 snd_pcm_open() 代码分析

主要工作如下:

  1. 将 pcm->card 和 file 添加链表
  2. 构造当前进程对应的等待队列 wait
  3. 将wait 保存在 pcm->open_wait 中
  4. 上锁
  5. 在while(1) 中打开文件,如果失败就退出
  6. 在阻塞模式下,设置SO_RCVTIMEO和SO_SNDTIMEO会导致read/write函数返回EAGAIN
    我们此返回 -EAGAIN 说明是正常的,数据还没写完
  7. 设置当前进和为可被中断
  8. 调度,让更高优先及的任务得到处理,或者让其他任务得到处理
  9. 等待调度到来,继续写播放数据
@ \kernel\msm-3.18\sound\core\pcm_native.c

static int snd_pcm_open(struct file *file, struct snd_pcm *pcm, int stream)
{
	int err;
	wait_queue_t wait;

	// 1. 将 pcm->card 和 file 添加链表
	err = snd_card_file_add(pcm->card, file);
	if (!try_module_get(pcm->card->module)) {
		err = -EFAULT;
		goto __error2;
	}
	// 2. 构造当前进程对应的等待队列 wait
	init_waitqueue_entry(&wait, current);
	// 3. 将wait 保存在 pcm->open_wait 中
	add_wait_queue(&pcm->open_wait, &wait);
	// 4. 上锁
	mutex_lock(&pcm->open_mutex);
	while (1) {
		// 5. 在while(1) 中打开文件,如果失败就退出
		err = snd_pcm_open_file(file, pcm, stream);
		if (err >= 0)
			break;
		// 6. 在阻塞模式下,设置SO_RCVTIMEO和SO_SNDTIMEO会导致read/write函数返回EAGAIN
			// 我们此返回 -EAGAIN 说明是正常的
		if (err == -EAGAIN) {
			if (file->f_flags & O_NONBLOCK) {  
				// 如果是非阻塞模式下,则直接退出,在非阻塞模式下,write或read返回-1,errno为EAGAIN,表示相应的操作还没执行完成。
				err = -EBUSY;
				break;
			}
		} else
			break;
		// 7. 设置当前进和为可被中断
		set_current_state(TASK_INTERRUPTIBLE);
		mutex_unlock(&pcm->open_mutex);
		// 8. 调度,让更高优先及的任务得到处理,或者让其他任务得到处理
		schedule();
		// 9. 等待调度到来,继续写播放数据
		mutex_lock(&pcm->open_mutex);
		if (pcm->card->shutdown) {
			err = -ENODEV;
			break;
		}
		if (signal_pending(current)) {
			err = -ERESTARTSYS;
			break;
		}
	}
	remove_wait_queue(&pcm->open_wait, &wait);
	mutex_unlock(&pcm->open_mutex);
	return err;
}

好! 至此,我们PCM 设备整个打开的过程就讲完了。
现在有点晚了,得睡觉了,明天,我们再来看capture 里面,是怎么open 的?
是否是一样呢,先保留好奇心,明天来学习,哈哈。
Date: 2019/09/18 - 23:20

2.8 pcm设备节点 snd_pcm_capture_open() 代码分析

可以看到,在 snd_pcm_capture_open 中,和前面 snd_pcm_playback_open 惟一不同的就是
传参为 SNDRV_PCM_STREAM_CAPTURE;

@ \kernel\msm-3.18\sound\core\pcm_native.c

static int snd_pcm_capture_open(struct inode *inode, struct file *file)
{
	struct snd_pcm *pcm;
	int err = nonseekable_open(inode, file);
	if (err < 0)
		return err;
	pcm = snd_lookup_minor_data(iminor(inode),
				    SNDRV_DEVICE_TYPE_PCM_CAPTURE);
	err = snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_CAPTURE);
	if (pcm)
		snd_card_unref(pcm->card);
	return err;
}

在 snd_pcm_open 中,SNDRV_PCM_STREAM_CAPTURE 是在 snd_pcm_open_file() 中被使用到的。
Date: 2019/09/18 - 23:40

2.9 pcm设备节点 snd_pcm_open_file() 代码分析

snd_pcm_open_file() 函数代码如下:

step 1: 获得 playback 或者 capture 对应的stream 流
step 2: 分配pcm_file 放在 private_data 中

@ \kernel\msm-3.18\sound\core\pcm_native.c

static int snd_pcm_open_file(struct file *file, struct snd_pcm *pcm, int stream)
{
	struct snd_pcm_file *pcm_file;
	struct snd_pcm_substream *substream;

	// step 1:  获得 playback 或者 capture 对应的stream 流
	err = snd_pcm_open_substream(pcm, stream, file, &substream);

	// step 2: 分配pcm_file 放在 private_data 中
	pcm_file = kzalloc(sizeof(*pcm_file), GFP_KERNEL);
	
	pcm_file->substream = substream;
	if (substream->ref_count == 1) {
		substream->file = pcm_file;
		substream->pcm_release = pcm_release_private;
	}
	file->private_data = pcm_file;

	return 0;
}

在说snd_pcm_open_substream() 这个函数之前我们先,我们先来分析下 pcm->streams 的 streams 类型

snd_pcm_open_substream()  中:
substream=pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream; 

streams 又是在哪定义的呢:

@kernel\msm-3.18\include\sound\pcm.h
struct snd_pcm_str streams[2];

snd_pcm_str 结构体描述如下:
struct snd_pcm_str {
	int stream;				/* stream (direction) */
	struct snd_pcm *pcm;
	/* -- substreams -- */
	unsigned int substream_count;
	unsigned int substream_opened;
	struct snd_pcm_substream *substream;

#ifdef CONFIG_SND_VERBOSE_PROCFS
	struct snd_info_entry *proc_root;
	struct snd_info_entry *proc_info_entry;
#ifdef CONFIG_SND_PCM_XRUN_DEBUG
	unsigned int xrun_debug;	/* 0 = disabled, 1 = verbose, 2 = stacktrace */
	struct snd_info_entry *proc_xrun_debug_entry;
#endif
#endif
	struct snd_kcontrol *chmap_kctl; /* channel-mapping controls */
	struct snd_kcontrol *vol_kctl; /* volume controls */
	struct snd_kcontrol *usr_kctl; /* user controls */
};

snd_pcm_open_substream函数的原型如下:

step 1: 获得 snd_pcm_substream 结构体数组操作,里面包含了 snd_pcm_str 类型的指针
step 2: 设置 snd_pcm_substream 流的数据格式配置信息
step 3: 打开stream 流
step 4: 配置 steame 流已经打开的标志位

@\src\kernel\msm-3.18\sound\core\pcm_native.c

int snd_pcm_open_substream(struct snd_pcm *pcm, int stream, struct file *file, struct snd_pcm_substream **rsubstream)
{
	struct snd_pcm_substream *substream;
	// step 1: 获得 snd_pcm_substream 结构体数组操作,里面包含了 snd_pcm_str 类型的指针
	err = snd_pcm_attach_substream(pcm, stream, file, &substream);
		------------>
		+	switch (stream) {
		+		case SNDRV_PCM_STREAM_PLAYBACK:
		+			if (pcm->info_flags & SNDRV_PCM_INFO_HALF_DUPLEX) {
		+				for (substream=pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream; substream; substream = substream->next) {
		+					if (SUBSTREAM_BUSY(substream))
		+						return -EAGAIN;
		+				}
		+			}
		+			break;
		+		case SNDRV_PCM_STREAM_CAPTURE:
		+			if (pcm->info_flags & SNDRV_PCM_INFO_HALF_DUPLEX) {
		+				for (substream=pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; substream; substream = substream->next) {
		+					if (SUBSTREAM_BUSY(substream))
		+						return -EAGAIN;
		+				}
		+			}
		+			break;
		<-----------
	// step 2: 设置 snd_pcm_substream 流的数据格式配置信息
	err = snd_pcm_hw_constraints_init(substream);

	// step 3: 打开stream 流
	if ((err = substream->ops->open(substream)) < 0)
		goto error;
	
	// step 4: 配置 steame 流已经打开的标志位
	substream->hw_opened = 1;

	err = snd_pcm_hw_constraints_complete(substream);

	*rsubstream = substream;
	return 0;

 error:
	snd_pcm_release_substream(substream);
	return err;
}



3. 如何打开一个PCM 设备

前面,我们大概讲了ALSA 的整个初始化过程,最后讲到了通过soc_new_pcm() 来创建 PCM 设备节点。
现在,问题来了,节点有了,但怎么用呢?

在 Tinyalsa 中,我们可以参考 pcm.c

注意: pcm_open 只是open file ,并初始化相关的环境,比如配置好参数。 其并不负表读写/播放数据等操作。
其主要工作 如下:

  1. 在pcm_open 函数的参数中,参数含义如下:
    card ===> 说明是第几块声卡
    device ===> 说明是这块声卡的第几个设备
    flags ===> 说明是要playback 还是capture
  2. 给 pcm 指针分配内存
  3. 保存 pcm_config 数据
  4. 根据card / device /flag 等参数,拼凑字符串,确认要打开的设备的名字
  5. 打开设备
  6. 重置为 Blocking mode 打开,阻塞模式打开
  7. 初始化 PCM 相关的参数 如 格式,采样速率,声道数等
  8. 设置硬件相关参数
  9. 配置period_size ,即每次读取 或 写入的数据大小
  10. 将 PCM 文件,映射到 mmap_buffer 中,这样,后面对pcm 读或者 写数据,只要对这个 mmap_buffer 操作就好了。
  11. 初始化,pcm 软件参数
  12. 写入 pcm 软件参数
  13. 获取 pcm 映射状态,再次确认
  14. 配置audio timestamp
@ \external\tinyalsa\pcm.c


// 1. 在pcm_open 函数的参数中,参数含义如下:
//	card  ===>  说明是第几块声卡
//	device  ===>  说明是这块声卡的第几个设备
//	flags  ===>  说明是要playback 还是capture
struct pcm *pcm_open(unsigned int card, unsigned int device, unsigned int flags, struct pcm_config *config)
{
    struct pcm *pcm;
    struct snd_pcm_info info;
    struct snd_pcm_hw_params params;
    struct snd_pcm_sw_params sparams;
    char fn[256];
    int rc;
	// 2. 给 pcm 指针分配内存
    pcm = calloc(1, sizeof(struct pcm));
	// 3. 保存 pcm_config 数据
    pcm->config = *config;
    
	// 4. 根据card / device /flag 等参数,拼凑字符串,确认要打开的设备的名字
    snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device, flags & PCM_IN ? 'c' : 'p');

    pcm->flags = flags;
	
	// 5. 打开设备 
    pcm->fd = open(fn, O_RDWR|O_NONBLOCK);

	// 6. 重置为 Blocking mode 打开,阻塞模式打开
    if (fcntl(pcm->fd, F_SETFL, fcntl(pcm->fd, F_GETFL) & ~O_NONBLOCK) < 0) {
        oops(pcm, errno, "failed to reset blocking mode '%s'", fn);
        goto fail_close;
    }
	
    pcm->subdevice = info.subdevice;

	// 7. 初始化 PCM 相关的参数 如 格式,采样速率,声道数等
    param_init(&params);
    param_set_mask(&params, SNDRV_PCM_HW_PARAM_FORMAT,pcm_format_to_alsa(config->format));
    param_set_mask(&params, SNDRV_PCM_HW_PARAM_SUBFORMAT,SNDRV_PCM_SUBFORMAT_STD);
    param_set_min(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, config->period_size);
    param_set_int(&params, SNDRV_PCM_HW_PARAM_SAMPLE_BITS,pcm_format_to_bits(config->format));
    param_set_int(&params, SNDRV_PCM_HW_PARAM_FRAME_BITS,pcm_format_to_bits(config->format) * config->channels);
    param_set_int(&params, SNDRV_PCM_HW_PARAM_CHANNELS,config->channels);
    param_set_int(&params, SNDRV_PCM_HW_PARAM_PERIODS, config->period_count);
    param_set_int(&params, SNDRV_PCM_HW_PARAM_RATE, config->rate);

    if (flags & PCM_NOIRQ) {
        if (!(flags & PCM_MMAP)) {
            oops(pcm, EINVAL, "noirq only currently supported with mmap().");
            goto fail_close;
        }

        params.flags |= SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP;
        pcm->noirq_frames_per_msec = config->rate / 1000;
    }

    if (flags & PCM_MMAP)
        param_set_mask(&params, SNDRV_PCM_HW_PARAM_ACCESS,SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
    else
        param_set_mask(&params, SNDRV_PCM_HW_PARAM_ACCESS,SNDRV_PCM_ACCESS_RW_INTERLEAVED);
	
	// 8. 设置硬件相关参数
	if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_HW_PARAMS, &params)) {
        oops(pcm, errno, "cannot set hw params");
        goto fail_close;
    }
	
    /* get our refined hw_params */
    config->period_size = param_get_int(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
    config->period_count = param_get_int(&params, SNDRV_PCM_HW_PARAM_PERIODS);
    
    // 9. 配置period_size ,即每次读取 或 写入的数据大小
    pcm->buffer_size = config->period_count * config->period_size;

	// 10. 将 PCM 文件,映射到 mmap_buffer 中,这样,后面对pcm 读或者  写数据,只要对这个 mmap_buffer 操作就好了。
    if (flags & PCM_MMAP) {
        pcm->mmap_buffer = mmap(NULL, pcm_frames_to_bytes(pcm, pcm->buffer_size),
                                PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, pcm->fd, 0);
        if (pcm->mmap_buffer == MAP_FAILED) {
            oops(pcm, errno, "failed to mmap buffer %d bytes\n",
                 pcm_frames_to_bytes(pcm, pcm->buffer_size));
            goto fail_close;
        }
    }

	// 11. 初始化,pcm 软件参数 
    memset(&sparams, 0, sizeof(sparams));
    sparams.tstamp_mode = SNDRV_PCM_TSTAMP_ENABLE;
    sparams.period_step = 1;

    if (!config->start_threshold) {
        if (pcm->flags & PCM_IN)
            pcm->config.start_threshold = sparams.start_threshold = 1;
        else
            pcm->config.start_threshold = sparams.start_threshold =
                config->period_count * config->period_size / 2;
    } else
        sparams.start_threshold = config->start_threshold;

    /* pick a high stop threshold - todo: does this need further tuning */
    if (!config->stop_threshold) {
        if (pcm->flags & PCM_IN)
            pcm->config.stop_threshold = sparams.stop_threshold =
                config->period_count * config->period_size * 10;
        else
            pcm->config.stop_threshold = sparams.stop_threshold =
                config->period_count * config->period_size;
    }
    else
        sparams.stop_threshold = config->stop_threshold;

    if (!pcm->config.avail_min) {
        if (pcm->flags & PCM_MMAP)
            pcm->config.avail_min = sparams.avail_min = pcm->config.period_size;
        else
            pcm->config.avail_min = sparams.avail_min = 1;
    } else
        sparams.avail_min = config->avail_min;

    sparams.xfer_align = config->period_size / 2; /* needed for old kernels */
    sparams.silence_threshold = config->silence_threshold;
    sparams.silence_size = config->silence_size;
    pcm->boundary = sparams.boundary = pcm->buffer_size;

    while (pcm->boundary * 2 <= INT_MAX - pcm->buffer_size)
        pcm->boundary *= 2;

	// 12. 写入 pcm 软件参数 
    if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams)) {
        oops(pcm, errno, "cannot set sw params");
        goto fail;
    }
	
	// 13. 获取 pcm 映射状态,再次确认
    rc = pcm_hw_mmap_status(pcm);
    if (rc < 0) {
        oops(pcm, errno, "mmap status failed");
        goto fail;
    }

	// 14. 配置audio timestamp
#ifdef SNDRV_PCM_IOCTL_TTSTAMP
    if (pcm->flags & PCM_MONOTONIC) {
        int arg = SNDRV_PCM_TSTAMP_TYPE_MONOTONIC;
        rc = ioctl(pcm->fd, SNDRV_PCM_IOCTL_TTSTAMP, &arg);
        if (rc < 0) {
            oops(pcm, errno, "cannot set timestamp type");
            goto fail;
        }
    }
#endif

    pcm->underruns = 0;
    return pcm;
}

现在我们写了 ,如何打开 PCM 设备,后面,我们在其他文章分析 读写流程时,再来详说,
如何播放音频(写数据),录音(读数据)。

open 流程到此完工了!!!

Date: 2019-09-19 _ 12:15



4. 阻塞 blocking mode 与非阻塞 nonblocking mode的区别

  1. 阻塞 blocking mode
    阻塞就是要等事情全部做完后,才能return 返回。

  2. 非阻塞 nonblocking mode
    非阻塞有点在后台的意思,数据你处理着,直接return 对应的错误,CPU继续往下跑,等数据全部处理完毕后,由程序再重新主动调用。


send 函数举例:
比如,调用send 函数发送较大量的Byte数接,在系统内部send做的工作就是把数据传输(Copy) 到 对应的输出缓冲区中,它执行成功,并不代表数据已经成功的发出去了(有可以还在缓冲区中等待)。
如果接收缓冲区 buff 此时并没有足够大的内存来保存 输出缓冲区中的数据时,这个时候就体现了 阻塞和非阻塞的不同之处了。

对于 阻塞模式:send 函数,将不会return 返加,它会一直等着,直到接收buff 有足够内存,并且全部接收copy 好数据后,才会 return 返回
对于非阻塞模式:如果没有足够大的接收buff,send函数会直接返回,并且返回错误:发送被阻塞,没有足够的buff,下一步你想想要怎么处理。


recv 函数举例:
调用recv 后,如果接收缓冲区没有数据的时候,看下阻塞 和 非阻塞的区别:
对于 阻塞模式:暂时挂起线程,但不return 返回,直到有接收到所要的数据后才会return 返回
对于非阻塞模式:读取数据,如果没有数据,则直接return 返回,返回错误为:当前没有数据。 意思是等有数据时,你再主动调用我一次吧。


4.1 同步

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。

4.2 异步

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以 CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是起功能已经由异步转化为同步),当一个客户端通过调用 Connect函数发出一个连接请求后,调用者线程立刻可以向下运行。当连接真正建立起来以后,socket底 层会发送一个消息通知该对象。这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供 多种选择,否则不受调用者控制。如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循 环去检查某个变量的值,这其实是一种很严重的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知 没太多区别。

4.3 阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上它们是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。

4.4 非阻塞

非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。



5. Audio timestamp时间戳分析

关于time stamp 时间戳的解释,可以参考如下两个网站,写的还不错
https://blog.csdn.net/wilson_guo/article/details/47127643
https://www.kernel.org/doc/html/v4.10/sound/designs/timestamping.html

5.1 用于音频同步

tiem stamp 最主要的作用还是用来同步:
如下:
媒体内容在播放时,最令人头痛的就是音视频不同步。从技术上来说,解决音视频同步问题的最佳方案就是时间戳:

首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);
生成数据流时依据参考时钟上的时间给每个数据块都打上时间戳(一般包括开始时间和结束时间);
在播放时,读取数据块上的时间戳,同时参考当前参考时钟上的时间来安排播放
(如果数据块的开始时间大于当前参考时钟上的时间,则不急于播放该数据块,直到参考时钟达到数据块的开始时间;
如果数据块的开始时间小于当前参考时钟上的时间,则“尽快”播放这块数据或者索性将这块数据“丢弃”,以使播放进度追上参考时钟)。

可见,避免音视频不同步现象有两个关键——一是在生成数据流时要打上正确的时间戳。如果数据块上打的时间戳本身就有问题,那么播放时再怎么调整也于事无补。

假如,
视频流内容是从0s开始的,假设10s时有人开始说话,要求配上音频流,那么音频流的起始时间应该是10s,如果时间戳从0s或其它时间开始打,则这个混合的音视频流在时间同步上本身就出了问题。


5.1 用于音频中的几个time
--------------------------------------------------------------> time
  ^               ^              ^                ^           ^
  |               |              |                |           |
 analog         link            dma              app       FullBuffer
 time           time           time              time        time
  |               |              |                |           |
  |< codec delay >|<--hw delay-->|<queued samples>|<---avail->|
  |<----------------- delay---------------------->|           |
                                 |<----ring buffer length---->|

The analog time is taken at the last stage of the playback, as close as possible to the actual transducer.

The link time is taken at the output of the SoC/chipset as the samples are pushed on a link.
The link time can be directly measured if supported in hardware by sample counters or wallclocks or indirectly estimated

The DMA time is measured using counters - typically the least reliable of all measurements due to the bursty nature of DMA transfers.

The app time corresponds to the time tracked by an application after writing in the ring buffer.

1. DMA timestamp, no compensation for DMA+analog delay
$ ./audio_time  -p --ts_type=1
playback: systime: 341121338 nsec, audio time 342000000 nsec,         systime delta -878662
playback: systime: 426236663 nsec, audio time 427187500 nsec,         systime delta -950837
playback: systime: 597080580 nsec, audio time 598000000 nsec,         systime delta -919420
playback: systime: 682059782 nsec, audio time 683020833 nsec,         systime delta -961051
playback: systime: 852896415 nsec, audio time 853854166 nsec,         systime delta -957751
playback: systime: 937903344 nsec, audio time 938854166 nsec,         systime delta -950822


2. DMA timestamp, compensation for DMA+analog delay
$ ./audio_time  -p --ts_type=1 -d
playback: systime: 341053347 nsec, audio time 341062500 nsec,         systime delta -9153
playback: systime: 426072447 nsec, audio time 426062500 nsec,         systime delta 9947
playback: systime: 596899518 nsec, audio time 596895833 nsec,         systime delta 3685
playback: systime: 681915317 nsec, audio time 681916666 nsec,         systime delta -1349
playback: systime: 852741306 nsec, audio time 852750000 nsec,         systime delta -8694


3. link timestamp, compensation for DMA+analog delay
$ ./audio_time  -p --ts_type=2 -d
playback: systime: 341060004 nsec, audio time 341062791 nsec,         systime delta -2787
playback: systime: 426242074 nsec, audio time 426244875 nsec,         systime delta -2801
playback: systime: 597080992 nsec, audio time 597084583 nsec,         systime delta -3591
playback: systime: 682084512 nsec, audio time 682088291 nsec,         systime delta -3779
playback: systime: 852936229 nsec, audio time 852940916 nsec,         systime delta -4687
playback: systime: 938107562 nsec, audio time 938112708 nsec,         systime delta -5146
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

"小夜猫&小懒虫&小财迷"的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值