音频应用编程

ALPHA I.MX6U开发板支持音频,板上搭载了音频编解码芯片WM8960,支持播放以及录音功能!

本章我们来学习Linux下的音频应用编程,音频应用编程相比于前面几个章节所介绍的内容、其难度有所上升,但是笔者仅向大家介绍Linux音频应用编程中的基础知识,而更多细节、更加深入的内容需要大家自己去学习。

本章将会讨论如下主题内容。

ALSA概述

ALSA是Advanced Linux Sound Architecture(高级的Linux声音体系)的缩写,目前已经成为了linux下的主流音频体系架构,提供了音频和MIDI的支持,替代了原先旧版本中的OSS(开发声音系统);学习过Linux音频驱动开发的读者肯定知道这个;事实上,ALSA是Linux系统下一套标准的、先进的音频驱动框架,那么这套框架的设计本身是比较复杂的,采用分离、分层思想设计而成,具体的细节便不给大家介绍了!作为音频应用编程,我们不用去研究这个。

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

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(1)

图 29.1.1 alsa音频示意图

alsa-lib简介

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

用户空间的alsa-lib对应用程序提供了统一的API接口,这样可以隐藏驱动层的实现细节,简化了应用程序的实现难度、无需应用程序开发人员直接去读写音频设备节点。所以本章,对于我们来说,学习音频应用编程其实就是学习alsa-lib库函数的使用、如何基于alsa-lib库函数开发音频应用程序。

ALSA提供了关于alsa-lib的使用说明文档,其链接地址为:alsa-project/alsa-doc/alsa-lib/,进入到该链接地址后,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(2)

图 29.2.1 alsa-lib使用参考手册

alsa-lib库支持功能比较多,提供了丰富的API接口供应用程序开发人员调用,根据函数的功能、作用将这些API进行了分类,可以点击上图中Modules按钮查看其模块划分,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(3)

图 29.2.2 alsa-lib模块

一个分类就是一个模块(module),有些模块下可能该包含了子模块,譬如上图中,模块名称前面有三角箭头的表示该模块包含有子模块。

可以看到,alsa-lib提供的接口确实非常多、模块很多,以上所列举出来的这些模块,很多模块笔者也不是很清楚它们的具体功能、作用,但是本章我们仅涉及到三个模块下的API函数,包括:PCM Interface、Error Interface以及Mixer Interface。

PCM Interface

PCM Interface,提供了PCM设备相关的操作接口,譬如打开/关闭PCM设备、配置PCM设备硬件或软件参数、控制PCM设备(启动、暂停、恢复、写入/读取数据),该模块下还包含了一些子模块,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(4)

图 29.2.3 PCM Interface下的子模块

点击模块名称可以查看到该模块提供的API接口有哪些以及相应的函数说明,这里就不给大家演示了!

Error Interface

该模块提供了关于错误处理相关的接口,譬如函数调用发生错误时,可调用该模块下提供的函数打印错误描述信息。

Mixer Interface

提供了关于混音器相关的一系列操作接口,譬如音量、声道控制、增益等等。

sound设备节点

在Linux内核设备驱动层、基于ALSA音频驱动框架注册的sound设备会在/dev/snd目录下生成相应的设备节点文件,譬如ALPHA I.MX6U开发板出厂系统/dev/snd目录下有如下文件:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(5)

图 29.3.1 /dev/snd目录下的文件

Tips:注意,Mini I.MX6U开发板出厂系统/dev/snd目录下是没有这些文件的,因为Mini板不支持音频、没有板载音频编解码芯片,所以本章实验例程无法在Mini板上进行测试,请悉知!

从上图可以看到有如下设备文件:

本章我们编写的应用程序,虽然是调用alsa-lib库函数去控制底层音频硬件,但最终也是落实到对sound设备节点的I/O操作,只不过alsa-lib已经帮我们封装好了。在Linux系统的/proc/asound目录下,有很多的文件,这些文件记录了系统中声卡相关的信息,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(6)

图 29.3.2 /proc/asound目录下的文件

cards:

通过"cat /proc/asound/cards"命令、查看cards文件的内容,可列出系统中可用的、注册的声卡,如下所示:

cat /proc/asound/cards

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(7)

图 29.3.3 查看系统中注册的所有声卡

我们的阿尔法板子上只有一个声卡(WM8960音频编解码器),所以它的编号为0,也就是card0。系统中注册的所有声卡都会在/proc/asound/目录下存在一个相应的目录,该目录的命名方式为cardX(X表示声卡的编号),譬如图 29.3.2中的card0;card0目录下记录了声卡0相关的信息,譬如声卡的名字以及声卡注册的PCM设备,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(8)

图 29.3.4 card0目录下的文件

devices:

列出系统中所有声卡注册的设备,包括control、pcm、timer、seq等等。如下所示:

cat /proc/asound/devices

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(9)

图 29.3.5 列出所有设备

pcm:

列出系统中的所有PCM设备,包括playback和capture:

cat /proc/asound/pcm

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(10)

图 29.3.6 列出系统中所有PCM设备

alsa-lib移植

因为alsa-lib是ALSA提供的一套Linux下的C语言函数库,需要将alsa-lib移植到开发板上,这样基于alsa-lib编写的应用程序才能成功运行,除了移植alsa-lib库之外,通常还需要移植alsa-utils,alsa-utils包含了一些用于测试、配置声卡的工具。

事实上,ALPHA I.MX6U开发板出厂系统中已经移植了alsa-lib和alsa-utils,本章我们直接使用出厂系统移植好的alsa-lib和alsa-utils进行测试,笔者也就不再介绍移植过程了。其实它们的移植方法也非常简单,如果你想自己尝试移植,网上有很多参考,大家可以自己去看看。

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

aplay

aplay是一个用于测试音频播放功能程序,可以使用aplay播放wav格式的音频文件,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(11)

图 29.4.1 使用aplay播放wav音乐

程序运行之后就会开始播放音乐,因为ALPHA开发板支持喇叭和耳机自动切换,如果不插耳机默认从喇叭播放音乐,插上耳机以后喇叭就会停止播放,切换为耳机播放音乐,这个大家可以自己进行测试。

需要注意的是,aplay工具只能解析wav格式音频文件,不支持mp3格式解码,所以无法使用aplay工具播放mp3音频文件。稍后笔者会向大家介绍如何基于alsa-lib编写一个简单地音乐播放器,实现与aplay相同的效果。

alsamixer

alsamixer是一个很重要的工具,用于配置声卡的混音器,它是一个字符图形化的配置工具,直接在开发板串口终端运行alsamixer命令,打开图形化配置界面,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(12)

图 29.4.2 alsamixer界面

alsamixer可对声卡的混音器进行配置,左上角“Card: wm8960-audio”表示当前配置的声卡为wm8960-audio,如果你的系统中注册了多个声卡,可以按F6进行选择。

按下H键可查看界面的操作说明,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(13)

