正点原子linux应用编程——提高篇4

MP157开发板支持音频,板上搭载了音频编解码芯片CS42L51,支持播放以及录音功能。

ALSA概述

ALSA是Advanced Linux Sound Architecture(高级的Linux声音体系)的缩写,目前已经成为了 linux
下的主流音频体系架构,提供了音频和MIDI的支持,替代了原先旧版本中的OSS(开发声音系统);这一部分在驱动的时候,对这套驱动框架有过介绍,这里就不做赘述。

在应用层,ALSA提供了一套标准的API,应用程序只需要调用这些API就可完成对底层音频硬
件设备的控制,譬如播放、录音等,这一套API称为alsa-lib。如下图所示:

正点原子linux应用编程——提高篇4_第1张图片

alsa-lib简介

alsa-lib是一套Linux应用层的C语言函数库,为音频应用程序开发提供了一套统一、标准的接口,应用程序只需调用这一套API即可完成对底层声卡设备的操控,譬如播放与录音。

alsa-lib库支持功能比较多,提供了丰富的API接口供应用程序开发人员调用,根据函数的功能、作用
将这些API进行了分类,可通过官网上Modules按钮查看其模块划分,这里主要涉及三个模块:PCM Interface、Error Interface以及Mixer Interface

  • PCM Interface:提供了PCM设备相关的操作接口,譬如打开/关闭PCM设备、配置PCM设备硬件或软件参数、控制PCM设备(启动、暂停、恢复、写入/读取数据);
  • Error Interface:提供了关于错误处理相关的接口;
  • Mixer Interface:提供了关于混音器相关的一系列操作接口。

sound设备节点

在Linux内核设备驱动层,基于ALSA音频驱动框架注册的sound设备会在/dev/snd目录下生成相应的设备节点文件。会有如下设备文件:

  • controlC0:用于声卡控制的设备节点,譬如通道选择、混音器、麦克风的控制等,C0表示声卡0(card0);
  • pcmC0D0p:用于播放(或叫放音、回放)的PCM设备节点。其中C0表示card0,也就是声卡0;而D0表示device 0,也就是设备0;最后一个字母p是playback的缩写,表示播放;所以pcmC0D0p便是系统的声卡0中的播放设备0;
  • pcmC0D1c:用于录音的PCM设备节点。对应系统的声卡0中的录音设备1;
  • pcmC0D2c:用于录音的PCM设备节点。对应系统的声卡0中的录音设备2;
  • timer:定时器。

在Linux系统的/proc/asound目录下,有很多的
文件,这些文件记录了系统中声卡相关的信息:

  • cards:通过"cat /proc/asound/cards"命令,查看cards文件的内容,可列出系统中可用的、注册的声卡;系统中注册的所有声卡都会在/proc/asound/目录下存在一个相应的目录,该目录的命名方式为cardX(X表示声卡的编号);
  • devices:通过"cat /proc/asound/devices"命令,查看系统中所有声卡注册的设备,包括control、pcm、timer、seq等等;
  • pcm:通过"cat /proc/asound/pcm"命令,查看系统中所有PCM设备,包括playback和capture。

alsa-lib移植

除了移植alsa-lib库之外,通常还需要移植alsa-utils,alsa-utils包含了一些用于测试、配置声卡的工具。

这个在驱动教程中有所介绍,在buildroot中使能配置然后编译就可以了。而且开发板的出厂系统中是已经移植了alsa-lib和alsa-utils。

alsa-utils提供了一些用于测试、配置声卡的工具,譬如aplay、arecord、alsactl、alsaloop、alsamixer、amixer等,在开发板出厂系统上可以直接使用这些工具,这些应用程序也都是基于alsa-lib编写的。

  • aplay:用于测试音频播放功能程序,可以使用aplay播放wav格式的音频文件;aplay只能解析wav格式音频文件;
  • alsamixer:用于配置声卡的混音器,它是一个字符图形化的配置工具,直接在开发板串口终端运行alsamixer命令,就打开图形化配置界面;
  • alsactl:将当前声卡配置保存,默认文件是/var/lib/alsa/asound.state,可通过如下命令保存与加载:
alsactl -f /var/lib/alsa/asound.state store
alsactl -f /var/lib/alsa/asound.state restore
  • amixer:声卡配置工具,但是是基于命令行操作的。
  • arecord:用于录音测试的应用程序。

编写alsa-lib应用程序

基本概念

  • 样本长度(Sample):采样位数,也称为位深度,是指数字声音信号的二进制位数;
  • 声道数(channel):分为单声道和双声道;
  • 帧(frame):记录一个声音单元,长度为样本长度与声道数乘积;
  • 采样率(Sample rate):每秒采样次数;
  • 交错模式(interleaved):音频数据的记录方式,分为交错模式和非交错模式;交错模式下,数据以连续帧形式存放;非交错模式下则是先记录一个周期内所有帧左声道再记录右声道;
  • 周期(period):音频设备处理数据的单位;
  • 缓冲区(buffer):一个缓冲区包含若干周期。

数据的传输:

  • PCM播放

在播放情况下,buffer中存放了需要播放的PCM音频数据,由应用程序向buffer中写入音频数据,buffer中的音频数据由DMA传输给音频设备进行播放,所以应用程序向buffer写入数据、音频设备从buffer读取数据,这就是buffer中数据的传输情况。

这里是有读指针和写指针,在缓冲区没有数据时均指向buffer起始位置,每次读写到末尾后会回到buffer起始位置。

  • PCM录音

buffer中存放了音频设备采集到的音频数据(外界模拟声音通过ADC转为数字声音),由音频设备向buffer中写入音频数据(DMA搬运),而应用程序从buffer中读取数据,所以音频设备向buffer写入数据、应用程序从buffer读取数据。

  • Over and Under Run

