ALPHA I.MX6U 开发板支持音频,板上搭载了音频编解码芯片 WM8960,支持播放以及录音功能!
本章将会讨论如下主题内容。
⚫ Linux 下 ALSA 框架概述;
⚫ alsa-lib 库介绍;
⚫ alsa-lib 库移植;
⚫ alsa-lib 库的使用;
⚫ 音频应用编程之播放;
⚫ 音频应用编程之录音
ALSA概述
ALSA 是 Advanced Linux Sound Architecture(高级的 Linux 声音体系)的缩写,目前已经成为了 linux下的主流音频体系架构,提供了音频和 MIDI 的支持,替代了原先旧版本中的 OSS(开发声音系统);ALSA 是 Linux 系统下一套标准的、先进的音频驱动框架,那么这套框架的设计本身是比较复杂的,采用分离、分层思想设计而成。
在应用层,ALSA 为我们提供了一套标准的 API,应用程序只需要调用这些 API 就可完成对底层音频硬
件设备的控制,譬如播放、录音等,这一套 API 称为 alsa-lib
对于我们来说,学习音频应用编程其实就是学习 alsa-lib 库函数的使用、如何基于 alsa-lib 库函数开发音频应用程序。
ALSA 提供了关于 alsa-lib 的使用说明文档,其链接地址为:ALSA project - the C library reference: Index, Preamble and License,
sound 设备节点
在 Linux 内核设备驱动层、基于 ALSA 音频驱动框架注册的 sound 设备会在/dev/snd 目录下生成相应的设备节点文件。
我们编写的应用程序,虽然是调用 alsa-lib 库函数去控制底层音频硬件,但最终也是落实到对 sound
设备节点的 I/O 操作,只不过 alsa-lib 已经帮我们封装好了,在 Linux 系统的/proc/asound 目录下,有很多的文件,这些文件记录了系统中声卡相关的信息。
编写一个简单地 alsa-lib 应用程序
对于 alsa-lib 库的使用,ALSA 提供了一些参考资料来帮助应用程序开发人员快速上手 alsa-lib、基于
alsa-lib 进行应用编程,以下笔者给出了链接:
ALSA Programming HOWTO v.0.0.8
ALSA project - the C library reference: Examples
第一份文档向用户介绍了如何使用 alsa-lib 编写简单的音频应用程序,包括 PCM 播放音频、PCM 录音
等,笔者也是参考了这份文档来编写本章教程,对应初学者,建议大家看一看。
第二个链接地址是 ALSA 提供的一些示例代码,如下所示:
一些基本概念
- 样本长度(Sample)
- 声道数(channel)
- 帧(frame)
- 采样率(Sample rate)
- 交错模式(interleaved)
- 周期(period)
- 缓冲区(buffer)
音频设备底层驱动程序使用 DMA 来搬运数据,这个 buffer 中有 4 个 period,每当 DMA 搬运完一个period 的数据就会触发一次中断,因此搬运整个 buffer 中的数据将产生 4 次中断。
数据之间的传输
Over and Under Run
2.编写一个音频应用程序
2.1打开 PCM 设备
需要包含头文件<alsa/asoundlib.h>
int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)
⚫ pcmp:snd_pcm_t 用于描述一个 PCM 设备,所以一个 snd_pcm_t 对象表示一个 PCM 设备;
snd_pcm_open 函数会打开参数 name 所指定的设备,实例化 snd_pcm_t 对象,并将对象的指针(也
就是 PCM 设备的句柄)通过 pcmp 返回出来。
⚫ name:参数 name 指定 PCM 设备的名字。alsa-lib 库函数中使用逻辑设备名而不是设备文件名,命名方式为"hw:i,j",i 表示声卡的卡号,j 则表示这块声卡上的设备号;譬如"hw:0,0"表示声卡 0 上的
PCM 设备 0,在播放情况下,这其实就对应/dev/snd/pcmC0D0p(如果是录音,则对应
/dev/snd/pcmC0D0c)。除了使用"hw:i,j"这种方式命名之外,还有其它两种常用的命名方式,譬如
"plughw:i,j"、"default"等,关于这些名字的不同,本章最后再向大家进行简单地介绍,这里暂时先
不去理会这个问题。
⚫ stream:参数 stream 指定流类型,有两种不同类型:SND_PCM_STREAM_PLAYBACK 和
SND_PCM_STREAM_CAPTURE ; SND_PCM_STREAM_PLAYBACK 表 示 播 放 ,SND_PCM_STREAM_CAPTURE 则表示采集。
⚫ mode:最后一个参数 mode 指定了 open 模式,通常情况下,我们会将其设置为 0,表示默认打开
模式,默认情况下使用阻塞方式打开设备;当然,也可将其设置为 SND_PCM_NONBLOCK,表示
以非阻塞方式打开设备。
关闭PCM设备API
int snd_pcm_close(snd_pcm_t *pcm);
2.2硬件参数设置
打开 PCM 设备之后,接着我们需要对设备进行设置,包括硬件配置和软件配置。软件配置就不再介绍
了,使用默认配置即可!我们主要是对硬件参数进行配置,譬如采样率、声道数、格式、访问类型、period周期大小、buffer 大小等。
snd_pcm_hw_params_t 数据类型描述PCM设备
在配置之前,需要实例化一个snd_pcm_hw_params_t对象。
snd_pcm_hw_params_t *hwparams = NULL;
//创建对象,申请内存
//方法一
snd_pcm_hw_params_malloc(&hwparams);
//方法二
snd_pcm_hw_params_alloca(&hwparams);
//释放对象
void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj)
补充:
malloc
和alloca
区别
malloc
和alloca
都是用于动态分配内存的函数,但是它们有一些区别。
malloc
是C标准库中的函数,它的作用是在堆上分配一块指定大小的内存区域,并返回该内存区域的地址。使用malloc
分配的内存区域在程序运行期间一直存在,直到显式调用free
函数释放该内存区域,或者程序结束时操作系统自动回收。
alloca
是一个非标准的函数,它通常由编译器提供支持。它的作用是在栈上分配一块指定大小的内存区域,并返回该内存区域的地址。使用alloca
分配的内存区域在函数返回时被自动释放,因此不需要显式调用free
函数来释放内存。但是,由于alloca
分配的内存区域在函数返回时被释放,因此不能在函数外部使用该内存区域。
在使用malloc
和alloca
时需要注意以下几点:
malloc
可以分配任意大小的内存区域,而alloca
只能分配栈大小范围内的内存区域。malloc
分配的内存区域需要手动释放,而alloca
分配的内存区域在函数返回时会自动释放。alloca
分配的内存是存储在栈中的,而栈的大小是有限的。如果分配的内存区域过大,可能会导致栈溢出。alloca
是一个非标准的函数,不是所有的编译器都支持。如果需要在不同的编译器间移植代码,最好不要使用alloca
。
因此,malloc
和alloca
适用于不同的场景。malloc
适用于需要动态分配大量内存的场景,而alloca
适用于需要动态分配小量内存的场景,并且内存区域的大小可以在编译期间确定。
初始化 snd_pcm_hw_params_t 对象
snd_pcm_hw_params_any(pcm_handle, hwparams);
对硬件参数进行设置
alsa-lib 提供了一系列的 snd_pcm_hw_params_set_xxx 函数用于设置 PCM 设备的硬件参数,同样也提供了一系列的 snd_pcm_hw_params_get_xxx 函数用于获取硬件参数。
(1)设置 access 访问类型: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
)
(2)设置数据格式:snd_pcm_hw_params_set_format()
(3)设置声道数:snd_pcm_hw_params_set_channels()
(4)设置采样率大小:snd_pcm_hw_params_set_rate()
(5)设置周期大小:snd_pcm_hw_params_set_period_size()
(6)设置 buffer 大小:snd_pcm_hw_params_set_buffer_size()
(7)安装/加载硬件配置参数:snd_pcm_hw_params()
2.3读/写数据
//播放
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
)
//注意;参数 buffer 指的是应用程序的缓冲区,不要与驱动层的环形缓冲区搞混了
阻塞与非阻塞
调用 snd_pcm_open()打开设备时,若指定为阻塞方式,则调用 snd_pcm_readi/snd_pcm_writei 以阻塞方式进行读/写。对于 PCM 录音来说,当 buffer 缓冲区中无数据可读时,调用 snd_pcm_readi()函数将会阻塞,直到音频设备向 buffer 中写入采集到的音频数据;同理,对于 PCM 播放来说,当 buffer 缓冲区中的数据满时,调用 snd_pcm_writei()函数将会阻塞,直到音频设备从 buffer 中读走数据进行播放。
若调用 snd_pcm_open()打开设备时,指定为非阻塞方式,则调用 snd_pcm_readi/snd_pcm_writei 以非阻塞方式进行读/写。对于 PCM 录音来说,当 buffer 缓冲区中无数据可读时,调用 snd_pcm_readi()不会阻塞、而是立即以错误形式返回;同理,对于 PCM 播放来说,当 buffer 缓冲区中的数据满时,调用 snd_pcm_writei()函数也不会阻塞、而是立即以错误形式返回。
snd_pcm_readn 和 snd_pcm_writen
snd_pcm_readi/snd_pcm_writei 适用于交错模式(interleaved)读/写数据,如果用户设置的访问类型并不是交错模式,而是非交错模式(non interleaved),此时便不可再使用 snd_pcm_readi/snd_pcm_writei 进行读写操作了,而需要使用 snd_pcm_readn 和 snd_pcm_writen 进行读写。