图 29.4.3 alsamixer界面操作说明

不同声卡支持的混音器配置选项是不同的,这个与具体硬件相关,需要硬件上的支持!上图展示的便是开发板WM8960声卡所支持的配置项,包括Playback播放和Capture录音,左上角View处提示:

View: F3:[Playback] F4: Capture F5: All

表示当前显示的是[Playback]的配置项,通过F4按键切换为Capture、或按F5显示所有配置项。

Tips:在终端按下F4或F5按键时,可能会直接退出配置界面,这个原因可能是F4或F5快捷键被其它程序给占用了,大家可以试试在Ubuntu系统下使用ssh远程登录开发板,然后在Ubuntu ssh终端执行alsamixer程序,笔者测试F4、F5都是正常的。

左上角Item处提示:

Item: Headphone [dB gain: -8.00, -8.00]

表示当前选择的是Headphone配置项,可通过键盘上的LEFT(向左)和RIGHT(向右)按键切换到其它配置项。当用户对配置项进行修改时,只能修改被选中的配置项,而中括号[dB gain: -7.00, -7.00]中的内容显示了该配置项当前的配置值。

上图中只是列出了其中一部分,还有一部分配置项并未显示出来,可以通过左右按键移动查看到其余配置项。WM8960声卡所支持的配置项特别多,包括播放音量、耳机音量、喇叭音量、capture录音音量、通道使能、ZC、AC、DC、ALC、3D等,配置项特别多,很多配置项笔者也不懂。以下列出了其中一些配置项及其说明:

Headphone:耳机音量,使用上(音量增加)、下(音量降低)按键可以调节播放时耳机输出的音量大小,当然可以通过Q(左声道音量增加)、Z(左声道音量降低)按键单独调节左声道音量或通过E(右声道音量增加)、C(右声道音量降低)按键单独调节右声道音量。

Headphone Playback ZC:耳机播放ZC(交流),通过M键打开或关闭ZC。

Speaker:喇叭播放音量,音量调节方法与Headphon相同。

Speaker AC:喇叭ZC,通过上下按键可调节大小。

Speaker DC:喇叭DC,通过上下按键可调节大小。

Speaker Playback ZC:喇叭播放ZC,通过M键打开或关闭ZC。

Playback:播放音量,播放音量作用于喇叭、也能作用于耳机,能同时控制喇叭和耳机的输出音量。调节方法与Headphon相同。

Capture:采集音量,也就是录音时的音量大小,调节方法与Headphon相同。

其它的配置项就不再介绍了,笔者也看不懂,后面会用到时再给大家解释!

开发板出厂系统中有一个配置文件/var/lib/alsa/asound.state,这其实就是WM8960声卡的配置文件,每当开发板启动进入系统时会自动读取该文件加载声卡配置;而每次系统关机时,又会将声卡当前的配置写入到该文件中进行保存,以便下一次启动时加载。加载与保存操作其实是通过alsactl工具完成的,稍后向大家介绍。

alsactl

配置好声卡之后,如果直接关机,下一次重启之后之前的设置都会消失,必须要重新设置,所以我们需要对配置进行保存,如何保存呢?可通过alsactl工具完成。

使用alsactl工具可以将当前声卡的配置保存在一个文件中,这个文件默认是/var/lib/alsa/asound.state,譬如使用alsactl工具将声卡配置保存在该文件中:

alsactl -f /var/lib/alsa/asound.state store

-f选项指定保存在哪一个文件中,当然也可以不用指定,如果不指定则使用alsactl默认的配置文件/var/lib/alsa/asound.state,store表示保存配置。保存成功以后就会生成/var/lib/alsa/asound.state这个文件,asound.state文件中保存了声卡的各种设置信息,大家可以打开此文件查看里面的内容,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(14)

图 29.4.4 asound.state文件部分内容

除了保存配置之外,还可以加载配置,譬如使用/var/lib/alsa/asound.state文件中的配置信息来配置声卡,可执行如下命令:

alsactl -f /var/lib/alsa/asound.state restore

restore表示加载配置,读取/var/lib/alsa/asound.state文件中的配置信息并对声卡进行设置。关于alsactl的详细使用方法,可以执行"alsactl -h"进行查看。

开发板出厂系统每次开机启动时便会自动从/var/lib/alsa/asound.state文件中读取配置信息并配置声卡,而每次关机时(譬如执行reset或poweroff命令)又会将声卡当前的配置写入到该文件中进行保存,以便下一次启动时加载。其实也就是在系统启动(或关机)时通过alsactl工具加载(或保存)配置。

amixer

amixer工具也是一个声卡配置工具,与alsamixer功能相同,区别在于,alsamixer是一个基于字符图形化的配置工具、而amixer不是图形化配置工具,直接使用命令行配置即可,详细地用法大家可以执行"amixer --help"命令查看,下面笔者简单地提一下该工具怎么用:

执行命令"amixer scontrols"可以查看到有哪些配置项,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(15)

图 29.4.5 查看有哪些配置项

从打印信息可知,这里打印出来的配置项与alsamixer配置界面中所看到的配置项是相同的,那如何进去配置呢?不同的配置项对应的配置方法(配置值或值类型)是不一样的,可以先使用命令"amixer scontents"查看配置项的说明,如下所示:

amixer scontents

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(16)

图 29.4.6 每一个配置项的配置说明

“Headphone”配置项用于设置耳机音量,音量可调节范围为0-127,当前音量为115(左右声道都是115);有些设置项是bool类型,只有on和off两种状态。

譬如将耳机音量左右声道都设置为100,可执行如下命令进行设置:

amixer sset Headphone 100,100

譬如打开或关闭Headphone Playback ZC:

amixer sset "Headphone Playback ZC" off #关闭ZC amixer sset "Headphone Playback ZC" on #打开ZC

以上给大家举了两个例子,配置方法还是很简单地!

arecord

arecord工具是一个用于录音测试的应用程序,这里笔者简单地给大家介绍一下工具的使用方法,详细的使用方法大家可以执行"arecord --help"命令查看帮助信息。譬如使用arecord录制一段10秒钟的音频,可以执行如下命令:

arecord -f cd -d 10 test.wav

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(17)

图 29.4.7 使用arecord工具录音

-f选项指定音频格式,cd则表示cd级别音频,也就是“16 bit little endian, 44100, stereo”;-d选项指定音频录制时间长度,单位是秒;test.wav指定音频数据保存的文件。当录制完成之后,会生成test.wav文件,接着我们可以使用aplay工具播放这一段音频。

以上给大家介绍了alsa-utils提供的几个测试音频、配置声卡的工具,当然,本文也只是进行了简单地介绍,更加详细的使用方法还需要大家自己查看帮助信息。

编写一个简单地alsa-lib应用程序

本小节开始,我们来学习如何基于alsa-lib编写音频应用程序,alsa-lib提供的库函数也别多,笔者肯定不会全部给大家介绍,只介绍基础的使用方法,关于更加深入、更加详细的使用方法需要大家自己去研究、学习。