当一个声卡处于工作状态时,环形缓冲区buffer中的数据总是连续地在音频设备和应用程序缓存区间传输,如下图所示:

正点原子linux应用编程——提高篇4_第2张图片

在录音例子中,如果应用程序读取数据不够快,环形缓冲区buffer中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖;这种数据的丢失被称为overrun。在播放例子中,如果应用程序写入数据到环形缓冲区buffer中的速度不够快,缓存区将会“饿死”(缓冲区中无数据可播放),这样的错误被称为underrun(欠载)在ALSA 文档中,将这两种情形统称为"XRUN",适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。

打开PCM设备

首先需要在应用程序中包含alsa-lib库的头文件,这样才能在应用程序中调用alsa-lib库函数以及使用相关宏。

第一步需要打开PCM设备,调用函数snd_pcm_open(),该函数原型如下所示:

int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode);

一共有4个参数:

  • pcmp:snd_pcm_t用于描述一个PCM设备,所以一个snd_pcm_t对象表示一个PCM设备;snd_pcm_open函数会打开参数name所指定的设备,实例化snd_pcm_t对象,并将对象的指针(也就是PCM设备的句柄)通过pcmp返回出来;
  • name:指定PCM设备名字,命名方式为"hw:i,j",i表示声卡的卡号,j则表示这块声卡上的设备号;
  • stream:指定流类型,SND_PCM_STREAM_PLAYBACK表示播放,SND_PCM_STREAM_CAPTURE则表示采集;
  • mode:指定了open模式,通常情况下会将其设置为0,表示默认打开模式,使用阻塞方式打开设备;也可将其设置为SND_PCM_NONBLOCK,表示以非阻塞方式打开设备。

设备打开成功,返回0;打开失败,返回小于0的错误编号,可以使用alsa-lib提供的库函数snd_strerror()来得到对应的错误描述信息。

函数snd_pcm_close()用于关闭PCM设备,函数原型如下所示:

int snd_pcm_close(snd_pcm_t *pcm);

设置硬件参数

主要是对采样率、声道数、格式、访问类型、period周期大小、buffer大小等进行配置。

实例化snd_pcm_hw_params_t对象

使用snd_pcm_hw_params_malloc或snd_pcm_hw_params_alloca()来实例化一个snd_pcm_hw_params_t对象,如下所示:

snd_pcm_hw_params_t *hwparams = NULL;

snd_pcm_hw_params_malloc(&hwparams);snd_pcm_hw_params_alloca(&hwparams);

snd_pcm_hw_params_free()函数用于释放snd_pcm_hw_params_t对象占用的内存空间。函数原型如下所示:

void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj);

初始化snd_pcm_hw_params_t对象

调用snd_pcm_hw_params_any()对snd_pcm_hw_params_t对象进行初始化操作,调用该函数会使用PCM设备当前的配置参数去初始化snd_pcm_hw_params_t对象,如下所示:

snd_pcm_hw_params_any(pcm_handle, hwparams);

第一个参数为PCM设备的句柄,第二个参数传入snd_pcm_hw_params_t对象的指针。

硬件参数设置

alsa-lib提供了一系列的snd_pcm_hw_params_set_xxx函数用于设置PCM设备的硬件参数,同样也提供了一系列的snd_pcm_hw_params_get_xxx函数用于获取硬件参数。

调用snd_pcm_hw_params_set_access设置访问类型,其函数原型如下所示:

int snd_pcm_hw_params_set_access(snd_pcm_t *pcm,
		snd_pcm_hw_params_t * params,
		snd_pcm_access_t access 
);

参数access指定设备的访问类型,是一个snd_pcm_access_t类型常量,通常将访问类型设置为SND_PCM_ACCESS_RW_INTERLEAVED,交错访问模式,通过snd_pcm_readi/snd_pcm_writei对PCM设备进行读/写操作。

函数调用成功返回0,失败返回小于0的错误码,可通过snd_strerror()函数获取错误信息。

调用snd_pcm_hw_params_set_format()函数设置PCM设备的数据格式,函数原型如下所示:

int snd_pcm_hw_params_set_format(snd_pcm_t *pcm,
		snd_pcm_hw_params_t *params,
		snd_pcm_format_t format
);

参数format指定数据格式,该参数是一个snd_pcm_format_t类型常量,用的最多的格式是SND_PCM_FORMAT_S16_LE,有符号16位、小端模式。当然,可能不支持这个格式,可以调用snd_pcm_hw_params_test_format()函数测试PCM设备是否支持某种格式,如下所示:

if (snd_pcm_hw_params_test_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE)) {
	// 返回一个非零值 表示不支持该格式
}
else {
	// 返回 0 表示支持
}

调用snd_pcm_hw_params_set_channels()函数设置PCM设备的声道数,函数原型如下所示:

int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm,
		snd_pcm_hw_params_t *params,
		unsigned int val
);

参数val指定声道数量,val=2表示双声道,也就是立体声。函数调用成功返回0,失败返回小于0的错误码。

调用snd_pcm_hw_params_set_rate()设置采样率大小,其函数原型如下所示:

int snd_pcm_hw_params_set_rate(snd_pcm_t *pcm,
		snd_pcm_hw_params_t *params,
		unsigned int val,
		int dir 
);

参数val指定采样率大小,譬如44100;参数dir用于控制方向,若dir=-1,则实际采样率小于参数val指定的值;dir=0表示实际采样率等于参数val;dir=1表示实际采样率大于参数val。函数调用成功返回0;失败将返回小于0的错误码。

调用snd_pcm_hw_params_set_period_size()函数设置周期大小,其函数原型如下所示:

int snd_pcm_hw_params_set_period_size(snd_pcm_t *pcm,
		snd_pcm_hw_params_t *params,
		snd_pcm_uframes_t val,
		int dir
);

alsa-lib使用snd_pcm_uframes_t类型表示帧的数量;参数dir与snd_pcm_hw_params_set_rate()函数的dir参数意义相同。

调用snd_pcm_hw_params_set_buffer_size()函数设置buffer的大小,其函数原型如下所示:

int snd_pcm_hw_params_set_buffer_size(snd_pcm_t *pcm,
		snd_pcm_hw_params_t *params,
		snd_pcm_uframes_t val
);

参数val指定buffer的大小,以帧为单位,通常buffer的大小是周期大小的整数倍;但函数snd_pcm_hw_params_set_buffer_size()是以帧为单位来表示buffer的大小,所以需要转换一下。函数调用成功返回0;失败返回一个小于0的错误码。

还可以调用snd_pcm_hw_params_set_periods()函数设置buffer大小,其函数原型如下所示:

int snd_pcm_hw_params_set_periods(snd_pcm_t *pcm,
		snd_pcm_hw_params_t *params,
		unsigned int val,
		int dir
);

参数val指定了buffer的大小,该大小以周期为单位、并不是以帧为单位,注意区分!参数dir与snd_pcm_hw_params_set_rate()函数的dir参数意义相同。函数调用成功返回0;失败将返回一个小于0的错误码。

调用snd_pcm_hw_params()加载/安装配置、将配置参数写入硬件使其生效,其函数原型如下所示:

int snd_pcm_hw_params(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);

函数调用成功返回0,失败将返回一个小于0的错误码。

函数snd_pcm_hw_params()调用之后,其内部
会自动调用snd_pcm_prepare()函数,PCM设备的状态被更改为SND_PCM_STATE_PREPARED。

读/写数据

如果是PCM播放,则调用snd_pcm_writei()函数向播放缓冲区buffer中写入音频数据;如果是PCM录音,则调用snd_pcm_readi()函数从录音缓冲区buffer中读取数据,它们的函数原型如下所示:

snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm,
		const void *buffer,
		snd_pcm_uframes_t size
);

snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm,
		void *buffer,
		snd_pcm_uframes_t size
);

参数pcm为PCM设备的句柄;参数size指定写入数据的大小,以帧为单位。通常情况下,两个函数都是读/写一个周期。

要注意的是,这里的buffer是应用程序的缓冲区,会通过函数与驱动层的环形buffer进行交互!

snd_pcm_readi/snd_pcm_writei调用成功,返回实际读取/写入的帧数;调用失败将返回一个负数错误码。即使调用成功,实际读取/写入的帧数不一定等于参数size所指定的帧数,仅当发生信号或XRUN时,返回的帧数可能会小于参数size。

调用snd_pcm_open()打开设备时,若指定为阻塞方式,则调用snd_pcm_readi/snd_pcm_writei以阻塞方
式进行读/写。若调用snd_pcm_open()打开设备时,指定为非阻塞方式,则调用snd_pcm_readi/snd_pcm_writei以非阻
塞方式进行读/写。对于PCM录音来说,当buffer缓冲区中无数据可读时,调用snd_pcm_readi()不会阻塞、而是立即以错误形式返回;同理,对于PCM播放来说,当buffer缓冲区中的数据满时,调用snd_pcm_writei()函数也不会阻塞、而是立即以错误形式返回

snd_pcm_readi/snd_pcm_writei适用于交错模式(interleaved)读/写数据,如果用户设置的访问类型并不是交错模式,而是非交错模式(non interleaved),此时便不可再使用snd_pcm_readi/snd_pcm_writei进行读写操作了,而需要使用snd_pcm_readn和snd_pcm_writen进行读写

PCM播放示例代码

首先需要定义wav文件的解析结构体,有WAV_RIFF、WAV_FMT、WAV_DATA三个结构体。

之后需要定义snd_pcm_t结构体指针pcm,定义缓冲区大小buf_bytes,缓冲区指针void指针buf,文件描述符fd,snd_pcm_uframes_t结构体period_size定义周期大小为1024帧,最后定义periods指定周期数为4。

编写snd_pcm_init函数,先定义snd_pcm_hw_params_t结构体指针hwparams,然后通过snd_pcm_open打开PCM设备,通过snd_pcm_hw_params_malloc实例化定义的hwparams,之后snd_pcm_hw_params_any来获取PCM设备当前硬件配置;之后开始设置参数,通过snd_pcm_hw_params_set_access设置为交错模式(SND_PCM_ACCESS_RW_INTERLEAVED),snd_pcm_hw_params_set_format设置数据格式为16位小端(SND_PCM_FORMAT_S16_LE),通过snd_pcm_hw_params_set_rate设置采样率为wav_fmt.SampleRate,之后通过snd_pcm_hw_params_set_channels为wav_fmt.NumChannels双声道,然后通过snd_pcm_hw_params_set_period_size设置周期为period_size,通过snd_pcm_hw_params_set_periods设置周期数,也就是驱动层buffer为periods,最后snd_pcm_hw_params使能配置,snd_pcm_hw_params_free释放hwparams配置信息,并设置buf_bytes为period_size*wav_fmt.BlockAlign。

编写open_wav_file函数,这里实例化wav相关结构体,然后open打开设备文件,并通过read读取wav_riff,通过strncmp来校验wav_riff.ChunkID是否为4,通过校验后read来读取wav_fmt,再次校验wav_fmt.Subchunk1ID是否为4;最后把wav_fmt的相关信息全部printf出来;之后通过lseek移动指针到sub-chunk-data,然后while判断read读的size是不是DATA_t的大小,为真则进入strncmp校验wav_data.Subchunk2ID是否为4,就return,如果不是那么lseek移动SEEK_CUR。(这个函数就是用来解析wav格式音频的,当成固有格式记录一下就好)。

