MP157开发板支持音频,板上搭载了音频编解码芯片CS42L51,支持播放以及录音功能。
ALSA是Advanced Linux Sound Architecture(高级的Linux声音体系)的缩写,目前已经成为了 linux
下的主流音频体系架构,提供了音频和MIDI的支持,替代了原先旧版本中的OSS(开发声音系统);这一部分在驱动的时候,对这套驱动框架有过介绍,这里就不做赘述。
在应用层,ALSA提供了一套标准的API,应用程序只需要调用这些API就可完成对底层音频硬
件设备的控制,譬如播放、录音等,这一套API称为alsa-lib。如下图所示:
alsa-lib是一套Linux应用层的C语言函数库,为音频应用程序开发提供了一套统一、标准的接口,应用程序只需调用这一套API即可完成对底层声卡设备的操控,譬如播放与录音。
alsa-lib库支持功能比较多,提供了丰富的API接口供应用程序开发人员调用,根据函数的功能、作用
将这些API进行了分类,可通过官网上Modules按钮查看其模块划分,这里主要涉及三个模块:PCM Interface、Error Interface以及Mixer Interface。
在Linux内核设备驱动层,基于ALSA音频驱动框架注册的sound设备会在/dev/snd目录下生成相应的设备节点文件。会有如下设备文件:
在Linux系统的/proc/asound目录下,有很多的
文件,这些文件记录了系统中声卡相关的信息:
除了移植alsa-lib库之外,通常还需要移植alsa-utils,alsa-utils包含了一些用于测试、配置声卡的工具。
这个在驱动教程中有所介绍,在buildroot中使能配置然后编译就可以了。而且开发板的出厂系统中是已经移植了alsa-lib和alsa-utils。
alsa-utils提供了一些用于测试、配置声卡的工具,譬如aplay、arecord、alsactl、alsaloop、alsamixer、amixer等,在开发板出厂系统上可以直接使用这些工具,这些应用程序也都是基于alsa-lib编写的。
alsactl -f /var/lib/alsa/asound.state store alsactl -f /var/lib/alsa/asound.state restore |
数据的传输:
在播放情况下,buffer中存放了需要播放的PCM音频数据,由应用程序向buffer中写入音频数据,buffer中的音频数据由DMA传输给音频设备进行播放,所以应用程序向buffer写入数据、音频设备从buffer读取数据,这就是buffer中数据的传输情况。
这里是有读指针和写指针,在缓冲区没有数据时均指向buffer起始位置,每次读写到末尾后会回到buffer起始位置。
buffer中存放了音频设备采集到的音频数据(外界模拟声音通过ADC转为数字声音),由音频设备向buffer中写入音频数据(DMA搬运),而应用程序从buffer中读取数据,所以音频设备向buffer写入数据、应用程序从buffer读取数据。
当一个声卡处于工作状态时,环形缓冲区buffer中的数据总是连续地在音频设备和应用程序缓存区间传输,如下图所示:
在录音例子中,如果应用程序读取数据不够快,环形缓冲区buffer中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖;这种数据的丢失被称为overrun。在播放例子中,如果应用程序写入数据到环形缓冲区buffer中的速度不够快,缓存区将会“饿死”(缓冲区中无数据可播放),这样的错误被称为underrun(欠载)。在ALSA 文档中,将这两种情形统称为"XRUN",适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。
首先需要在应用程序中包含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个参数:
设备打开成功,返回0;打开失败,返回小于0的错误编号,可以使用alsa-lib提供的库函数snd_strerror()来得到对应的错误描述信息。
函数snd_pcm_close()用于关闭PCM设备,函数原型如下所示:
int snd_pcm_close(snd_pcm_t *pcm);
主要是对采样率、声道数、格式、访问类型、period周期大小、buffer大小等进行配置。
使用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_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进行读写。
首先需要定义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配置声卡: 同样先定义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配置声卡: 这里因为涉及录音,需要运行后对着麦克风说话,如下: 之后播放可以直接用aplay命令播放: 只需要通过异步处理函数即可。 alsa-lib提供了snd_async_add_pcm_handler()函数用于注册异步处理函数,其实只需要通过这个函数注册一个异步处理函数即可,其函数原型如下所示: 需要传入4个参数: handler就是异步处理对象的句柄。 调用该函数之后,用户传入的PCM设备将会与异步处理对象关联起来,在异步处理函数callback中可以通过异步处理对象的句柄获取到PCM设备的句柄,通过snd_async_handler_get_pcm()获取,如下所示: 实现异步I/O,应用程序通常需要完成这三件事情: 示例如下: 调用snd_async_add_pcm_handler()注册了异步回调函数snd_playback_async_callback(),当环形缓冲区有空闲的周期可填充数据时(以播放为例),音频设备驱动程序会向应用程序发送信号(SIGIO),接着应用程序便会跳转到snd_playback_async_callback()函数执行。而对于录音来说,当环形缓冲区中有数据可读时(譬如音频设备已经录制了一个周期、并将数据写入到了环形缓冲区),驱动程序便会向应用程序发送信号,接着应用程序跳转到回调函数执行。 在录音情况下,应用程序调用snd_pcm_avail_update()函数用于获取当前可读取的帧数;在播放情况下,应用程序调用该函数用于获取当前可写入的帧数。原型如下: 这里与上面示例的区别如下: 需要编写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死循环就可以了。 同样只写一下区别部分: 编写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 I/O多路复用来实现读写数据。 snd_pcm_poll_descriptors_count()用于获取PCM句柄的轮询描述符计数,其函数原型如下所示: 为每一个轮询描述符分配一个struct pollfd对象,如下: 调用snd_pcm_poll_descriptors()函数对struct pollfd对象进行填充(初始化),其函数原型如下所示: 参数space表示pfds数组中的元素个数。 调用poll()函数来监视PCM设备是否有数据可读或可写,当有数据可读或可写时,poll()函数返回,此时可以调用snd_pcm_poll_descriptors_revents()函数获取文件描述符中返回的事件类型,并与poll的events标志进行比较,以确定是否可读或可写,snd_pcm_poll_descriptors_revents()函数原型如下所示: 参数nfds表示pfds数组中元素的个数,调用该函数获取文件描述符中返回的事件,通过参数revents返回出来;注意,不要直接读取struct pollfd对象中的revents成员变量,因为 snd_pcm_poll_descriptors_revents()函数会对poll()系统调用返回的revents掩码进行“分解”以纠正语义(POLLIN = 读取,POLLOUT = 写入)。 示例如下: 这里还是只列出区别之处: 需要定义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。 定义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。 alsa-lib提供了函数snd_pcm_state()用于获取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状态,该函数原型如下所示: 调用成功返回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()函数使设备停止运行,譬如 函数调用成功返回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()函数可让设备暂停,其函数原型如下所示: 参数enable等于1,表示使设备暂停;enable等于0表示使设备恢复运行。调用成功返回0;失败返回一个负值错误码。当然,也不是所有设备都可以暂停,需要通过snd_pcm_hw_params_can_pause()函数来判断设备是否支持暂停,其函数原型如下所示: 函数返回1表示硬件支持暂停;返回0表示硬件不支持暂停。 SND_PCM_STATE_SUSPENDED表示硬件已经挂起suspended,如果硬件发生了挂起,应用程序可以调用snd_pcm_resume()函数 函数调用成功返回0;失败返回一个负值错误码。当然,并非所有硬件都支持此功能,可以调用snd_pcm_hw_params_can_resume()函数判断硬件是否支持从挂起中恢复,其函数原型如下所示: 函数调用返回1表示支持,返回0表示不支持。 SND_PCM_STATE_DISCONNECTED表示硬件已经断开连接。 状态之间的切换如下所示: 这里加入播放过程控制,可使用空格控制暂停和恢复播放。 这里需要加上一个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调用出错时,会返回一个小于0的错误码,可调用snd_strerror()函数获取对应的错误描述信息。 当返回值等于-EBADFD,表示PCM设备的状态不对,因为执行snd_pcm_readi/snd_pcm_writei读取/写 当返回值等于-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()函数打开一个空的混音器,其函数原型如下所示: snd_mixer_t数据结构描述混音器,调用snd_mixer_open()函数会实例化一个snd_mixer_t对象,并将对象的指针(混音器的句柄)通过mixerp返回出来。参数mode指定了打开模式,通常设置为0使用默认模式即可。函数调用成功返回0;失败返回一个小于0的错误码。 调用snd_mixer_attach()函数进行关联声卡控制设备,其函数原型如下所示: 参数mixer对应的是混音器的句柄,参数name指定了声卡控制设备的名字,同样这里使用的也是逻辑设备名,而非设备节点的名字,命名方式为"hw:i",i表示声卡的卡号,通常一个声卡对应一个控制设备。函数调用成功返回0;失败返回一个小于0的错误码。 调用snd_mixer_selem_register()函数注册混音器,其函数原型如下所示: 参数options和参数classp直接设置为NULL即可。函数调用成功返回0;失败返回一个小于0的错误码。 最后需要加载混音器,调用snd_mixer_load()函数完成加载,函数原型如下所示: 函数调用成功返回0;失败返回小于0的错误码。 alsa-lib中把混音器的配置项称为元素(element)。 alsa-lib使用数据结构snd_mixer_elem_t来描述一个元素。混音器有很多的元素(很多配置项),通过snd_mixer_first_elem()函数可以找到混音器的第一个元素,其函数原型如下所示: 通过snd_mixer_last_elem()函数可找到混音器的最后一个元素,如下: 调用snd_mixer_elem_next()和snd_mixer_elem_prev()函数可获取指定元素的下一个元素和上一个元素: 遍历如下所示: 调用snd_mixer_selem_get_name()函数可获取指定元素的名字,如下所示: 有两种配置值:第一种它的配置值是在一个范围内的数值,譬如音量大小的调节;另一种则是bool类型,用于控制开启或关闭,譬如0表示关闭配置、1表示使能配置。 可以调用snd_mixer_selem_has_playback_volume或snd_mixer_selem_has_capture_volume函数来判断一个指定元素的配置值是否是volume类型,也就是上文的第一种类型。函数原型如下所示: 函数返回0表示不是volume类型;返回1表示是volume类型。 调用snd_mixer_selem_has_playback_switch和snd_mixer_selem_has_capture_switch函数判断一个指定元素的配置值是否是switch类型,也就是上面说的第二种情况。函数原型如下所示: 函数返回0表示不是switch类型;返回1表示是switch类型。 通过snd_mixer_selem_has_playback_channel或snd_mixer_selem_has_capture_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函数判断一个指定的元素是否是单声道元素,其函数原型如下所示: 调用snd_mixer_selem_get_playback_volume或snd_mixer_selem_get_capture_volume获取指定元素的音量大小,其函数原型如下所示: 参数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设置指定元素的音量值,其函数原型如下所示: 参数elem指定元素、参数channel指定该元素的某个声道,参数value指定音量值。 调用snd_mixer_selem_set_playback_volume_all或snd_mixer_selem_set_capture_volume_all可一次性设置指定元素所有声道的音量,函数原型如下所示: 调用snd_mixer_selem_get_playback_volume_range或snd_mixer_selem_get_capture_volume_range获取指定元素的音量范围,其函数原型如下所示: 需要额外定义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提供了一些PCM插件,以扩展PCM设备的功能和特性,插件负责各种样本转换、通道之间的样本复制等。 这些插件,可以直接去看教程的讲解,也可以去alsa的官网找文档。 这一章的内容很多,主要就是Linux音频控制的各种API的学习,回头好好看看这一章的API总结,然后看看正点原子给的源代码,熟悉一下流程。
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录音示例代码
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 -f cd cap.wav
使用异步方法
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
);
typedef void(*snd_async_callback_t)(snd_async_handler_t *handler)
struct my_private_data *data = snd_async_handler_get_callback_private(handler);
snd_pcm_t *pcm_handle = snd_async_handler_get_pcm(handler);
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_pcm_avail_update()函数
snd_pcm_sframes_t snd_pcm_avail_update(snd_pcm_t *pcm);
PCM异步播放示例
PCM异步录音示例
使用poll()函数
使用poll I/O多路复用实现读写
获取计数
int snd_pcm_poll_descriptors_count(snd_pcm_t *pcm);
分配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
int snd_pcm_poll_descriptors(
snd_pcm_t *pcm,
struct pollfd *pfds,
unsigned int space
);
poll监听+获取事件
nt snd_pcm_poll_descriptors_revents(
snd_pcm_t *pcm,
struct pollfd *pfds,
unsigned int nfds,
unsigned short *revents
);
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播放示例
PCM录音示例
PCM设备状态
snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm);
int snd_pcm_prepare(snd_pcm_t *pcm);
停止播放、停止音频采集;它们的函数原型如下所示:int snd_pcm_drain(snd_pcm_t *pcm);
int snd_pcm_drop(snd_pcm_t *pcm);
int snd_pcm_pause(snd_pcm_t *pcm, int enable);
int snd_pcm_hw_params_can_pause(const snd_pcm_hw_params_t *params);
从挂起中恢复,并确保不会丢失样本数据(精细恢复)。原型如下所示:int snd_pcm_resume(snd_pcm_t *pcm);
int snd_pcm_hw_params_can_resume(const snd_pcm_hw_params_t *params);
PCM播放——加入状态控制
snd_pcm_readi/snd_pcm_writei错误处理
入数据需要PCM设备处于SND_PCM_STATE_PREPARED或SND_PCM_STATE_RUNNING状态。混音器设置
打开混音器
int snd_mixer_open(snd_mixer_t **mixerp, int mode);
Attach关联设备
int snd_mixer_attach(snd_mixer_t *mixer, const char *name);
注册
int snd_mixer_selem_register(
snd_mixer_t *mixer,
struct snd_mixer_selem_regopt *options,
snd_mixer_class_t **classp);
加载
int snd_mixer_load(snd_mixer_t * mixer);
查找元素
snd_mixer_elem_t *snd_mixer_first_elem(snd_mixer_t *mixer);
snd_mixer_elem_t *snd_mixer_last_elem(snd_mixer_t *mixer);
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); //找到下一个元素
}
const char *snd_mixer_selem_get_name(snd_mixer_elem_t *elem);
获取/更改元素
int snd_mixer_selem_has_playback_volume(snd_mixer_elem_t *elem);
int snd_mixer_selem_has_capture_volume(snd_mixer_elem_t *elem);
int snd_mixer_selem_has_playback_switch(snd_mixer_elem_t *elem);
int snd_mixer_selem_has_capture_switch(snd_mixer_elem_t *elem);
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
);
int snd_mixer_selem_is_playback_mono(snd_mixer_elem_t *elem);
int snd_mixer_selem_is_capture_mono(snd_mixer_elem_t *elem);
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
);
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
);
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
);
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
);
示例程序
回环测试例程
总结
ALSA插件
学习总结