对于alsa-lib库的使用,ALSA提供了一些参考资料来帮助应用程序开发人员快速上手alsa-lib、基于alsa-lib进行应用编程,以下笔者给出了链接:

users.suse/~mana/alsa090_howto.html alsa-project/alsa-doc/alsa-lib/examples.html

第一份文档向用户介绍了如何使用alsa-lib编写简单的音频应用程序,包括PCM播放音频、PCM录音等,笔者也是参考了这份文档来编写本章教程,对应初学者,建议大家看一看。

第二个链接地址是ALSA提供的一些示例代码,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(18)

图 29.5.1 ALSA提供的参考代码

点击对应源文件即可查看源代码。

以上便是ALSA提供的帮助文档以及参考代码,链接地址已经给出了,大家有兴趣可以看一下。

本小节笔者将向大家介绍如何基于alsa-lib编写一个简单地音频应用程序,譬如播放音乐、录音等;但在此之前,首先我们需要先来了解一些基本的概念,为后面的学习打下一个坚实的基础!

一些基本概念

主要是与音频相关的基本概念,因为在alsa-lib应用编程中会涉及到这些概念,所以先给大家进行一个简单地介绍。

样本长度(Sample)

样本是记录音频数据最基本的单元,样本长度就是采样位数,也称为位深度(Bit Depth、Sample Size、Sample Width)。是指计算机在采集和播放声音文件时,所使用数字声音信号的二进制位数,或者说每个采样样本所包含的位数(计算机对每个通道采样量化时数字比特位数),通常有8bit、16bit、24bit等。

声道数(channel)

分为单声道(Mono)和双声道/立体声(Stereo)。1表示单声道、2表示立体声。

帧(frame)

帧记录了一个声音单元,其长度为样本长度与声道数的乘积,一段音频数据就是由苦干帧组成的。

把所有声道中的数据加在一起叫做一帧,对于单声道:一帧 = 样本长度 * 1;双声道:一帧 = 样本长度 * 2。譬如对于样本长度为16bit的双声道来说,一帧的大小等于:16 * 2 / 8 = 4个字节。

采样率(Sample rate)

也叫采样频率,是指每秒钟采样次数,该次数是针对桢而言。譬如常见的采样率有:

8KHz - 电话所用采样率

22.05KHz - FM调频广播所用采样率

44.1KHz - 音频 CD,也常用于MPEG-1音频(VCD、SVCD、MP3)所用采样率

48KHz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率。

交错模式(interleaved)

交错模式是一种音频数据的记录方式,分为交错模式和非交错模式。在交错模式下,数据以连续桢的形式存放,即首先记录完桢1的左声道样本和右声道样本(假设为立体声格式),再记录桢2的左声道样本和右声道样本。而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。不过多数情况下,我们一般都是使用交错模式。

周期(period)

周期是音频设备处理(读、写)数据的单位,换句话说,也就是音频设备读写数据的单位是周期,每一次读或写一个周期的数据,一个周期包含若干个帧;譬如周期的大小为1024帧,则表示音频设备进行一次读或写操作的数据量大小为1024帧,假设一帧为4个字节,那么也就是1024*4=4096个字节数据。

一个周期其实就是两次硬件中断之间的帧数,音频设备每处理(读或写)完一个周期的数据就会产生一个中断,所以两个中断之间相差一个周期,关于中断的问题,稍后再向大家介绍!

缓冲区(buffer)

数据缓冲区,一个缓冲区包含若干个周期,所以buffer是由若干个周期所组成的一块空间。下面一张图直观地表示了buffer、period、frame、sample(样本长度)之间的关系,假设一个buffer包含4个周期、而一个周包含1024帧、一帧包含两个样本(左、右两个声道):

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(19)

图 29.5.2 buffer/period/frame/sample之间的关系示例图

音频设备底层驱动程序使用DMA来搬运数据,这个buffer中有4个period,每当DMA搬运完一个period的数据就会触发一次中断,因此搬运整个buffer中的数据将产生4次中断。ALSA为什么这样做?直接把整个buffer中的数据一次性搬运过去岂不是更快?情况并非如此,我们没有考虑到一个很重要的问题,那就是延迟;如果数据缓存区buffer很大,一次传输整个buffer中的数据可能会导致不可接受的延迟,因为一次搬运的数据量越大,所花费的时间就越长,那么必然会导致数据从传输开始到发出声音(以播放为例)这个过程所经历的时间就会越长,这就是延迟。为了解决这个问题,ALSA把缓存区拆分成多个周期,以周期为传输单元进行传输数据。

所以,周期不宜设置过大,周期过大会导致延迟过高;但周期也不能太小,周期太小会导致频繁触发中断,这样会使得CPU被频繁中断而无法执行其它的任务,使得效率降低!所以,周期大小要合适,在延迟可接受的情况下,尽量设置大一些,不过这个需要根据实际应用场合而定,有些应用场合,可能要求低延迟、实时性高,但有些应用场合没有这种需求。

数据之间的传输

这里再介绍一下数据之间传输的问题,这个问题很重要,大家一定要理解,这样会更好的帮助我们理解代码、理解代码的逻辑。

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

图 29.5.2中标识有read pointer和write pointer指针,write pointer指向当前应用程序写buffer的位置、read pointer指向当前音频设备读buffer的位置。在数据传输之前(播放之前),buffer缓冲区是没有数据的,此时write/read pointer均指向了buffer的起始位置,也就是第一个周期的起始位置,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(20)

图 29.5.3 pointer指向buffer起始位置

应用程序向buffer写入多少帧数据,则write pointer指针向前移动多少帧,当应用程序向buffer中写入一个周期的数据时,write pointer指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以此类推!当write pointer移动到buffer末尾时,又会回到buffer的起始位置,以此循环!所以由此可知,这是一个环形缓冲区。

以上是应用程序写buffer的一个过程,接着再来看看音频设备读buffer(播放)的过程。在播放开始之前,read pointer指向了buffer的起始位置,也就是第一个周期的起始位置。音频设备每次只播放一个周期的数据(读取一个周期),每一次都是从read pointer所指位置开始读取;每读取一个周期,read pointer指针向前移动一个周期,同样,当read pointer指针移动到buffer末尾时,又会回到buffer的起始位置,以此构成一个循环!

应用程序需要向buffer中写入音频数据,音频设备才能读取数据进行播放,如果read pointer所指向的周期并没有填充音频数据,则无法播放!当buffer数据满时,应用程序将不能再写入数据,否则就会覆盖之前的数据,必须要等待音频设备播放完一个周期,音频设备每播放完一个周期,这个周期就变成空闲状态了,此时应用程序就可以写入一个周期的数据以填充这个空闲周期。

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