main函数中,调用open_wav_file打开wav文件,然后snd_pcm_init初始化PCM设备,malloc为buf申请内存,然后进入for死循环播放:通过memset给buf清零,然后read打开音频,通过snd_pcm_writei写入数据,此时如果返回的ret

测试时,可以通过amixer配置声卡:

amixer -c STM32MP1DK cset name='PCM Playback Switch' 'on','on' // 打开左右声道
amixer -c STM32MP1DK cset name='PCM Playback Volume' '85','85' // 设置播放音量
amixer -c STM32MP1DK cset name='Analog Playback Volume' '204','204' // 模拟信号播放音量
amixer -c STM32MP1DK cset name='PCM channel mixer' 'L R' //

PCM录音示例代码

同样先定义PCM相关参数,snd_pcm_t结构体指针pcm,定义好snd_pcm_uframes_t变量period_size指定周期大小为1024,int periods指定周期数为4,再定义rate为44100作为采样率。

编写snd_pcm_init函数,需要定义snd_pcm_hw_params_t结构体指针hwparams,之后snd_pcm_open打开PCM设备,然后snd_pcm_hw_params_malloc实例化hwparams,之后的配置跟上面的示例是一样的。

main函数中,同样,snd_pcm_init初始化PCM设备,然后定义好buf_bytes并malloc分配buf,之后open新建文件,进入for死循环录音:通过snd_pcm_readi读取PCM数据,读一个周期,然后write写入新建的文件中。

测试时,同样amixer配置声卡:

amixer -c STM32MP1DK cset name='PGA-ADC Mux Left' '3'
amixer -c STM32MP1DK cset name='PGA-ADC Mux Right' '3'
amixer -c STM32MP1DK cset name='Mic Boost Volume' '1','1'

这里因为涉及录音,需要运行后对着麦克风说话,如下:

之后播放可以直接用aplay命令播放:

aplay -f cd cap.wav

使用异步方法

只需要通过异步处理函数即可。

snd_async_add_pcm_handler()函数

alsa-lib提供了snd_async_add_pcm_handler()函数用于注册异步处理函数,其实只需要通过这个函数注册一个异步处理函数即可,其函数原型如下所示:

int snd_async_add_pcm_handler(snd_async_handler_t **handler,
		snd_pcm_t *pcm,
		snd_async_callback_t callback,
		void *private_data
);

需要传入4个参数:

  • handler:参数snd_async_handler_t用于描述一个异步处理,所以一个snd_async_handler_t对象表示一个异步处理对象;调用snd_async_add_pcm_handler()函数会实例化一个snd_async_handler_t对象,并将对象的指针(指针作为异步处理对象的句柄)通过*handler 返回出来。
  • pcm:pcm设备的句柄。
  • callback:异步处理函数(或者叫回调函数),snd_async_callback_t函数指针如下所示:
typedef void(*snd_async_callback_t)(snd_async_handler_t *handler)

handler就是异步处理对象的句柄。

  • private_data:传递给异步处理函数的私有数据,私有数据的数据类型,可以由用户自己定义,调用snd_async_add_pcm_handler()函数时,参数private_date指向私有数据对象。在异步处理函数中便可以获取到私有数据,调用snd_async_handler_get_callback_private()函数即可,如下所示:
struct my_private_data *data = snd_async_handler_get_callback_private(handler);

调用该函数之后,用户传入的PCM设备将会与异步处理对象关联起来,在异步处理函数callback中可以通过异步处理对象的句柄获取到PCM设备的句柄,通过snd_async_handler_get_pcm()获取,如下所示:

snd_pcm_t *pcm_handle = snd_async_handler_get_pcm(handler);

实现异步I/O,应用程序通常需要完成这三件事情:

  • 使能异步I/O;
  • 设置异步I/O的所有者;
  • 注册信号处理函数(譬如SIGIO信号或其它实时信号)。

示例如下:

static void snd_playback_async_callback(snd_async_handler_t *handler)
{
 	snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取 PCM 句柄
......
}

int main(void)
{
	......
	snd_async_handler_t *async_handler = NULL;
	/* 注册异步处理函数 */
	ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
	if (0 > ret)
		fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
	......
}

调用snd_async_add_pcm_handler()注册了异步回调函数snd_playback_async_callback(),当环形缓冲区有空闲的周期可填充数据时(以播放为例),音频设备驱动程序会向应用程序发送信号(SIGIO),接着应用程序便会跳转到snd_playback_async_callback()函数执行。而对于录音来说,当环形缓冲区中有数据可读时(譬如音频设备已经录制了一个周期、并将数据写入到了环形缓冲区),驱动程序便会向应用程序发送信号,接着应用程序跳转到回调函数执行。

snd_pcm_avail_update()函数

在录音情况下,应用程序调用snd_pcm_avail_update()函数用于获取当前可读取的帧数;在播放情况下,应用程序调用该函数用于获取当前可写入的帧数。原型如下:

snd_pcm_sframes_t snd_pcm_avail_update(snd_pcm_t *pcm);

PCM异步播放示例

这里与上面示例的区别如下:

需要编写snd_playback_async_callback函数,里面需要定义snd_pcm_t结构体指针handle,通过snd_async_handler_get_pcm获取PCM句柄传入handle,然后定义snd_pcm_sframes_t结构体avail,通过snd_pcm_avail_update读取到avail获取环形缓冲区待填充帧数据;之后while判断avail>=period_size,为真就可以写入一个周期,memset清零buf然后read,通过snd_pcm_write写入handle;如果不满足,说明实际写入帧数小于指定帧数,就需要lseek将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节,然后再次snd_pcm_avail_update获取更新avail。

