Linux音频了解

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

Linux音频了解_第1张图片

对于我们来说,学习音频应用编程其实就是学习 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 提供的一些示例代码,如下所示:

Linux音频了解_第2张图片

一些基本概念

  • 样本长度(Sample)
  • 声道数(channel)
  • 帧(frame)
  • 采样率(Sample rate)
  • 交错模式(interleaved)
  • 周期(period)
  • 缓冲区(buffer)

Linux音频了解_第3张图片

音频设备底层驱动程序使用 DMA 来搬运数据,这个 buffer 中有 4 个 period,每当 DMA 搬运完一个period 的数据就会触发一次中断,因此搬运整个 buffer 中的数据将产生 4 次中断。

数据之间的传输

Over and Under Run

2.编写一个音频应用程序

2.1打开 PCM 设备

需要包含头文件

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)

补充:

mallocalloca区别

mallocalloca都是用于动态分配内存的函数,但是它们有一些区别。

malloc是C标准库中的函数,它的作用是在堆上分配一块指定大小的内存区域,并返回该内存区域的地址。使用malloc分配的内存区域在程序运行期间一直存在,直到显式调用free函数释放该内存区域,或者程序结束时操作系统自动回收。

alloca是一个非标准的函数,它通常由编译器提供支持。它的作用是在栈上分配一块指定大小的内存区域,并返回该内存区域的地址。使用alloca分配的内存区域在函数返回时被自动释放,因此不需要显式调用free函数来释放内存。但是,由于alloca分配的内存区域在函数返回时被释放,因此不能在函数外部使用该内存区域。

在使用mallocalloca时需要注意以下几点:

  1. malloc可以分配任意大小的内存区域,而alloca只能分配栈大小范围内的内存区域。
  2. malloc分配的内存区域需要手动释放,而alloca分配的内存区域在函数返回时会自动释放。
  3. alloca分配的内存是存储在栈中的,而栈的大小是有限的。如果分配的内存区域过大,可能会导致栈溢出。
  4. alloca是一个非标准的函数,不是所有的编译器都支持。如果需要在不同的编译器间移植代码,最好不要使用alloca

因此,mallocalloca适用于不同的场景。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 进行读写。

3.编写播放器代码

你可能感兴趣的:(Linux应用开发,linux,音视频)