回到图 29.5.2中,此时write pointer指向音频设备写buffer的位置、read pointer指向应用程序读buffer的位置。在录音开始之前,buffer缓冲区是没有数据的,此时write/read pointer均指向了buffer的起始位置,也就是第一个周期的起始位置,如图 29.5.3中所示。

音频设备向buffer写入多少帧数据,则write pointer指针向前移动多少帧,音频设备每次只采集一个周期,将采集到的数据写入buffer中,从write pointer所指位置开始写入;当音频设备向buffer中写入一个周期的数据时,write pointer指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以此类推!当write pointer移动到buffer末尾时,又会回到buffer的起始位置,以此构成循环!

以上是音频设备写buffer的一个过程,接着再来看看应用程序读buffer的过程。在录音开始之前,read pointer指向了buffer的起始位置,也就是第一个周期的起始位置。同样,应用程序从buffer读取了多少帧数据,则read pointer指针向前移动多少帧;从read pointer所指位置开始读取,当read pointer指针移动到buffer末尾时,又会回到buffer的起始位置,以此构成一个循环!

音频设备需要向buffer中写入音频数据,应用程序才能从buffer中读取数据(录音),如果read pointer所指向的周期并没有填充音频数据,则无法读取!当buffer中没有数据时,需要等待音频设备向buffer中写入数据,音频设备每次写入一个周期,当应用程序读取完这个周期的数据后,这个周期又变成了空闲周期,需要等待音频设备写入数据。

Over and Under Run

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

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(21)

图 29.5.4 buffer中数据的传输

上图展示了声卡在工作状态下,buffer中数据的传输情况,总是连续地在音频设备和应用程序缓存区间传输,但事情并不总是那么完美、也会出现有例外;譬如在录音例子中,如果应用程序读取数据不够快,环形缓冲区buffer中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖;这种数据的丢失被称为overrun。在播放例子中,如果应用程序写入数据到环形缓冲区buffer中的速度不够快,缓存区将会“饿死”(缓冲区中无数据可播放);这样的错误被称为underrun(欠载)。在ALSA文档中,将这两种情形统称为"XRUN",适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。

打开PCM设备

从本小节开始,将正式介绍如何编写一个音频应用程序,首先我们需要在应用程序中包含alsa-lib库的头文件<alsa/asoundlib.h>,这样才能在应用程序中调用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个参数,如下所示:

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

与snd_pcm_open相对应的是snd_pcm_close(),函数snd_pcm_close()用于关闭PCM设备,函数原型如下所示:

int snd_pcm_close(snd_pcm_t *pcm);

使用示例:

调用snd_pcm_open()函数打开声卡0的PCM播放设备0:

snd_pcm_t *pcm_handle = NULL; int ret; ret = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_open error: %s\n", snd_strerror(ret)); return -1; }

设置硬件参数

打开PCM设备之后,接着我们需要对设备进行设置,包括硬件配置和软件配置。软件配置就不再介绍了,使用默认配置即可!我们主要是对硬件参数进行配置,譬如采样率、声道数、格式、访问类型、period周期大小、buffer大小等。

实例化snd_pcm_hw_params_t对象

alsa-lib使用snd_pcm_hw_params_t数据类型来描述PCM设备的硬件配置参数,在配置参数之前,我们需要实例化一个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);

它们之间的区别也就是C库函数malloc和alloca之间的区别。当然,你也可以直接使用malloc()或alloca()来分配一个snd_pcm_hw_params_t对象,亦或者直接定义全局变量或栈自动变量。与snd_pcm_hw_params_malloc/snd_pcm_hw_params_alloca相对应的是snd_pcm_hw_params_free,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_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函数用于获取硬件参数。

(1)设置access访问类型:snd_pcm_hw_params_set_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 )

参数access指定设备的访问类型,是一个snd_pcm_access_t类型常量,这是一个枚举类型,如下所示:

enum snd_pcm_access_t { SND_PCM_ACCESS_MMAP_INTERLEAVED = 0, //mmap access with simple interleaved channels SND_PCM_ACCESS_MMAP_NONINTERLEAVED, //mmap access with simple non interleaved channels SND_PCM_ACCESS_MMAP_COMPLEX, //mmap access with complex placement SND_PCM_ACCESS_RW_INTERLEAVED, //snd_pcm_readi/snd_pcm_writei access SND_PCM_ACCESS_RW_NONINTERLEAVED, //snd_pcm_readn/snd_pcm_writen access SND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED };

通常,将访问类型设置为SND_PCM_ACCESS_RW_INTERLEAVED,交错访问模式,通过snd_pcm_readi/snd_pcm_writei对PCM设备进行读/写操作。

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

使用示例:

ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED); if (0 > ret) fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));

(2)设置数据格式:snd_pcm_hw_params_set_format()

调用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类型常量,这是一个枚举类型,如下所示:

enum snd_pcm_format_t { SND_PCM_FORMAT_UNKNOWN = -1, SND_PCM_FORMAT_S8 = 0, SND_PCM_FORMAT_U8, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE, SND_PCM_FORMAT_U16_LE, SND_PCM_FORMAT_U16_BE, SND_PCM_FORMAT_S24_LE, SND_PCM_FORMAT_S24_BE, SND_PCM_FORMAT_U24_LE, SND_PCM_FORMAT_U24_BE, SND_PCM_FORMAT_S32_LE, SND_PCM_FORMAT_S32_BE, SND_PCM_FORMAT_U32_LE, SND_PCM_FORMAT_U32_BE, SND_PCM_FORMAT_FLOAT_LE, SND_PCM_FORMAT_FLOAT_BE, SND_PCM_FORMAT_FLOAT64_LE, SND_PCM_FORMAT_FLOAT64_BE, SND_PCM_FORMAT_IEC958_SUBFRAME_LE, SND_PCM_FORMAT_IEC958_SUBFRAME_BE, SND_PCM_FORMAT_MU_LAW, SND_PCM_FORMAT_A_LAW, SND_PCM_FORMAT_IMA_ADPCM, SND_PCM_FORMAT_MPEG, SND_PCM_FORMAT_GSM, SND_PCM_FORMAT_S20_LE, SND_PCM_FORMAT_S20_BE, SND_PCM_FORMAT_U20_LE, SND_PCM_FORMAT_U20_BE, SND_PCM_FORMAT_SPECIAL = 31, SND_PCM_FORMAT_S24_3LE = 32, SND_PCM_FORMAT_S24_3BE, SND_PCM_FORMAT_U24_3LE, SND_PCM_FORMAT_U24_3BE, SND_PCM_FORMAT_S20_3LE, SND_PCM_FORMAT_S20_3BE, SND_PCM_FORMAT_U20_3LE, SND_PCM_FORMAT_U20_3BE, SND_PCM_FORMAT_S18_3LE, SND_PCM_FORMAT_S18_3BE, SND_PCM_FORMAT_U18_3LE, SND_PCM_FORMAT_U18_3BE, SND_PCM_FORMAT_G723_24, SND_PCM_FORMAT_G723_24_1B, SND_PCM_FORMAT_G723_40, SND_PCM_FORMAT_G723_40_1B, SND_PCM_FORMAT_DSD_U8, SND_PCM_FORMAT_DSD_U16_LE, SND_PCM_FORMAT_DSD_U32_LE, SND_PCM_FORMAT_DSD_U16_BE, SND_PCM_FORMAT_DSD_U32_BE, SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_DSD_U32_BE, SND_PCM_FORMAT_S16 = SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_U16 = SND_PCM_FORMAT_U16_LE, SND_PCM_FORMAT_S24 = SND_PCM_FORMAT_S24_LE, SND_PCM_FORMAT_U24 = SND_PCM_FORMAT_U24_LE, SND_PCM_FORMAT_S32 = SND_PCM_FORMAT_S32_LE, SND_PCM_FORMAT_U32 = SND_PCM_FORMAT_U32_LE, SND_PCM_FORMAT_FLOAT = SND_PCM_FORMAT_FLOAT_LE, SND_PCM_FORMAT_FLOAT64 = SND_PCM_FORMAT_FLOAT64_LE, SND_PCM_FORMAT_IEC958_SUBFRAME = SND_PCM_FORMAT_IEC958_SUBFRAME_LE, SND_PCM_FORMAT_S20 = SND_PCM_FORMAT_S20_LE, SND_PCM_FORMAT_U20 = SND_PCM_FORMAT_U20_LE };