snd_pcm_init函数是一样的初始化配置,里面就是先要定义snd_async_handler_t结构体指针async_handler,最后要通过snd_async_add_pcm_handler注册异步处理函数。

open_wav_file函数是一样的。

main函数中,初始化和打开wav一样是调用open_wav_file和snd_pcm_init,并malloc申请buf;之后需要调用snd_pcm_avail_update获取avail,当avail大于一个周期,就memset清零buf然后read,通过snd_pcm_writei写入数据到环形缓冲区;如果实际写入小于一个周期,同样lseek将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节,然后再次snd_pcm_avail_update获取avail。最后进入for死循环就可以了。

PCM异步录音示例

同样只写一下区别部分:

编写snd_capture_async_callback函数,定义snd_pcm_t结构体指针handle,调用snd_async_handler_get_pcm获取传入handle,然后定义snd_pcm_sframes_t结构体avail;之后调用snd_pcm_avail_update传入avail检查有多少帧可读,之后while循环判断avail>=period_size判断,每次读取一个周期,为真则调用snd_pcm_readi读取一个周期的PCM数据,然后write写入文件中,之后再次snd_pcm_avail_update更新avail。

snd_pcm_init是类似的,只要加一个snd_async_handler_t结构体指针async_handler,然后在最后面调用snd_async_add_pcm_handler注册异步处理函数就可以了。

main函数中,同样的先初始化然后申请缓冲,open新建文件,然后snd_pcm_start开始录音,之后进入for死循环就可以了。

使用poll()函数

学习如何使用poll I/O多路复用来实现读写数据。

使用poll I/O多路复用实现读写

获取计数

snd_pcm_poll_descriptors_count()用于获取PCM句柄的轮询描述符计数,其函数原型如下所示:

int snd_pcm_poll_descriptors_count(snd_pcm_t *pcm);

分配struct pollfd对象

为每一个轮询描述符分配一个struct pollfd对象,如下:

struct pollfd *pfds = NULL;
int count;

/* 获取 PCM 句柄的轮询描述符计数 */
count = snd_pcm_poll_descriptors_count(pcm);
if (0 >= count) {
	fprintf(stderr, "Invalid poll descriptors count\n");
	return -1;
}

/* 分配内存 */
pfds = calloc(count, sizeof(struct pollfd));
if (NULL == pfds) {
	perror("calloc error");
	return -1;
}

填充struct pollfd

调用snd_pcm_poll_descriptors()函数对struct pollfd对象进行填充(初始化),其函数原型如下所示:

int snd_pcm_poll_descriptors(
	snd_pcm_t *pcm,
	struct pollfd *pfds,
	unsigned int space
);

参数space表示pfds数组中的元素个数。

poll监听+获取事件

调用poll()函数来监视PCM设备是否有数据可读或可写,当有数据可读或可写时,poll()函数返回,此时可以调用snd_pcm_poll_descriptors_revents()函数获取文件描述符中返回的事件类型,并与poll的events标志进行比较,以确定是否可读或可写,snd_pcm_poll_descriptors_revents()函数原型如下所示:

nt snd_pcm_poll_descriptors_revents(
	snd_pcm_t *pcm,
	struct pollfd *pfds,
	unsigned int nfds,
	unsigned short *revents
);

参数nfds表示pfds数组中元素的个数,调用该函数获取文件描述符中返回的事件,通过参数revents返回出来;注意,不要直接读取struct pollfd对象中的revents成员变量,因为 snd_pcm_poll_descriptors_revents()函数会对poll()系统调用返回的revents掩码进行“分解”以纠正语义(POLLIN = 读取,POLLOUT = 写入)。

示例如下:

for ( ; ; ) {
	ret = poll(pfds, count, -1);//调用 poll
	if (0 > ret) {
		perror("poll error");
		return -1;
	}

	ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
	if (0 > ret)
		return -1;
	if (revents & POLLERR) //发生 I/O 错误
		return -1;
	if (revents & POLLIN) {//表示可读取数据
		// 从 PCM 设备读取数据
	}
	if (revents & POLLOUT) {//表示可写入数据
		// 将数据写入 PCM 设备
	}
}

PCM播放示例

这里还是只列出区别之处:

需要定义pollfd结构体指针pfds。

snd_pcm_init与一开始的实验是一样的。

open_wav_file是一样的。

编写snd_pcm_poll_init函数,通过snd_pcm_poll_descriptors_count获取PCM句柄轮询的描述符个数传入count,然后calloc给pfds分配内存,snd_pcm_poll_descriptors填充pfds。

main函数中,同样要定义snd_pcm_sframes_t结构体变量avail,先调用open_wav_file打开wav音频,snd_pcm_init初始化PCM设备,malloc给buf申请内存,然后调用snd_pcm_poll_init初始化多路I/O复用,之后进入for死循环调用poll,然后snd_pcm_poll_descriptors_revents获取事件,这里是写入,所以事件为revents&POLLOUT,如果为真,则snd_pcm_avail_update传给avail获取待填充数据,然后while判断avail>=period_size按周期写入数据,memset清零buf后read文件,再调用snd_pcm_writei写入pcm设备;不满一个周期,就lseek重新定位读指针(与之前是一样的),然后snd_pcm_avail_update更新avail。

PCM录音示例

定义pollfd结构体指针pfds。

snd_pcm_init是一样的。

编写snd_pcm_poll_init函数,这个跟播放是一样的。

main函数中,同样先snd_pcm_init,然后malloc申请buf内存,open新建文件后,调用snd_pcm_poll_init初始化,然后snd_pcm_start开始录音,进入for死循环:调用poll函数,然后snd_pcm_poll_descriptors_revents,这里是录音所以是读数据,事件为revents&POLLIN,然后就是snd_pcm_avail_update获取给到avail,检查可读的数据长度,然后while判断avail>=period_size来按照周期读取,为真则snd_pcm_readi读取一个周期数据,然后write写入文件,最后snd_pcm_avail_update更新avail。