用的最多的格式是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表示支持 }

(3)设置声道数:snd_pcm_hw_params_set_channels()

调用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的错误码。

使用示例:

ret = snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 2); if (0 > ret) fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));

(4)设置采样率大小:snd_pcm_hw_params_set_rate()

调用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的错误码。

使用示例:

ret = snd_pcm_hw_params_set_rate(pcm_handle, hwparams, 44100, 0); if (0 > ret) fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));

(5)设置周期大小:snd_pcm_hw_params_set_period_size()

这里说的周期,也就是29.5.1小节中向大家介绍的周期,一个周期的大小使用帧来衡量,譬如一个周期1024帧;调用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参数意义相同。

使用示例(将周期大小设置为1024帧):

ret = snd_pcm_hw_params_set_period_size(pcm_handle, hwparams, 1024, 0); if (0 > ret) fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));

注意,参数val的单位是帧、而不是字节。

(6)设置buffer大小:snd_pcm_hw_params_set_buffer_size()

调用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的大小是周期大小的整数倍,譬如16个周期;但函数snd_pcm_hw_params_set_buffer_size()是以帧为单位来表示buffer的大小,所以需要转换一下,譬如将buffer大小设置为16个周期,则参数val等于16 * 1024(假设一个周期为1024帧)=16384帧。

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

使用示例:

ret = snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, 16*1024); if (0 > ret) fprintf(stderr, "snd_pcm_hw_params_set_buffer_size error: %s\n", snd_strerror(ret));

除了snd_pcm_hw_params_set_buffer_size()函数之外,我们还可以调用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的错误码。

使用示例:

ret = snd_pcm_hw_params_set_periods(pcm_handle, hwparams, 16, 0); //buffer大小为16个周期

if (0 > ret) fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));

(7)安装/加载硬件配置参数:snd_pcm_hw_params()

参数设置完成之后,最后调用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。

设备有多种不同的状态,SND_PCM_STATE_PREPARED为其中一种,关于状态的问题,后面在向大家介绍。调用snd_pcm_prepare()函数会使得PCM设备处于SND_PCM_STATE_PREPARED状态(也就是处于一种准备好的状态)。

使用示例:

ret = snd_pcm_hw_params(pcm_handle, hwparams); if (0 > ret) fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));

读/写数据

接下来就可以进行读/写数据了,如果是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设备的句柄;调用snd_pcm_writei()函数,将参数buffer(应用程序的缓冲区)缓冲区中的数据写入到驱动层的播放环形缓冲区buffer中,参数size指定写入数据的大小,以帧为单位;通常情况下,每次调用snd_pcm_writei()写入一个周期数据。

调用snd_pcm_readi()函数,将从驱动层的录音环形缓冲区buffer中读取数据到参数buffer指定的缓冲区中(应用程序的缓冲区),参数size指定读取数据的大小,以帧为单位;通常情况下,每次调用snd_pcm_readi()读取一个周期数据。

Tips:snd_pcm_writei/snd_pcm_readi函数原型中,参数buffer指的是应用程序的缓冲区,不要与驱动层的环形缓冲区搞混了!

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

阻塞与非阻塞

调用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进行读写。

示例代码之PCM播放

通过上小节的一个介绍,相信大家对alsa-lib音频应用编程已经有了基本的认识和理解,本小节我们来编写一个简单地音乐播放器,可以播放WAV音频文件,代码笔者已经写好了,如下所示:

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_playback.c。

示例代码 29.5.1 一个简单地PCM播放示例程序 /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved. 文件名 : pcm_playback.c 作者 : 邓涛 版本 : V1.0 描述 : 一个简单地PCM播放示例代码--播放WAV音频文件 其他 : 无 论坛 : openedv 日志 : 初版 V1.0 2021/7/20 邓涛创建 ***************************************************************/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <alsa/asoundlib.h> /************************************ 宏定义 ************************************/ #define PCM_PLAYBACK_DEV "hw:0,0" /************************************ WAV音频文件解析相关数据结构申明 ************************************/ typedef struct WAV_RIFF { char ChunkID[4]; /* "RIFF" */ u_int32_t ChunkSize; /* 从下一个地址开始到文件末尾的总字节数 */ char Format[4]; /* "WAVE" */ } __attribute__ ((packed)) RIFF_t; typedef struct WAV_FMT { char Subchunk1ID[4]; /* "fmt " */ u_int32_t Subchunk1Size; /* 16 for PCM */ u_int16_t AudioFormat; /* PCM = 1*/ u_int16_t NumChannels; /* Mono = 1, Stereo = 2, etc. */ u_int32_t SampleRate; /* 8000, 44100, etc. */ u_int32_t ByteRate; /* = SampleRate * NumChannels * BitsPerSample/8 */ u_int16_t BlockAlign; /* = NumChannels * BitsPerSample/8 */ u_int16_t BitsPerSample; /* 8bits, 16bits, etc. */ } __attribute__ ((packed)) FMT_t; static FMT_t wav_fmt; typedef struct WAV_DATA { char Subchunk2ID[4]; /* "data" */ u_int32_t Subchunk2Size; /* data size */ } __attribute__ ((packed)) DATA_t; /************************************ static静态全局变量定义 ************************************/ static snd_pcm_t *pcm = NULL; //pcm句柄 static unsigned int buf_bytes; //应用程序缓冲区的大小(字节为单位) static void *buf = NULL; //指向应用程序缓冲区的指针 static int fd = -1; //指向WAV音频文件的文件描述符 static snd_pcm_uframes_t period_size = 1024; //周期大小(单位: 帧) static unsigned int periods = 16; //周期数(设备驱动层buffer的大小) static int snd_pcm_init(void) { snd_pcm_hw_params_t *hwparams = NULL; int ret; /* 打开PCM设备 */ ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_open error: %s: %s\n", PCM_PLAYBACK_DEV, snd_strerror(ret)); return -1; } /* 实例化hwparams对象 */ snd_pcm_hw_params_malloc(&hwparams); /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */ ret = snd_pcm_hw_params_any(pcm, hwparams); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret)); goto err2; } /************** 设置参数 ***************/ /* 设置访问类型: 交错模式 */ ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret)); goto err2; } /* 设置数据格式: 有符号16位、小端模式 */ ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret)); goto err2; } /* 设置采样率 */ ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret)); goto err2; } /* 设置声道数: 双声道 */ ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret)); goto err2; } /* 设置周期大小: period_size */ ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret)); goto err2; } /* 设置周期数(驱动层buffer的大小): periods */ ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret)); goto err2; } /* 使配置生效 */ ret = snd_pcm_hw_params(pcm, hwparams); snd_pcm_hw_params_free(hwparams); //释放hwparams对象占用的内存 if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret)); goto err1; } buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小 return 0; err2: snd_pcm_hw_params_free(hwparams); //释放内存 err1: snd_pcm_close(pcm); //关闭pcm设备 return -1; } static int open_wav_file(const char *file) { RIFF_t wav_riff; DATA_t wav_data; int ret; fd = open(file, O_RDONLY); if (0 > fd) { fprintf(stderr, "open error: %s: %s\n", file, strerror(errno)); return -1; } /* 读取RIFF chunk */ ret = read(fd, &wav_riff, sizeof(RIFF_t)); if (sizeof(RIFF_t) != ret) { if (0 > ret) perror("read error"); else fprintf(stderr, "check error: %s\n", file); close(fd); return -1; } if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验 strncmp("WAVE", wav_riff.Format, 4)) { fprintf(stderr, "check error: %s\n", file); close(fd); return -1; } /* 读取sub-chunk-fmt */ ret = read(fd, &wav_fmt, sizeof(FMT_t)); if (sizeof(FMT_t) != ret) { if (0 > ret) perror("read error"); else fprintf(stderr, "check error: %s\n", file); close(fd); return -1; } if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验 fprintf(stderr, "check error: %s\n", file); close(fd); return -1; } /* 打印音频文件的信息 */ printf("<<<<音频文件格式信息>>>>\n\n"); printf(" file name: %s\n", file); printf(" Subchunk1Size: %u\n", wav_fmt.Subchunk1Size); printf(" AudioFormat: %u\n", wav_fmt.AudioFormat); printf(" NumChannels: %u\n", wav_fmt.NumChannels); printf(" SampleRate: %u\n", wav_fmt.SampleRate); printf(" ByteRate: %u\n", wav_fmt.ByteRate); printf(" BlockAlign: %u\n", wav_fmt.BlockAlign); printf(" BitsPerSample: %u\n\n", wav_fmt.BitsPerSample); /* sub-chunk-data */ if (0 > lseek(fd, sizeof(RIFF_t) 8 wav_fmt.Subchunk1Size, SEEK_SET)) { perror("lseek error"); close(fd); return -1; } while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) { /* 找到sub-chunk-data */ if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验 return 0; if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) { perror("lseek error"); close(fd); return -1; } } fprintf(stderr, "check error: %s\n", file); return -1; } /************************************ main主函数 ************************************/ int main(int argc, char *argv[]) { int ret; if (2 != argc) { fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]); exit(EXIT_FAILURE); } /* 打开WAV音频文件 */ if (open_wav_file(argv[1])) exit(EXIT_FAILURE); /* 初始化PCM Playback设备 */ if (snd_pcm_init()) goto err1; /* 申请读缓冲区 */ buf = malloc(buf_bytes); if (NULL == buf) { perror("malloc error"); goto err2; } /* 播放 */ for ( ; ; ) { memset(buf, 0x00, buf_bytes); //buf清零 ret = read(fd, buf, buf_bytes); //从音频文件中读取数据 if (0 >= ret) // 如果读取出错或文件读取完毕 goto err3; ret = snd_pcm_writei(pcm, buf, period_size); if (0 > ret) { fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret)); goto err3; } else if (ret < period_size) {//实际写入的帧数小于指定的帧数 //此时我们需要调整下音频文件的读位置 //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节 //frame_bytes表示一帧的字节大小 if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) { perror("lseek error"); goto err3; } } } err3: free(buf); //释放内存 err2: snd_pcm_close(pcm); //关闭pcm设备 err1: close(fd); //关闭打开的音频文件 exit(EXIT_FAILURE); }

本应用程序实现可以播放WAV音频文件,关于WAV文件格式的解析,本文档不作说明,WAV文件格式其实非常简单,大家自己百度了解。

在main()函数中,首先对参数进行了校验,执行测试程序需要用户传入一个参数,这个参数用于指定一个需要播放的WAV音频文件。接着调用自定义函数open_wav_file()对WAV文件进行解析,其实也就是对它的头部数据进行校验、解析,获取音频格式信息以及音频数据的位置偏移量。

接着调用自定义函数snd_pcm_init()对PCM设备进行初始化,在snd_pcm_init()函数中,首先调用alsa-lib库函数snd_pcm_open()打开PCM播放设备,接着对PCM设备硬件参数进行设置,包括:访问类型、数据格式、采样率、声道数、周期大小以及buffer的大小,这些内容前面已经给大家详细介绍过,这里不再重述!

回到main()函数,调用C库函数malloc()申请分配一个缓冲区,用于存放从音频文件中读取出来的音频数据。

一切准备好之后,就可以播放音频了,在for循环中,首先调用read()函数从音频文件中读取出音频数据,每次读取一个周期,将读取到的数据存放在buf指向的缓冲区中,接着调用alsa-lib库函数snd_pcm_writei()写入数据进行播放。示例程序中调用snd_pcm_open()时使用的是阻塞方式,当驱动层环形缓冲区buffer还未满时,调用snd_pcm_writei()并不会阻塞,而是会将数据写入到环形缓冲区中、然后返回;调用一次snd_pcm_writei()写入一个周期数据、调用一次再写入一个周期;当环形缓冲区数据满时,调用snd_pcm_writei()会阻塞,直到音频设备播放完一个周期、此时会出现一个空闲周期,接着snd_pcm_writei()将数据填充到这个空闲周期后返回。

以上对示例代码进行了一个简单地介绍,代码本身非常简单,没什么难点,代码中注释信息也已经描述地比较清楚了,相信大家都可以看懂。需要注意,必须要在源码中包含alsa-lib的头文件<alsa/asoundlib.h>!

编译示例代码