PCM设备状态

alsa-lib提供了函数snd_pcm_state()用于获取PCM设备当前的状态,其函数原型如下所示:

snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm);

返回值是一个snd_pcm_state_t类型的变量,snd_pcm_state_t其实是一个枚举类型,描述了PCM所有状态。

SND_PCM_STATE_OPEN表示PCM设备处于打开状态,譬如当调用snd_pcm_open()后,PCM设备就处于该状态。

SND_PCM_STATE_SETUP表示设备已经初始化完成了,参数已经配置好了。

SND_PCM_STATE_PREPARED表示设备已经准备好了,可以开始播放了、可以开始录音了。当应用程序调用snd_pcm_hw_params() 函数之后,设备就处于SND_PCM_STATE_PREPARED状态。应用程序中,可以调用snd_pcm_prepare()函数使设备处于SND_PCM_STATE_PREPARED状态,该函数原型如下所示:

int snd_pcm_prepare(snd_pcm_t *pcm);

调用成功返回0,失败将返回一个负数错误码。

SND_PCM_STATE_RUNNING表示设备正在运行,譬如正在播放、正在录音。可以调用snd_pcm_start()函数以启动PCM设备,启动成功之后,设备开始播放或采集,此时设备处于SND_PCM_STATE_RUNNING状态。此外,当设备处于SND_PCM_STATE_PREPARED状态时,应用程序调用snd_pcm_readi/snd_pcm_writei进行读写数据时,这些函数内部会自动调用snd_pcm_start()函数。当设备处于运行状态时,应用程序可调用snd_pcm_drop()或snd_pcm_drain()函数使设备停止运行,譬如
停止播放、停止音频采集;它们的函数原型如下所示:

int snd_pcm_drain(snd_pcm_t *pcm);
int snd_pcm_drop(snd_pcm_t *pcm);

函数调用成功返回0;失败返回负值错误码。snd_pcm_drop()函数将立即停止PCM,丢弃挂起的帧;而snd_pcm_drain会处理完挂起的帧后再停止PCM。此时,设备回到SND_PCM_STATE_SETUP状态。

当发生XRUN时,设备会处于SND_PCM_STATE_XRUN状态,当处于SND_PCM_STATE_XRUN状态时,应用程序可以调用snd_pcm_prepare()使设备恢复,使其回到SND_PCM_STATE_PREPARED状态。

SND_PCM_STATE_DRAINING,这个不是很清楚。

SND_PCM_STATE_PAUSED表示设备处于暂停状态。当设备正在运行时(也就是处于SND_PCM_STATE_RUNNING状态),应用程序调用snd_pcm_pause()函数可让设备暂停,其函数原型如下所示:

int snd_pcm_pause(snd_pcm_t *pcm, int enable);

参数enable等于1,表示使设备暂停;enable等于0表示使设备恢复运行。调用成功返回0;失败返回一个负值错误码。当然,也不是所有设备都可以暂停,需要通过snd_pcm_hw_params_can_pause()函数来判断设备是否支持暂停,其函数原型如下所示:

int snd_pcm_hw_params_can_pause(const snd_pcm_hw_params_t *params);

函数返回1表示硬件支持暂停;返回0表示硬件不支持暂停。

SND_PCM_STATE_SUSPENDED表示硬件已经挂起suspended,如果硬件发生了挂起,应用程序可以调用snd_pcm_resume()函数
从挂起中恢复,并确保不会丢失样本数据(精细恢复)。原型如下所示:

int snd_pcm_resume(snd_pcm_t *pcm);

函数调用成功返回0;失败返回一个负值错误码。当然,并非所有硬件都支持此功能,可以调用snd_pcm_hw_params_can_resume()函数判断硬件是否支持从挂起中恢复,其函数原型如下所示:

int snd_pcm_hw_params_can_resume(const snd_pcm_hw_params_t *params);

函数调用返回1表示支持,返回0表示不支持。

SND_PCM_STATE_DISCONNECTED表示硬件已经断开连接。

状态之间的切换如下所示:

正点原子linux应用编程——提高篇4_第3张图片

PCM播放——加入状态控制

这里加入播放过程控制,可使用空格控制暂停和恢复播放。

这里需要加上一个termios结构体变量old_cfg,static类型来保存终端当前配置。

编写snd_playback_async_callback函数,这个是一样的。

编写snd_pcm_init函数,这个跟之前的异步版本也是一样的。

open_wav_file这个跟之前也是一样的。

main函数中,需要定义termios结构体变量new_cfg,sigset_t类型的sset。先要屏蔽SIGIO信号,sigemptyset传入sset信号集,然后sigaddset加入SIGIO再调用sigprocmask传入SIG_BLOCK;之后open_wav_file,snd_pcm_init并malloc分配buf;之后配置终端,tcgetattr传入STDIN_FILENO获取终端,然后memcpy把new_cfg传到old_cfg备份一下,之后设置new_cfg的c_lflag变量一次&=~ICANON和ECHO来设置非规范终端并禁止回显,之后tcsetattr是的new_cfg生效;播放的代码是一样的,snd_pcm_avail_update获取avail之后按照周期写入;之后需要sigprocmask取消SIGIO屏蔽,进入for死循环:getchar获取输入的字符并通过switch判断,如果是空格,就去switch判断pcm当前状态(snd_pcm_state来获取),SND_PCM_STATE_PAUSED就是snd_pcm_pause恢复运行;SND_PCM_STATE_RUNNING就是snd_pcm_pause暂停运行。