接下来编译上述示例代码,编译的方法非常简单,按照以前的惯例,编译时无非是要指定两个路径(alsa-lib头文件所在路径、alsa-lib库文件所在路径)以及链接库(需要链接的库文件名称),譬如:

${CC} -o testApp testApp.c -Ixxx -Lyyy -lzzz

xxx表示头文件的路径,yyy表示库文件的路径,zzz表示链接库。

但是我们并没有自己移植alsa-lib,也就意味着我们在Ubuntu下并没有移植、安装alsa-lib,所以这些路径无法指定。其实,我们使用的交叉编译工具对应的安装目录下已经安装了alsa-lib,进入到交叉编译工具安装目录下的sysroots/cortexa7hf-neon-poky-linux-gnueabi目录,譬如笔者使用的Ubuntu系统,交叉编译工具安装路径为/opt/fsl-imx-x11/4.1.15-2.1.0。

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(22)

图 29.5.5 cortexa7hf-neon-poky-linux-gnueabi目录下的文件夹

该目录下有两个目录,lib和usr,这两个目录其实就是Linux系统根目录下的lib和usr;所以lib目录下存放了一些链接库文件,usr目录下包含了include和lib目录,分别存放了头文件和链接库文件。usr/include/alsa目录下存放了alsa-lib的头文件,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(23)

图 29.5.6 alsa-lib的头文件

我们需要包含的头文件asoundlib.h头文件就在该目录下。

usr/lib目录下包含了alsa-lib库文件,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(24)

图 29.5.7 alsa-lib库文件

alsa-lib链接库libasound.so就在该目录下。那既然找到了alsa-lib的头文件路径和库文件路径,编译应用程序时直接指定这些路径即可。但我们不需要自己手动指定这些路径,交叉编译器已经把这些路径添加到它的搜索路径中了,使用echo ${CC}查看环境变量CC的内容,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(25)

图 29.5.8 CC环境变量的内容

其中交叉编译器arm-poky-linux-gnueabi-gcc有一个选--sysroot,它指定了一个路径,这个路径就是交叉编译工具安装目录下的sysroots/cortexa7hf-neon-poky-linux-gnueabi目录,--sysroot选项用于设置目标平台的根目录,设置了平台根目录之后,当编译应用程序时,编译器会将根目录下的usr/include添加到头文件搜索路径中、将根目录下的lib和usr/lib添加到库文件搜索路径中。

所以由此可知,编译应用程序时,我们只需指定链接库即可,如下所示:

${CC} -o testApp testApp.c -lasound

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(26)

图 29.5.9 编译应用程序

测试应用程序

将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,并拷贝一个WAV音频文件到/home/root目录下,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(27)

图 29.5.10 将测试程序和WAV音频文件拷贝到开发板家目录

接着进行测试,在测试之前,我们还需要对声卡混音器进行配置,当然,你也可以不配置,因为开发板出厂系统中声卡是已经配置好的。这里我们直接使用amixer工具进行配置,配置如下:

# 打开耳机播放ZC amixer sset 'Headphone Playback ZC' on # 打开喇叭播放ZC amixer sset 'Speaker Playback ZC' on amixer sset 'Speaker AC' 3 amixer sset 'Speaker DC' 3 # 音量设置 amixer sset Headphone 105,105 //耳机音量设置 amixer sset Playback 230,230 //播放音量设置 amixer sset Speaker 118,118 //喇叭音量设置 # 打开左右声道 amixer sset 'Right Output Mixer PCM' on //打开右声道 amixer sset 'Left Output Mixer PCM' on //打开左声道

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(28)

图 29.5.11 声卡设置

由于篇幅有限,打印信息不能给大家全部截取出来。声音的大小,大家根据情况进行调节。

声卡设置完成之后,接着运行测试程序,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(29)

图 29.5.12 执行测试程序

程序运行之后,对传入的WAV文件进行解析,并将其音频格式信息打印出来。

此时开发板喇叭便会开始播放音乐,如果连接了耳机,则会通过耳机播放音乐。

示例代码值PCM录音

本小节我们来编写一个PCM音频录制(录音)的测试程序,示例代码笔者已经给出,如下所示:

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_capture.c。

示例代码 29.5.2 一个简单地PCM录音示例程序 /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved. 文件名 : pcm_capture.c 作者 : 邓涛 版本 : V1.0 描述 : 一个简单地PCM音频采集示例代码--录音 其他 : 无 论坛 : openedv 日志 : 初版 V1.0 2021/7/20 邓涛创建 ***************************************************************/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <alsa/asoundlib.h> /************************************ 宏定义 ************************************/ #define PCM_CAPTURE_DEV "hw:0,0" /************************************ static静态全局变量定义 ************************************/ static snd_pcm_t *pcm = NULL; //pcm句柄 static snd_pcm_uframes_t period_size = 1024; //周期大小(单位: 帧) static unsigned int periods = 16; //周期数(buffer的大小) static unsigned int rate = 44100; //采样率 static int snd_pcm_init(void) { snd_pcm_hw_params_t *hwparams = NULL; int ret; /* 打开PCM设备 */ ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_open error: %s: %s\n", PCM_CAPTURE_DEV, snd_strerror(ret)); return -1; } /* 实例化hwparams对象 */ snd_pcm_hw_params_malloc(&hwparams); /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */ ret = snd_pcm_hw_params_any(pcm, hwparams); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret)); goto err2; } /************** 设置参数 ***************/ /* 设置访问类型: 交错模式 */ ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret)); goto err2; } /* 设置数据格式: 有符号16位、小端模式 */ ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret)); goto err2; } /* 设置采样率 */ ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret)); goto err2; } /* 设置声道数: 双声道 */ ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret)); goto err2; } /* 设置周期大小: period_size */ ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret)); goto err2; } /* 设置周期数(buffer的大小): periods */ ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0); if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret)); goto err2; } /* 使配置生效 */ ret = snd_pcm_hw_params(pcm, hwparams); snd_pcm_hw_params_free(hwparams); //释放hwparams对象占用的内存 if (0 > ret) { fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret)); goto err1; } return 0; err2: snd_pcm_hw_params_free(hwparams); //释放内存 err1: snd_pcm_close(pcm); //关闭pcm设备 return -1; } /************************************ main主函数 ************************************/ int main(int argc, char *argv[]) { unsigned char *buf = NULL; unsigned int buf_bytes; int fd = -1; int ret; if (2 != argc) { fprintf(stderr, "Usage: %s <output_file>\n", argv[0]); exit(EXIT_FAILURE); } /* 初始化PCM Capture设备 */ if (snd_pcm_init()) exit(EXIT_FAILURE); /* 申请读缓冲区 */ buf_bytes = period_size * 4; //字节大小 = 周期大小*帧的字节大小 16位双声道 buf = malloc(buf_bytes); if (NULL == buf) { perror("malloc error"); goto err1; } /* 打开一个新建文件 */ fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL); if (0 > fd) { fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno)); goto err2; } /* 录音 */ for ( ; ; ) { //memset(buf, 0x00, buf_bytes); //buf清零 ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期 if (0 > ret) { fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret)); goto err3; } // snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数 ret = write(fd, buf, ret * 4); //将读取到的数据写入文件中 if (0 >= ret) goto err3; } err3: close(fd); //关闭文件 err2: free(buf); //释放内存 err1: snd_pcm_close(pcm); //关闭pcm设备 exit(EXIT_FAILURE); }