这里要注意,MP157开发板的CS42L51硬件不支持pause功能,需要进行如下修改:switch判断pcm状态,如果是SND_PCM_STATE_SETUP,那就调用snd_pcm_prepare恢复状态;如果是SMD_PCM_STATE_RUNNING,就调用snd_pcm_drop停止播放。

snd_pcm_readi/snd_pcm_writei错误处理

当snd_pcm_readi/snd_pcm_writei调用出错时,会返回一个小于0的错误码,可调用snd_strerror()函数获取对应的错误描述信息。

当返回值等于-EBADFD,表示PCM设备的状态不对,因为执行snd_pcm_readi/snd_pcm_writei读取/写
入数据需要PCM设备处于SND_PCM_STATE_PREPARED或SND_PCM_STATE_RUNNING状态。

当返回值等于-EPIPE,表示发生了XRUN,可以根据自己的实际需要进行处理,譬如调用snd_pcm_drop()停止PCM设备,或者调用snd_pcm_prepare()使设备恢复进入准备状态。

当返回值等于-ESTRPIPE,表示硬件发生了挂起,此时PCM设备处于SND_PCM_STATE_SUSPENDED状态,譬如可以调用snd_pcm_resume()函数从挂起中精确恢复,如果硬件不支持,还可调用snd_pcm_prepare()函数使设备进入准备状态,或者执行其它的处理,根据应用需求的进行相应的处理。

混音器设置

混音器相关的接口在alsa-lib的Mixer Interface模块中有介绍。

打开混音器

调用snd_mixer_open()函数打开一个空的混音器,其函数原型如下所示:

int snd_mixer_open(snd_mixer_t **mixerp, int mode);

snd_mixer_t数据结构描述混音器,调用snd_mixer_open()函数会实例化一个snd_mixer_t对象,并将对象的指针(混音器的句柄)通过mixerp返回出来。参数mode指定了打开模式,通常设置为0使用默认模式即可。函数调用成功返回0;失败返回一个小于0的错误码。

Attach关联设备

调用snd_mixer_attach()函数进行关联声卡控制设备,其函数原型如下所示:

int snd_mixer_attach(snd_mixer_t *mixer, const char *name);

参数mixer对应的是混音器的句柄,参数name指定了声卡控制设备的名字,同样这里使用的也是逻辑设备名,而非设备节点的名字,命名方式为"hw:i",i表示声卡的卡号,通常一个声卡对应一个控制设备。函数调用成功返回0;失败返回一个小于0的错误码。

注册

调用snd_mixer_selem_register()函数注册混音器,其函数原型如下所示:

int snd_mixer_selem_register(
	snd_mixer_t *mixer,
	struct snd_mixer_selem_regopt *options,
	snd_mixer_class_t **classp);

参数options和参数classp直接设置为NULL即可。函数调用成功返回0;失败返回一个小于0的错误码。

加载

最后需要加载混音器,调用snd_mixer_load()函数完成加载,函数原型如下所示:

int snd_mixer_load(snd_mixer_t * mixer);

函数调用成功返回0;失败返回小于0的错误码。

查找元素

alsa-lib中把混音器的配置项称为元素(element)。

alsa-lib使用数据结构snd_mixer_elem_t来描述一个元素。混音器有很多的元素(很多配置项),通过snd_mixer_first_elem()函数可以找到混音器的第一个元素,其函数原型如下所示:

snd_mixer_elem_t *snd_mixer_first_elem(snd_mixer_t *mixer);

通过snd_mixer_last_elem()函数可找到混音器的最后一个元素,如下:

snd_mixer_elem_t *snd_mixer_last_elem(snd_mixer_t *mixer);

调用snd_mixer_elem_next()和snd_mixer_elem_prev()函数可获取指定元素的下一个元素和上一个元素:

snd_mixer_elem_t *snd_mixer_elem_next(snd_mixer_elem_t *elem);
snd_mixer_elem_t *snd_mixer_elem_prev(snd_mixer_elem_t *elem);

遍历如下所示:

snd_mixer_elem_t *elem = NULL;

elem = snd_mixer_first_elem(mixer);//找到第一个元素
while (elem) {
	......
	......
	snd_mixer_elem_next(elem); //找到下一个元素
}

调用snd_mixer_selem_get_name()函数可获取指定元素的名字,如下所示:

const char *snd_mixer_selem_get_name(snd_mixer_elem_t *elem);

获取/更改元素

有两种配置值:第一种它的配置值是在一个范围内的数值,譬如音量大小的调节;另一种则是bool类型,用于控制开启或关闭,譬如0表示关闭配置、1表示使能配置。

可以调用snd_mixer_selem_has_playback_volume或snd_mixer_selem_has_capture_volume函数来判断一个指定元素的配置值是否是volume类型,也就是上文的第一种类型。函数原型如下所示:

int snd_mixer_selem_has_playback_volume(snd_mixer_elem_t *elem);
int snd_mixer_selem_has_capture_volume(snd_mixer_elem_t *elem);

函数返回0表示不是volume类型;返回1表示是volume类型。

调用snd_mixer_selem_has_playback_switch和snd_mixer_selem_has_capture_switch函数判断一个指定元素的配置值是否是switch类型,也就是上面说的第二种情况。函数原型如下所示:

int snd_mixer_selem_has_playback_switch(snd_mixer_elem_t *elem);
int snd_mixer_selem_has_capture_switch(snd_mixer_elem_t *elem);

函数返回0表示不是switch类型;返回1表示是switch类型。

通过snd_mixer_selem_has_playback_channel或snd_mixer_selem_has_capture_channel函数可判断指定元素是否包含指定通道,其函数原型如下所示:

int snd_mixer_selem_has_playback_channel(
	snd_mixer_elem_t *elem,
	snd_mixer_selem_channel_id_t channel
);
int snd_mixer_selem_has_capture_channel(
	snd_mixer_elem_t *elem,
	snd_mixer_selem_channel_id_t channel
);

参数channel用于指定一个通道,snd_mixer_selem_channel_id_t是一个枚举类型。如果元素是双声道元素,通常只包含左前(SND_MIXER_SCHN_FRONT_LEFT)和右前(SND_MIXER_SCHN_FRONT_RIGHT)两个声道。如果是单声道设备,通常只包含SND_MIXER_SCHN_MONO,其数值等于SND_MIXER_SCHN_FRONT_LEFT。

可以调用snd_mixer_selem_is_playback_mono或snd_mixer_selem_is_capture_mono函数判断一个指定的元素是否是单声道元素,其函数原型如下所示:

int snd_mixer_selem_is_playback_mono(snd_mixer_elem_t *elem);
int snd_mixer_selem_is_capture_mono(snd_mixer_elem_t *elem);

调用snd_mixer_selem_get_playback_volume或snd_mixer_selem_get_capture_volume获取指定元素的音量大小,其函数原型如下所示:

int snd_mixer_selem_get_playback_volume(
	snd_mixer_elem_t *elem,
	snd_mixer_selem_channel_id_t channel,
	long *value
);
int snd_mixer_selem_get_capture_volume(
	snd_mixer_elem_t *elem,
	snd_mixer_selem_channel_id_t channel,
	long *value
);

参数elem指定对应的元素,参数channel指定该元素的某个声道。调用snd_mixer_selem_get_playback_volume()函数可获取elem元素的channel声道对应的音量大小,并将获取到的音量值通过value返回出来。函数调用成功返回0,失败返回一个小于0的错误码。

调用snd_mixer_selem_set_playback_volume或snd_mixer_selem_set_capture_volume设置指定元素的音量值,其函数原型如下所示:

int snd_mixer_selem_set_playback_volume(
	snd_mixer_elem_t *elem,
	snd_mixer_selem_channel_id_t channel,
	long value
);
int snd_mixer_selem_set_capture_volume(
	snd_mixer_elem_t *elem,
	snd_mixer_selem_channel_id_t channel,
	long value
);

参数elem指定元素、参数channel指定该元素的某个声道,参数value指定音量值。

调用snd_mixer_selem_set_playback_volume_all或snd_mixer_selem_set_capture_volume_all可一次性设置指定元素所有声道的音量,函数原型如下所示:

int snd_mixer_selem_set_playback_volume_all(
	snd_mixer_elem_t *elem,
	long value 
);
int snd_mixer_selem_set_capture_volume_all(
	snd_mixer_elem_t *elem,
	long value 
);

调用snd_mixer_selem_get_playback_volume_range或snd_mixer_selem_get_capture_volume_range获取指定元素的音量范围,其函数原型如下所示:

int snd_mixer_selem_get_playback_volume_range(
	snd_mixer_elem_t *elem,
	long *min,
	long *max
);
int snd_mixer_selem_get_capture_volume_range(
	snd_mixer_elem_t *elem,
	long *min,
	long *max
);

示例程序

需要额外定义snd_mixer_t指针mixer,snd_mixer_elem_t指针play_back_vol_elem,定义termios结构体变量old_cfg,int类型alsa_can_pause。

编写snd_pcm_fill_buf函数,这个就是把之前按周期填充数据写成了一个函数。

编写snd_pyalback_async_callback函数,里面就是调用snd_pcm_fill_buf函数。

编写snd_pcm_init函数,这个就是之前的异步初始化。

编写snd_mixer_init函数,定义snd_mixer_emel_t指针elem,char指针elem_name,以及minvol和maxvol;调用snd_mixer_open打开混音器,然后snd_mixer_attach关联声卡,snd_mixer_selem_register注册混音器,snd_mixer_load加载混音器,snd_mixer_first_elem找到第一个元素后通过while来遍历:调用snd_mixer_selem_get_name获取元素名称存入elem_name,然后strcmp比对elem_name是否为PCM,然后调用snd_mixer_selem_has_playback_volume判断是否为音量控制元素,之后通过snd_mixer_selem_get_playback_volume_range获取设置范围并通过snd_mixer_selem_set_playback_volume_all设置;如果是elem_name为Analog,一样按照上面来一遍设置音量;最后elem通过snd_mixer_elem_next获取下一个完成遍历。

open_wav_file是一样的。

main函数中,同样先屏蔽SIGIO信号,然后open_wav_file,snd_pcm_init初始化PCM,之后调用snd_mixer_init初始化混音器,malloc分配buf,然后进行终端配置并调用snd_pcm_fill_buf播放,之后取消SIGIO屏蔽,进入for死循环:getchar获取输入的字符之后,空格还是一样来完成状态切换;如果是w那就是增加音量,调用snd_mixer_selem_get_playback_volume获取后通过snd_mixer_selem_set_playback_volume_all设置;s就是音量降低,代码是类似的。

回环测试例程

alsa-utils提供了一个用于回环测试的工具alsaloop,可以实现边录音、边播放。执行"alsaloop --help"可以查看alsaloop测试程序的使用帮助信息;直接运行"alsaloop -C hw:0,1 -t 1000"可以进行测试。

总结

ALSA插件

ALSA提供了一些PCM插件,以扩展PCM设备的功能和特性,插件负责各种样本转换、通道之间的样本复制等。

这些插件,可以直接去看教程的讲解,也可以去alsa的官网找文档。

学习总结

这一章的内容很多,主要就是Linux音频控制的各种API的学习,回头好好看看这一章的API总结,然后看看正点原子给的源代码,熟悉一下流程。

你可能感兴趣的:(linux学习,linux,学习,笔记)