在main()函数中,首先对参数进行了校验,执行测试程序需要用户传入一个参数,指定输出文件,因为示例程序中会将录制的音频数据保存到该文件中。

接着调用自定义函数snd_pcm_init()对PCM设备进行初始化,在snd_pcm_init()函数中,首先调用alsa-lib库函数snd_pcm_open()打开PCM录音设备,接着对PCM设备硬件参数进行设置,访问类型设置交错模式SND_PCM_ACCESS_RW_INTERLEAVED、数据格式设置为SND_PCM_FORMAT_S16_LE、采样率设置为44100、双声道、周期大小设置为1024帧、buffer大小设置为16个周期。

回到main()函数,调用C库函数malloc()申请分配一个缓冲区,用于存放从驱动层环形缓冲区buffer读取出来的音频数据。并打开一个新建文件(因为使用了O_CREAT | O_EXCL标志)。

一切准备好之后,就可以进行音频录制了,在for循环中,首先调用alsa-lib库函数snd_pcm_readi()从环形缓冲区中读取音频设备采集到的音频数据,读取出来之后调用write()函数将数据写入到文件中。示例程序中调用snd_pcm_open()时使用的是阻塞方式,当环形缓冲区buffer中有数据可读时,调用snd_pcm_readi()并不会阻塞,而是读取出数据、然后返回;调用一次snd_pcm_readi()读取一个周期、调用一次再读取一个周期;当环形缓冲区为空时,调用snd_pcm_readi()会阻塞,直到音频设备采集到一个周期数据、此时被阻塞snd_pcm_readi()调用被唤醒、读取这一个周期然后返回。

编译示例代码

接下来我们编译示例代码,如下所示:

${CC} -o testApp testApp.c -lasound

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(30)

图 29.5.13 编译示例代码

测试应用程序

将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,在执行测试程序之前,我们需要对声卡进行配置,同样使用amixer工具进行配置,如下:

amixer sset Capture 58,58 //录制音量大小 amixer sset 'ADC PCM' 200,200 //PCM ADC # 左声道Mixer Boost管理 amixer sset 'Left Input Mixer Boost' off amixer sset 'Left Boost Mixer LINPUT1' off amixer sset 'Left Input Boost Mixer LINPUT1' 0 amixer sset 'Left Boost Mixer LINPUT2' off amixer sset 'Left Input Boost Mixer LINPUT2' 0 amixer sset 'Left Boost Mixer LINPUT3' off amixer sset 'Left Input Boost Mixer LINPUT3' 0 # 右声道Mixer Boost管理 amixer sset 'Right Input Mixer Boost' on amixer sset 'Right Boost Mixer RINPUT1' on amixer sset 'Right Input Boost Mixer RINPUT1' 5 amixer sset 'Right Boost Mixer RINPUT2' on amixer sset 'Right Input Boost Mixer RINPUT2' 5 amixer sset 'Right Boost Mixer RINPUT3' off amixer sset 'Right Input Boost Mixer RINPUT3' 0

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(31)

图 29.5.14 声卡配置(录音)

左右声道的Mixer Boost(混音器增强)为什么要这样去配置?这个与硬件设计有关系,我们就不去解释这个了。具体详情可以参考《I.MX6U嵌入式Linux驱动开发指南》文档中音频驱动章节的内容。

接下来,执行测试程序进行录音,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(32)

图 29.5.15 录音

执行测试程序之后,就开始录音了,接着我们可以对着底板上的麦(MIC)说话,板载的MIC如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(33)

图 29.5.16 板载麦克风

程序就会把我们说的话录进去;如果想要停止录音、只能终止进程,按Ctrl C终止应用程序;此时在当前目录下会生成cap.wav音频文件,如下所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(34)

图 29.5.17 生成cap.wav文件

生成的文件是一个纯音频数据的文件,并不是WAV格式的文件,因为这个文件没有头部信息,程序中如果检测到该文件不是WAV格式文件、会直接退出,所以不能直接使用上小节29.5.5的测试程序播放cap.wav文件,这里要注意!当然你可以对上小节的示例代码进行修改,也可直接使用aplay工具播放这段录制的音频,如下:

aplay -f cd cap.wav

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(35)

图 29.5.18 使用aplay播放录制的音频

如果录制正常,使用aplay播放出来的声音就是我们录制的声音!

LINE_IN测试

除了麦克风之外,开发板底板上还有一个LINE_IN接口,也就是线路输入,如下图所示:

正点原子linux 开发版评测(正点原子I.MX6U嵌入式Linux)(36)

图 29.5.19 LINE_IN接口

上图中左边的是耳机接口、右边的是LINE_IN接口,支持音频输入,我们通过本测试程序对LINE_IN接口进行测试,采集LINE_IN接口输入的音频。测试时我们使用一根3.5mm公对公音频线,一头连接到手机或者电脑、另外一头连接到LINE_IN接口上,然后手机或电脑端播放音乐,那么音频数据就会通过LINE_IN接口输入到开发板被我们的应用程序采集(录制)。

在测试之前,我们需要对声卡进行配置,如下所示:

amixer sset Capture 58,58 //录制音量大小 amixer sset 'ADC PCM' 200,200 //PCM ADC # 左声道Mixer Boost管理 amixer sset 'Left Input Mixer Boost' off amixer sset 'Left Boost Mixer LINPUT1' off amixer sset 'Left Input Boost Mixer LINPUT1' 0 amixer sset 'Left Boost Mixer LINPUT2' on amixer sset 'Left Input Boost Mixer LINPUT2' 5 amixer sset 'Left Boost Mixer LINPUT3' off amixer sset 'Left Input Boost Mixer LINPUT3' 0 # 右声道Mixer Boost管理 amixer sset 'Right Input Mixer Boost' on amixer sset 'Right Boost Mixer RINPUT1' off amixer sset 'Right Input Boost Mixer RINPUT1' 0 amixer sset 'Right Boost Mixer RINPUT2' off amixer sset 'Right Input Boost Mixer RINPUT2' 0 amixer sset 'Right Boost Mixer RINPUT3' on amixer sset 'Right Input Boost Mixer RINPUT3' 5

配置 好之后就可以进行测试了,执行程序之后,手机或电脑端播放音乐,开发板采集从LINE_IN接口输入的音频数据,测试方式跟MIC麦克风一样,大家自己去测试!

,