ALPHA I.MX6U 开发板支持音频,板上搭载了音频编解码芯片 WM8960,支持播放以及录音功能!本章我们来学习 Linux 下的音频应用编程, 音频应用编程相比于前面几个章节所介绍的内容、 其难度有所上升, 但是笔者仅向大家介绍 Linux 音频应用编程中的基础知识,而更多细节、更加深入的内容需要大家自己去学习。本章将会讨论如下主题内容。
⚫ Linux 下 ALSA 框架概述;
⚫ alsa-lib 库介绍;
⚫ alsa-lib 库移植;
⚫ alsa-lib 库的使用;
⚫ 音频应用编程之播放;
⚫ 音频应用编程之录音。
ALSA 概述
ALSA 是 Advanced Linux Sound Architecture(高级的 Linux 声音体系) 的缩写,目前已经成为了 linux下的主流音频体系架构, 提供了音频和 MIDI 的支持,替代了原先旧版本中的 OSS(开发声音系统); 学习过 Linux 音频驱动开发的读者肯定知道这个; 事实上, ALSA 是 Linux 系统下一套标准的、先进的音频驱动框架, 那么这套框架的设计本身是比较复杂的, 采用分离、分层思想设计而成,具体的细节便不给大家介绍了! 作为音频应用编程,我们不用去研究这个。在应用层, ALSA 为我们提供了一套标准的 API,应用程序只需要调用这些 API 就可完成对底层音频硬件设备的控制, 譬如播放、录音等, 这一套 API 称为 alsa-lib。 如下图所示:
alsa-lib 简介
如上所述, alsa-lib 是一套 Linux 应用层的 C 语言函数库, 为音频应用程序开发提供了一套统一、标准的接口,应用程序只需调用这一套 API 即可完成对底层声卡设备的操控,譬如播放与录音。用户空间的 alsa-lib 对应用程序提供了统一的 API 接口,这样可以隐藏驱动层的实现细节,简化了应用程序的实现难度、无需应用程序开发人员直接去读写音频设备节点。 所以本章,对于我们来说,学习音频应用编程其实就是学习 alsa-lib 库函数的使用、如何基于 alsa-lib 库函数开发音频应用程序。ALSA 提供了关于 alsa-lib 的使用说明文档,其链接地址为: https://www.alsa-project.org/alsa-doc/alsa-lib/,进入到该链接地址后,如下所示:
alsa-lib 库支持功能比较多, 提供了丰富的 API 接口供应用程序开发人员调用, 根据函数的功能、作用将这些 API 进行了分类, 可以点击上图中 Modules 按钮查看其模块划分, 如下所示:
一个分类就是一个模块(module),有些模块下可能包含了子模块,譬如上图中,模块名称前面有三角箭头的表示该模块包含有子模块。
⚫ Global defines and functions: 包括一些全局的定义,譬如函数、宏等;
⚫ Constants for Digital Audio Interfaces: 数字音频接口相关的常量;
⚫ Input Interface: 输入接口;
⚫ Output Interface: 输出接口;
⚫ Error handling: 错误处理相关接口;
⚫ Configuration Interface: 配置接口;
⚫ Control Interface: 控制接口;
⚫ PCM Interface: PCM 设备接口;
⚫ RawMidi Interface: RawMidi 接口;
⚫ Timer Interface: 定时器接口;
⚫ Hardware Dependant Interface: 硬件相关接口;
⚫ MIDI Sequencer: MIDI 音序器;
⚫ External PCM plugin SDK: 外部 PCM 插件 SDK;
⚫ External Control Plugin SDK: 外部控制插件 SDK;
⚫ Mixer Interface: 混音器接口;
⚫ Use Case Interface: 用例接口;
⚫ Topology Interface: 拓扑接口。
可以看到, alsa-lib 提供的接口确实非常多、 模块很多, 以上所列举出来的这些模块,很多模块笔者也不是很清楚它们的具体功能、作用, 但是本章我们仅涉及到三个模块下的 API 函数,包括: PCM Interface、Error Interface 以及 Mixer Interface。
PCM Interface
PCM Interface,提供了 PCM 设备相关的操作接口,譬如打开/关闭 PCM 设备、配置 PCM 设备硬件或软件参数、控制 PCM 设备(启动、暂停、恢复、写入/读取数据) ,该模块下还包含了一些子模块,如下所示:
Error Interface
该模块提供了关于错误处理相关的接口,譬如函数调用发生错误时,可调用该模块下提供的函数打印错误描述信息。
Mixer Interface
提供了关于混音器相关的一系列操作接口,譬如音量、 声道控制、增益等等。
sound 设备节点
在 Linux 内核设备驱动层、基于 ALSA 音频驱动框架注册的 sound 设备会在/dev/snd 目录下生成相应的设备节点文件,譬如 ALPHA I.MX6U 开发板出厂系统/dev/snd 目录下有如下文件:
从上图可以看到有如下设备文件:
⚫ controlC0: 用于声卡控制的设备节点, 譬如通道选择、 混音器、 麦克风的控制等, C0 表示声卡 0(card0);
⚫ pcmC0D0c: 用于录音的 PCM 设备节点。其中 C0 表示 card0,也就是声卡 0;而 D0 表示 device0,也就是设备 0;最后一个字母 c 是 capture 的缩写,表示录音;所以 pcmC0D0c 便是系统的声卡0 中的录音设备 0;
⚫ pcmC0D0p: 用于播放(或叫放音、回放)的 PCM 设备节点。其中 C0 表示 card0,也就是声卡 0;而 D0 表示 device 0,也就是设备 0;最后一个字母 p 是 playback 的缩写,表示播放; 所以 pcmC0D0p便是系统的声卡 0 中的播放设备 0;
⚫ pcmC0D1c: 用于录音的 PCM 设备节点。对应系统的声卡 0 中的录音设备 1;
⚫ pcmC0D1p: 用于播放的 PCM 设备节点。对应系统的声卡 0 中的播放设备 1。
⚫ timer: 定时器。
本章我们编写的应用程序,虽然是调用 alsa-lib 库函数去控制底层音频硬件,但最终也是落实到对 sound设备节点的 I/O 操作,只不过 alsa-lib 已经帮我们封装好了。在 Linux 系统的/proc/asound 目录下,有很多的文件,这些文件记录了系统中声卡相关的信息,如下所示:
cards:
通过"cat /proc/asound/cards"命令、查看 cards 文件的内容,可列出系统中可用的、注册的声卡,如下所示:
我们的板子上只有一个声卡(WM8960 音频编解码器),所以它的编号为 0,也就是 card0。系统中注册的所有声卡都会在/proc/asound/目录下存在一个相应的目录,该目录的命名方式为 cardX(X 表示声卡的编号),card0 目录下记录了声卡 0 相关的信息,譬如声卡的名字以及声卡注册的 PCM 设备,如下所示:
devices:
列出系统中所有声卡注册的设备,包括 control、 pcm、 timer、 seq 等等。 如下所示:
pcm:
列出系统中的所有 PCM 设备,包括 playback 和 capture:
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 格式的音频文件,如下所示:
程序运行之后就会开始播放音乐,因为 ALPHA 开发板支持喇叭和耳机自动切换,如果不插耳机默认从喇叭播放音乐,插上耳机以后喇叭就会停止播放,切换为耳机播放音乐,这个大家可以自己进行测试。需要注意的是, aplay 工具只能解析 wav 格式音频文件,不支持 mp3 格式解码,所以无法使用 aplay 工具播放 mp3 音频文件。 稍后笔者会向大家介绍如何基于 alsa-lib 编写一个简单地音乐播放器,实现与 aplay相同的效果。
alsamixer
alsamixer 是一个很重要的工具,用于配置声卡的混音器, 它是一个字符图形化的配置工具,直接在开发板串口终端运行 alsamixer 命令,打开图形化配置界面,如下所示:
alsamixer 可对声卡的混音器进行配置,左上角“Card: wm8960-audio”表示当前配置的声卡为 wm8960-audio,如果你的系统中注册了多个声卡,可以按 F6 进行选择。按下 H 键可查看界面的操作说明,如下所示:
不同声卡支持的混音器配置选项是不同的,这个与具体硬件相关,需要硬件上的支持!上图展示的便是开发板 WM8960 声卡所支持的配置项, 包括 Playback 播放和 Capture 录音,左上角 View 处提示:
View: F3:[Playback] F4: Capture F5: All
表示当前显示的是[Playback]的配置项, 通过 F4 按键切换为 Capture、或按 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 文件中保存了声卡的各种设置信息,大家可以打开此文件查看里面的内容, 如下所示:
除了保存配置之外,还可以加载配置,譬如使用/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"可以查看到有哪些配置项,如下所示:
从打印信息可知,这里打印出来的配置项与 alsamixer 配置界面中所看到的配置项是相同的, 那如何进去配置呢?不同的配置项对应的配置方法(配置值或值类型)是不一样的,可以先使用命令"amixer scontents"查看配置项的说明, 如下所示:
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
-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 进行应用编程,以下笔者给出了链接:
https://users.suse.com/~mana/alsa090_howto.html
https://www.alsa-project.org/alsa-doc/alsa-lib/examples.html
第一份文档向用户介绍了如何使用 alsa-lib 编写简单的音频应用程序, 包括 PCM 播放音频、 PCM 录音等, 笔者也是参考了这份文档来编写本章教程,对应初学者,建议大家看一看。第二个链接地址是 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 帧、一帧包含两个样本(左、右两个声道):
音频设备底层驱动程序使用 DMA 来搬运数据, 这个 buffer 中有 4 个 period,每当 DMA 搬运完一个period 的数据就会触发一次中断,因此搬运整个 buffer 中的数据将产生 4 次中断。 ALSA 为什么这样做? 直接把整个 buffer 中的数据一次性搬运过去岂不是更快?情况并非如此,我们没有考虑到一个很重要的问题,那就是延迟;如果数据缓存区 buffer 很大,一次传输整个 buffer 中的数据可能会导致不可接受的延迟,因为一次搬运的数据量越大,所花费的时间就越长,那么必然会导致数据从传输开始到发出声音(以播放为例)
这个过程所经历的时间就会越长,这就是延迟。 为了解决这个问题, ALSA 把缓存区拆分成多个周期,以周期为传输单元进行传输数据。所以, 周期不宜设置过大,周期过大会导致延迟过高;但周期也不能太小,周期太小会导致频繁触发中断,这样会使得 CPU 被频繁中断而无法执行其它的任务, 使得效率降低! 所以,周期大小要合适,在延迟可接受的情况下,尽量设置大一些,不过这个需要根据实际应用场合而定,有些应用场合,可能要求低延迟、实时性高,但有些应用场合没有这种需求。
数据之间的传输
这里再介绍一下数据之间传输的问题,这个问题很重要,大家一定要理解,这样会更好的帮助我们理解代码、理解代码的逻辑。
⚫ PCM 播放情况下
在播放情况下, buffer 中存放了需要播放的 PCM 音频数据,由应用程序向 buffer 中写入音频数据, buffer中的音频数据由 DMA 传输给音频设备进行播放,所以应用程序向 buffer 写入数据、音频设备从 buffer 读取数据,这就是 buffer 中数据的传输情况。
下图中标识有 read pointer 和 write pointer 指针, write pointer 指向当前应用程序写 buffer 的位置、read pointer 指向当前音频设备读 buffer 的位置。 在数据传输之前(播放之前) , buffer 缓冲区是没有数据的,此时 write/read 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 数据满时,应用程序将不能再写入数据,否则就会覆盖之前的数据,必须要等待音频设备播放完一个周期, 音频设备每播放完一个周期,这个周期就变成空闲状态了,此时应用程序就可以写入一个周期的数据以填充这个空闲周期。
⚫ PCM 录音情况下
在录音情况下, buffer 中存放了音频设备采集到的音频数据(外界模拟声音通过 ADC 转为数字声音),由音频设备向 buffer 中写入音频数据(DMA 搬运),而应用程序从 buffer 中读取数据,所以音频设备向buffer 写入数据、应用程序从 buffer 读取数据,这就是录音情况下 buffer 中数据的传输情况。此时 write pointer 指向音频设备写 buffer 的位置、 read pointer 指向应用程序读 buffer的位置。在录音开始之前, buffer 缓冲区是没有数据的,此时 write/read pointer 均指向了 buffer 的起始位置,也就是第一个周期的起始位置,
音频设备向 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 中的数据总是连续地在音频设备和应用程序缓存区间传输,如下图所示:
上图展示了声卡在工作状态下, buffer 中数据的传输情况, 总是连续地在音频设备和应用程序缓存区间传输,但事情并不总是那么完美、也会出现有例外;譬如在录音例子中,如果应用程序读取数据不够快, 环形缓冲区 buffer 中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖; 这种数据的丢失被称为 over run。 在播放例子中,如果应用程序写入数据到环形缓冲区 buffer 中的速度不够快,缓存区将会“饿死”(缓冲区中无数据可播放); 这样的错误被称为 under run(欠载) 。在 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 个参数,如下所示:
⚫ 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,表示以非阻塞方式打开设备。
设备打开成功, 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 channelsSND_PCM_ACCESS_MMAP_NONINTERLEAVED, //mmap access with simple non interleaved channelsSND_PCM_ACCESS_MMAP_COMPLEX, //mmap access with complex placementSND_PCM_ACCESS_RW_INTERLEAVED, //snd_pcm_readi/snd_pcm_writei accessSND_PCM_ACCESS_RW_NONINTERLEAVED, //snd_pcm_readn/snd_pcm_writen accessSND_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()
这里说的周期,也就是前面小节中向大家介绍的周期,一个周期的大小使用帧来衡量,譬如一个周期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 播放
#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");elsefprintf(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");elsefprintf(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()函数中,首先调用 alsalib 库函数 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>!
示例代码 PCM 录音
#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()函数中,首先调用 alsalib 库函数 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()调用被唤醒、读取这一个周期然后返回。
示例代码 异步方式播放
上面的示例代码都是采用了同步方式进行读写,这样会使得应用程序无法做一些其它的事情, 本小节我们来学习如何使用异步方式读写。其实使用异步方式读写非常简单,只需要注册异步处理函数即可。
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);
关于 snd_async_add_pcm_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 信号或其它实时信号)。
这是内容在前面小节给大家详细介绍过,这里不再啰嗦! snd_async_add_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_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);
本小节主要给大家介绍这两个函数,因为后面的示例代码中会使用到。
#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静态函数************************************/
static void snd_playback_async_callback(snd_async_handler_t *handler)
{snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄snd_pcm_sframes_t avail;int ret;avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充while (avail >= period_size) { //我们一次写入一个周期memset(buf, 0x00, buf_bytes); //buf清零ret = read(fd, buf, buf_bytes);if (0 >= ret)goto out;ret = snd_pcm_writei(handle, buf, period_size);if (0 > ret) {fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));goto out;}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 out;}}avail = snd_pcm_avail_update(handle); //再次获取、更新avail}return;
out:snd_pcm_close(handle); //关闭pcm设备free(buf);close(fd); //关闭打开的音频文件exit(EXIT_FAILURE); //退出程序
}static int snd_pcm_init(void)
{snd_pcm_hw_params_t *hwparams = NULL;snd_async_handler_t *async_handler = 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; //变量赋值,一个周期的字节大小/* 注册异步处理函数 */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));goto err1;}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");elsefprintf(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");elsefprintf(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[])
{snd_pcm_sframes_t avail;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;}/* 播放:先将环形缓冲区填满数据 */avail = snd_pcm_avail_update(pcm); //获取环形缓冲区中有多少帧数据需要填充while (avail >= period_size) { //我们一次写入一个周期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;}}avail = snd_pcm_avail_update(pcm); //再次获取、更新avail}for ( ; ; ) {/* 主程序可以做一些其它的事,当环形缓冲区有空闲周期需要写入数据时* 音频设备驱动程序会向应用程序发送SIGIO信号* 接着应用程序跳转到snd_playback_async_callback()函数执行 *///do_something();sleep(1);}err3:free(buf); //释放内存
err2:snd_pcm_close(pcm); //关闭pcm设备
err1:close(fd); //关闭打开的音频文件exit(EXIT_FAILURE);
}
在 snd_pcm_init() 函 数 中 , 我 们 调 用了 snd_async_add_pcm_handler() 函 数 注 册 了 异 步 回调 函 数snd_playback_async_callback(),当可写入数据时,跳转到 snd_playback_async_callback()函数去执行。在异步回调函数中, 我们首先调用 snd_pcm_avail_update()获取当前可写入多少帧数据,然后在 while()循环中调用 read()读取音频文件的数据、接着调用 snd_pcm_writei()向环形缓冲区写入数据,每次循环写入一个周期,直到把缓冲区写满,然后退出回调函数。回到 main()函数中,在进入 for()死循环之前,我们先将环形缓冲区填满,执行的代码与回调函数中的代码相同,这里就不再说明了!
示例代码 异步方式录音
#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 unsigned int buf_bytes; //应用层缓冲区的大小(字节为单位)
static void *buf = NULL; //指向应用层缓冲区的指针
static int fd = -1; //输出文件的文件描述符
static snd_pcm_uframes_t period_size = 1024; //周期大小(单位: 帧)
static unsigned int periods = 16; //周期数(驱动层环形缓冲区的大小)
static unsigned int rate = 44100; //采样率/************************************static静态函数************************************/
static void snd_capture_async_callback(snd_async_handler_t *handler)
{snd_pcm_t *handle = snd_async_handler_get_pcm(handler);snd_pcm_sframes_t avail;int ret;avail = snd_pcm_avail_update(handle); //检查有多少帧数据可读while (avail >= period_size) { //每次读取一个周期//memset(buf, 0x00, buf_bytes); //buf清零ret = snd_pcm_readi(handle, buf, period_size);//读取PCM数据 一个周期if (0 > ret) {fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));goto out;}// snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数ret = write(fd, buf, ret * 4); //将读取到的数据写入文件中if (0 >= ret)goto out;avail = snd_pcm_avail_update(handle); //再次读取、更新avail}return;
out:snd_pcm_close(handle); //关闭pcm设备free(buf);close(fd); //关闭打开的音频文件exit(EXIT_FAILURE); //退出程序
}static int snd_pcm_init(void)
{snd_pcm_hw_params_t *hwparams = NULL;snd_async_handler_t *async_handler = 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;}/* 注册异步处理函数 */ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_capture_async_callback, NULL);if (0 > ret) {fprintf(stderr, "snd_async_add_pcm_handler 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[])
{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;}/* 录音 */ret = snd_pcm_start(pcm); //开始录音if (0 > ret) {fprintf(stderr, "snd_pcm_start error: %s\n", snd_strerror(ret));goto err3;}for ( ; ; ) {/* 主程序可以做一些其它的事,当环形缓冲区有数据可读时* 音频设备驱动程序会向应用程序发送SIGIO信号* 接着应用程序跳转到snd_capture_async_callback()函数执行、读取数据 *///do_something();sleep(1);}err3:close(fd); //关闭文件
err2:free(buf); //释放内存
err1:snd_pcm_close(pcm); //关闭pcm设备exit(EXIT_FAILURE);
}
使用 poll()函数
上小节我们使用了异步 I/O 方式读写 PCM 设备,本小节我们来学习如何使用 poll I/O 多路复用来实现读写数据。
使用 poll I/O 多路复用实现读写
I/O 多路复用是一种高级 I/O,可通过 select()或 poll()函数来实现 I/O 多路复用,本小节我们使用 poll()函数来实现 I/O 多路复用,接下来将向大家介绍!获取计数: snd_pcm_poll_descriptors_count该函数用于获取 PCM 句柄的轮询描述符计数,其函数原型如下所示:
int snd_pcm_poll_descriptors_count(snd_pcm_t *pcm);
调用该函数返回 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
接下来调用 snd_pcm_poll_descriptors()函数对 struct pollfd 对象进行填充(初始化),其函数原型如下所示:
int snd_pcm_poll_descriptors(snd_pcm_t *pcm,struct pollfd *pfds,unsigned int space);
参数 space 表示 pfds 数组中的元素个数。
/* 填充 pfds */
ret = snd_pcm_poll_descriptors(pcm, pfds, count);
if (0 > ret)return -1;
poll+snd_pcm_poll_descriptors_revents
一切准备完成之后,就可以调用 poll()函数来监视 PCM 设备是否有数据可读或可写, 当有数据可读或可写时, poll()函数返回, 此时我们可以调用snd_pcm_poll_descriptors_revents()函数获取文件描述符中返回的事件类型,并与 poll 的 events 标志进行比较,以确定是否可读或可写, snd_pcm_poll_descriptors_revents()
函数原型如下所示:
int 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);//调用 pollif (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 设备}
}
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <poll.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 struct pollfd *pfds = NULL;
static int count;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");elsefprintf(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");elsefprintf(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;
}static int snd_pcm_poll_init(void)
{int ret;/* 获取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;}/* 填充pfds */ret = snd_pcm_poll_descriptors(pcm, pfds, count);if (0 > ret)return -1;return 0;
}/************************************main主函数************************************/
int main(int argc, char *argv[])
{unsigned short revents;snd_pcm_sframes_t avail;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;}/* I/O多路复用poll初始化 */if (snd_pcm_poll_init())goto err3;for (;;) {ret = poll(pfds, count, -1);//调用pollif (0 > ret) {perror("poll error");goto err3;}ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);if (0 > ret)goto err3;if (revents & POLLERR)goto err3;if (revents & POLLOUT) { //可写数据avail = snd_pcm_avail_update(pcm);//获取环形缓冲区中有多少帧数据需要填充while (avail >= period_size) { //我们一次写入一个周期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) {if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {perror("lseek error");goto err3;}}avail = snd_pcm_avail_update(pcm); //再次获取、更新avail}}}err3:free(buf); //释放内存
err2:snd_pcm_close(pcm); //关闭pcm设备
err1:close(fd); //关闭打开的音频文件exit(EXIT_FAILURE);
}
PCM音频采集示例代码--使用I/O多路复用(poll)读数据#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <poll.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 struct pollfd *pfds = NULL;
static int count;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;
}static int snd_pcm_poll_init(void)
{int ret;/* 获取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;}/* 填充pfds */ret = snd_pcm_poll_descriptors(pcm, pfds, count);if (0 > ret)return -1;return 0;
}/************************************main主函数************************************/
int main(int argc, char *argv[])
{unsigned char *buf = NULL;unsigned int buf_bytes;unsigned short revents;snd_pcm_sframes_t avail;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;}/* I/O多路复用poll初始化 */if (snd_pcm_poll_init())goto err3;/* 开始录音 */ret = snd_pcm_start(pcm);if (0 > ret) {fprintf(stderr, "snd_pcm_start error: %s\n", snd_strerror(ret));goto err3;}for (;;) {ret = poll(pfds, count, -1);//调用pollif (0 > ret) {perror("poll error");goto err3;}ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);if (0 > ret)goto err3;if (revents & POLLERR)goto err3;if (revents & POLLIN) { //可读数据avail = snd_pcm_avail_update(pcm); //检查有多少帧数据可读while (avail >= period_size) { //每次读取一个周期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;}ret = write(fd, buf, ret * 4); //将读取到的数据写入文件中if (0 >= ret)goto err3;avail = snd_pcm_avail_update(pcm); //再次读取、更新avail}}}err3:close(fd); //关闭文件
err2:free(buf); //释放内存
err1:snd_pcm_close(pcm); //关闭pcm设备exit(EXIT_FAILURE);
}
PCM 设备的状态
本小节向大家介绍 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 设备包含的所有状态,如下所示:
enum snd_pcm_state_t {SND_PCM_STATE_OPEN = 0,SND_PCM_STATE_SETUP,SND_PCM_STATE_PREPARED,SND_PCM_STATE_RUNNING,SND_PCM_STATE_XRUN,SND_PCM_STATE_DRAINING,SND_PCM_STATE_PAUSED,SND_PCM_STATE_SUSPENDED,SND_PCM_STATE_DISCONNECTED,SND_PCM_STATE_LAST = SND_PCM_STATE_DISCONNECTED,SND_PCM_STATE_PRIVATE1 = 1024
}
SND_PCM_STATE_OPEN
该状态表示 PCM 设备处于打开状态,譬如当调用 snd_pcm_open()后, PCM 设备就处于该状态。
SND_PCM_STATE_SETUP
alsa-lib 文档中的解释为“Setup installed”! 该状态表示设备已经初始化完成了,参数已经配置好了。
SND_PCM_STATE_PREPARED
该状态表示设备已经准备好了,可以开始了“Ready to start”!譬如可以开始播放了、 可以开始录音了。前 面 提 到 了 这 个 状 态 , 当 应 用 程 序 调 用 snd_pcm_hw_params() 函 数 之 后 , 设 备 就 处 于SND_PCM_STATE_PREPARED 状态了。 应用程序中,可以调用 snd_pcm_prepare()函数使设备处于SND_PCM_STATE_PREPARED 状态,该函数原型如下所示:
int snd_pcm_prepare(snd_pcm_t *pcm);
该行数调用成功返回 0,失败将返回一个负数错误码。
函数调用成功, PCM 设备将处于 SND_PCM_STATE_PREPARED 状态。 事实上,应用程序调用时snd_pcm_hw_params()时,函数内部会自动调用 snd_pcm_prepare(),所以为什么调用 snd_pcm_hw_params()之后设备就已经处于 SND_PCM_STATE_PREPARED 状态了;调用 snd_pcm_hw_params()函数,其实应该发生了两种状态的转变为:首先由 SND_PCM_STATE_OPEN 变为 SND_PCM_STATE_SETUP 状态、再由SND_PCM_STATE_SETUP 变为 SND_PCM_STATE_PREPARED 状态。
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_writei 写入数据后,会自动开启 PCM 设备进行播放,这里要注意,一定是在数据写入到环形缓冲区之后、才开启 PCM设备播放音频,因为一旦开启之后,环形缓冲区中必须要有至少一个周期的数据可供音频设备播放,否则将会发生欠载(underrun) 、函数调用以错误形式返回;在录音模式下, 调用 snd_pcm_readi()函数后,自动开启 PCM 进行音频采集。
当设备处于运行状态时,应用程序可调用 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,而是处理完挂起的帧之后再停止 PCM; 对于播放, 会等待所有待播放的帧播放完毕(应该就是环形缓冲区中的待播放数据) ,然后停止 PCM; 对于录音, 停止 PCM 之前会检索残留帧。
当调用 snd_pcm_drop()或 snd_pcm_drain()停止 PCM 设备后,设备将回到 SND_PCM_STATE_SETUP 状态。
SND_PCM_STATE_XRUN
当发生 XRUN 时,设备会处于 SND_PCM_STATE_XRUN 状态, XRUN 前面给大家解释过了,这里不再重述! 当处于 SND_PCM_STATE_XRUN 状态时,应用程序可以调用 snd_pcm_prepare()使设备恢复,使其回到 SND_PCM_STATE_PREPARED 状态。
SND_PCM_STATE_DRAINING
alsa-lib 文档中的解释为“Draining: running (playback) or stopped (capture)”。
SND_PCM_STATE_PAUSED
pause 就是暂停的意思,所以该状态表示设备处于暂停状态。 譬如当设备正在运行时(也就是处于SND_PCM_STATE_RUNNING 状态),应用程序调用 snd_pcm_pause()函数可让设备暂停,其函数原型如下所示:
int snd_pcm_pause(snd_pcm_t *pcm, int enable);
函 数 snd_pcm_pause() 既 可 以 使 的 设 备 暂 停 、 同 样 也 可 使 其 恢 复 ( 从 暂 停 恢 复 运 行 , 即SND_PCM_STATE_RUNNING--->SND_PCM_STATE_RUNNING),通过参数 enable 控制;当 enable 等于1,表示使设备暂停; enable 等于 0 表示使设备恢复运行。snd_pcm_pause()函数调用成功返回 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()函数从挂起中恢复,并确保不会丢失样本数据(精细恢复)。 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
该状态表示硬件已经断开连接。
状态之间的转换
通过上面的介绍,我们已经知道了 PCM 设备的几种不同的状态、以及它们的一个转换关系,为了能够加深大家的印象,笔者对其进行了整理, 主要整理了 SND_PCM_STATE_OPEN、SND_PCM_STATE_SETUP、SND_PCM_STATE_PREPARED 、 SND_PCM_STATE_RUNNING 、 SND_PCM_STATE_XRUN 以 及
SND_PCM_STATE_PAUSED 这 6 种状态之间的转换关系, 如下图所示:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <signal.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 struct termios old_cfg; //用于保存终端当前的配置参数/************************************static静态函数************************************/
static void snd_playback_async_callback(snd_async_handler_t *handler)
{snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄snd_pcm_sframes_t avail;int ret;avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充while (avail >= period_size) { //我们一次写入一个周期memset(buf, 0x00, buf_bytes); //buf清零ret = read(fd, buf, buf_bytes);if (0 >= ret)goto out;ret = snd_pcm_writei(handle, buf, period_size);if (0 > ret) {fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));goto out;}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 out;}}avail = snd_pcm_avail_update(handle); //再次获取、更新avail}return;
out:snd_pcm_drain(pcm); //停止PCMsnd_pcm_close(handle); //关闭pcm设备tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态free(buf);close(fd); //关闭打开的音频文件exit(EXIT_FAILURE); //退出程序
}static int snd_pcm_init(void)
{snd_pcm_hw_params_t *hwparams = NULL;snd_async_handler_t *async_handler = 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; //变量赋值,一个周期的字节大小/* 注册异步处理函数 */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));goto err1;}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");elsefprintf(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");elsefprintf(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[])
{snd_pcm_sframes_t avail;struct termios new_cfg;sigset_t sset;int ret;if (2 != argc) {fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);exit(EXIT_FAILURE);}/* 屏蔽SIGIO信号 */sigemptyset(&sset);sigaddset(&sset, SIGIO);sigprocmask(SIG_BLOCK, &sset, NULL);/* 打开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;}/* 终端配置 */tcgetattr(STDIN_FILENO, &old_cfg); //获取终端<标准输入-标准输出构成了一套终端>memcpy(&new_cfg, &old_cfg, sizeof(struct termios));//备份new_cfg.c_lflag &= ~ICANON; //将终端设置为非规范模式new_cfg.c_lflag &= ~ECHO; //禁用回显tcsetattr(STDIN_FILENO, TCSANOW, &new_cfg);//使配置生效/* 播放:先将环形缓冲区填满数据 */avail = snd_pcm_avail_update(pcm); //获取环形缓冲区中有多少帧数据需要填充while (avail >= period_size) { //我们一次写入一个周期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;}}avail = snd_pcm_avail_update(pcm); //再次获取、更新avail}sigprocmask(SIG_UNBLOCK, &sset, NULL); //取消SIGIO信号屏蔽char ch;for ( ; ; ) {ch = getchar(); //获取用户输入的控制字符switch (ch) {case 'q': //Q键退出程序sigprocmask(SIG_BLOCK, &sset, NULL);//屏蔽SIGIO信号goto err3;case ' ': //空格暂停/恢复switch (snd_pcm_state(pcm)) {case SND_PCM_STATE_PAUSED: //如果是暂停状态则恢复运行ret = snd_pcm_pause(pcm, 0);if (0 > ret)fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));break;case SND_PCM_STATE_RUNNING: //如果是运行状态则暂停ret = snd_pcm_pause(pcm, 1);if (0 > ret)fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));break;}break;}}err3:snd_pcm_drop(pcm); //停止PCMtcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态free(buf); //释放内存
err2:snd_pcm_close(pcm); //关闭pcm设备
err1:close(fd); //关闭打开的音频文件exit(EXIT_FAILURE);
}
snd_pcm_readi/snd_pcm_writei 错误处理
当 snd_pcm_readi/snd_pcm_writei 调用出错时,会返回一个小于 0(负值)的错误码, 可调用 snd_strerror()函数获取对应的错误描述信息。 前面的示例代码中我们并没有对 snd_pcm_readi/snd_pcm_writei 的错误返回做过多、细节的处理,而是简单地在出错之后退出。事实上, 当调用 snd_pcm_readi/snd_pcm_writei 出错时,可根据不同的情况作进一步的处理,在 alsa-lib文档中有介绍到, snd_pcm_readi/snd_pcm_writei 函数的不同错误返回值,表示不同的含义,如下所示:
snd_pcm_readi()函数与它相同。
当返回值等于-EBADFD,表示 PCM 设备的状态不对,因为执行 snd_pcm_readi/snd_pcm_writei 读取/写入数据需要 PCM 设备处于 SND_PCM_STATE_PREPARED 或 SND_PCM_STATE_RUNNING 状态,前面已经详细地给大家介绍了 PCM 设备的状态间转换问题。当返回值等于-EPIPE,表示发生了 XRUN,此时可以怎么做呢? 这个可以根据自己的实际需要进行处理,譬如调用 snd_pcm_drop()停止 PCM 设备,或者调用 snd_pcm_prepare()使设备恢复进入准备状态。当返回值等于-ESTRPIPE,表示硬件发生了挂起,此时 PCM 设备处于 SND_PCM_STATE_SUSPENDED状态 ,譬如你可以调用 snd_pcm_resume()函数从挂起中精确恢复 ,如果硬件不支持 ,还可调用snd_pcm_prepare()函数使设备进入准备状态, 或者执行其它的处理,根据应用需求的进行相应的处理。以上给大家介绍了调用 snd_pcm_readi/snd_pcm_writei 函数出错时的一些情况以及可以采取的一些措施!
混音器设置
前面给大家介绍了 alsa-utils 提供的两个声卡配置工具: alsamixer 和 amixer。这两个工具同样是基于 alsalib 库函数编写的,本小节我们来学习如何在自己的应用程序中通过调用 alsa-lib 库函数对声卡混音器进行配置,譬如音量调节。混音器相关的接口在 alsa-lib 的 Mixer Interface 模块中有介绍,点击下图中“Mixer Interface”可查看混音器相关接口的介绍,如下所示:
大家可以简单地浏览下该模块下提供了那些函数,点击函数名可以查看该函数的简单介绍信息。
打开混音器: snd_mixer_open
在使用混音器之后,需要打开混音器,调用 snd_mixer_open()函数打开一个空的混音器,其函数原型如下所示:
int snd_mixer_open(snd_mixer_t **mixerp, int mode);
alsa-lib 使用 snd_mixer_t 数据结构描述混音器,调用 snd_mixer_open()函数会实例化一个 snd_mixer_t 对象,并将对象的指针(也就是混音器的句柄)通过 mixerp 返回出来。参数 mode 指定了打开模式,通常设置为 0 使用默认模式即可!
函数调用成功返回 0;失败返回一个小于 0 的错误码。
使用示例:
snd_mixer_t *mixer = NULL;
int ret;
ret = snd_mixer_open(&mixer, 0);
if (0 > ret)fprintf(stderr, "snd_mixer_open error: %s\n", snd_strerror(ret));
Attach 关联设备: snd_mixer_attach
调用 snd_mixer_open()函数打开并实例化了一个空的混音器, 接下来我们要去关联声卡控制设备,调用snd_mixer_attach()函数进行关联,其函数原型如下所示:
int snd_mixer_attach(snd_mixer_t *mixer, const char *name);
参数 mixer 对应的是混音器的句柄,参数 name 指定了声卡控制设备的名字,同样这里使用的也是逻辑设备名,而非设备节点的名字,命名方式为"hw:i", i 表示声卡的卡号,通常一个声卡对应一个控制设备;譬如"hw:0"表示声卡 0 的控制设备,这其实就对应/dev/snd/controlC0 设备。 与 snd_pcm_open()函数中 PCM 设备的命名一样, snd_mixer_attach()函数中声卡控制设备的命名也有其它方式,这里暂时先不管这个问题。调用 snd_mixer_open()函数会将参数 name 所指定的控制设备与混音器 mixer 进行关联。
函数调用成功返回 0;失败返回一个小于 0 的错误码。
使用示例:
ret = snd_mixer_attach(mixer, "hw:0");
if (0 > ret)fprintf(stderr, "snd_mixer_attach error: %s\n", snd_strerror(ret));
注册: snd_mixer_selem_register
调用 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 的错误码。
使用示例:
ret = snd_mixer_selem_register(mixer, NULL, NULL);
if (0 > ret)fprintf(stderr, "snd_mixer_selem_register error: %s\n", snd_strerror(ret));
加载: snd_mixer_load
最后需要加载混音器,调用 snd_mixer_load()函数完成加载,函数原型如下所示:
int snd_mixer_load(snd_mixer_t * mixer);
函数调用成功返回 0;失败返回小于 0 的错误码。
使用示例:
ret = snd_mixer_load(mixer);
if (0 > ret)fprintf(stderr, "snd_mixer_load error: %s\n", snd_strerror(ret));
查找元素
经过上面一系列步骤之后,接下来就可以使用混音器了, alsa-lib 中把混音器的配置项称为元素(element),譬如耳机音量调节 Headphone 是一个元素、 'Headphone Playback ZC'是一个元素、 'Right Output Mixer PCM'也是一个元素。
snd_mixer_first_elem 和 snd_mixer_last_elem
alsa-lib 使用数据结构 snd_mixer_elem_t 来描述一个元素,所以一个 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_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_first_elem 和 snd_mixer_elem_next() 或 者 snd_mixer_last_elem() 和
snd_mixer_elem_prev()就可以遍历整个混音器中的所有元素,如下所示:
snd_mixer_elem_t *elem = NULL;
elem = snd_mixer_first_elem(mixer);//找到第一个元素
while (elem) {............snd_mixer_elem_next(elem); //找到下一个元素
}
snd_mixer_selem_get_name
调用 snd_mixer_selem_get_name()函数可获取指定元素的名字,如下所示:
const char *snd_mixer_selem_get_name(snd_mixer_elem_t *elem);
获取元素的名字之后,进行对比,以确定是否是我们要找的元素:
const char *name = snd_mixer_selem_get_name(elem);
if (!strcmp(name, "Headphone")) {//该配置项是"Headphone"
}
else {//该配置项不是"Headphone"
}
获取/更改元素的配置值
前面给大家提到了混音器的配置值有两种类型,第一种它的配置值是在一个范围内的数值,譬如音量大小的调节;另一种则是 bool 类型,用于控制开启或关闭,譬如 0 表示关闭配置、 1 表示使能配置。
snd_mixer_selem_has_playback_volume/snd_mixer_selem_has_capture_volume
我们可以调用 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
调用 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
通过 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 是一个枚举类型,如下所示:
enum snd_mixer_selem_channel_id_t {SND_MIXER_SCHN_UNKNOWN = -1,SND_MIXER_SCHN_FRONT_LEFT = 0, //左前SND_MIXER_SCHN_FRONT_RIGHT, //右前SND_MIXER_SCHN_REAR_LEFT, //左后SND_MIXER_SCHN_REAR_RIGHT, //右后SND_MIXER_SCHN_FRONT_CENTER, //前中SND_MIXER_SCHN_WOOFER, //低音喇叭SND_MIXER_SCHN_SIDE_LEFT, //左侧SND_MIXER_SCHN_SIDE_RIGHT, //右侧SND_MIXER_SCHN_REAR_CENTER, //后中SND_MIXER_SCHN_LAST = 31,SND_MIXER_SCHN_MONO = SND_MIXER_SCHN_FRONT_LEFT //单声道
};
如 果 元 素 是 双 声 道 元 素 , 通 常 只 包 含 左 前 ( 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
调用 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 的错误码。
譬如,获取左前声道的音量(播放):
long value;
snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &value);
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);
调用 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可一次性设置指定元素所有声道的音量,函数原型如下所示:
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);
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <signal.h>
#include <alsa/asoundlib.h>/************************************宏定义************************************/
#define PCM_PLAYBACK_DEV "hw:0,0"
#define MIXER_DEV "hw: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 snd_mixer_t *mixer = NULL; //混音器句柄
static snd_mixer_elem_t *playback_vol_elem = NULL; //播放<音量控制>元素
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 struct termios old_cfg; //用于保存终端当前的配置参数/************************************static静态函数************************************/
static void snd_playback_async_callback(snd_async_handler_t *handler)
{snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄snd_pcm_sframes_t avail;int ret;avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充while (avail >= period_size) { //我们一次写入一个周期memset(buf, 0x00, buf_bytes); //buf清零ret = read(fd, buf, buf_bytes);if (0 >= ret)goto out;ret = snd_pcm_writei(handle, buf, period_size);if (0 > ret) {fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));goto out;}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 out;}}avail = snd_pcm_avail_update(handle); //再次获取、更新avail}return;
out:snd_pcm_drain(pcm); //停止PCMsnd_mixer_close(mixer); //关闭混音器snd_pcm_close(handle); //关闭pcm设备tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态free(buf);close(fd); //关闭打开的音频文件exit(EXIT_FAILURE); //退出程序
}static int snd_pcm_init(void)
{snd_pcm_hw_params_t *hwparams = NULL;snd_async_handler_t *async_handler = 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; //变量赋值,一个周期的字节大小/* 注册异步处理函数 */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));goto err1;}return 0;err2:snd_pcm_hw_params_free(hwparams); //释放内存
err1:snd_pcm_close(pcm); //关闭pcm设备return -1;
}static int snd_mixer_init(void)
{snd_mixer_elem_t *elem = NULL;const char *elem_name;long minvol, maxvol;int ret;/* 打开混音器 */ret = snd_mixer_open(&mixer, 0);if (0 > ret) {fprintf(stderr, "snd_mixer_open error: %s\n", snd_strerror(ret));return -1;}/* 关联一个声卡控制设备 */ret = snd_mixer_attach(mixer, MIXER_DEV);if (0 > ret) {fprintf(stderr, "snd_mixer_attach error: %s\n", snd_strerror(ret));goto err;}/* 注册混音器 */ret = snd_mixer_selem_register(mixer, NULL, NULL);if (0 > ret) {fprintf(stderr, "snd_mixer_selem_register error: %s\n", snd_strerror(ret));goto err;}/* 加载混音器 */ret = snd_mixer_load(mixer);if (0 > ret) {fprintf(stderr, "snd_mixer_load error: %s\n", snd_strerror(ret));goto err;}/* 遍历混音器中的元素 */elem = snd_mixer_first_elem(mixer);//找到第一个元素while (elem) {elem_name = snd_mixer_selem_get_name(elem);//获取元素的名称/* 针对开发板出厂系统:WM8960声卡设备 */if(!strcmp("Speaker", elem_name) || //耳机音量<对喇叭外音输出有效>!strcmp("Headphone", elem_name) ||//喇叭音量<对耳机输出有效>!strcmp("Playback", elem_name)) {//播放音量<总的音量控制,对喇叭和耳机输出都有效>if (snd_mixer_selem_has_playback_volume(elem)) {//是否是音量控制元素snd_mixer_selem_get_playback_volume_range(elem, &minvol, &maxvol);//获取音量可设置范围snd_mixer_selem_set_playback_volume_all(elem, (maxvol-minvol)*0.9 + minvol);//全部设置为90%if (!strcmp("Playback", elem_name))playback_vol_elem = elem;}}elem = snd_mixer_elem_next(elem);}return 0;err:snd_mixer_close(mixer);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");elsefprintf(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");elsefprintf(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;
}static void show_help(void)
{printf("<<<<<<<基于alsa-lib音乐播放器>>>>>>>>>\n\n""操作菜单:\n"" q 退出程序\n"" space<空格> 暂停播放/恢复播放\n"" w 音量增加++\n"" s 音量减小--\n\n");
}/************************************main主函数************************************/
int main(int argc, char *argv[])
{snd_pcm_sframes_t avail;struct termios new_cfg;sigset_t sset;int ret;if (2 != argc) {fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);exit(EXIT_FAILURE);}/* 屏蔽SIGIO信号 */sigemptyset(&sset);sigaddset(&sset, SIGIO);sigprocmask(SIG_BLOCK, &sset, NULL);/* 打开WAV音频文件 */if (open_wav_file(argv[1]))exit(EXIT_FAILURE);/* 初始化PCM Playback设备 */if (snd_pcm_init())goto err1;/* 初始化混音器 */if (snd_mixer_init())goto err2;/* 申请读缓冲区 */buf = malloc(buf_bytes);if (NULL == buf) {perror("malloc error");goto err3;}/* 终端配置 */tcgetattr(STDIN_FILENO, &old_cfg); //获取终端<标准输入-标准输出构成了一套终端>memcpy(&new_cfg, &old_cfg, sizeof(struct termios));//备份new_cfg.c_lflag &= ~ICANON; //将终端设置为非规范模式new_cfg.c_lflag &= ~ECHO; //禁用回显tcsetattr(STDIN_FILENO, TCSANOW, &new_cfg);//使配置生效/* 播放:先将环形缓冲区填满数据 */avail = snd_pcm_avail_update(pcm); //获取环形缓冲区中有多少帧数据需要填充while (avail >= period_size) { //我们一次写入一个周期memset(buf, 0x00, buf_bytes); //buf清零ret = read(fd, buf, buf_bytes);if (0 >= ret)goto err4;ret = snd_pcm_writei(pcm, buf, period_size);//向环形缓冲区中写入数据if (0 > ret) {fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));goto err4;}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 err4;}}avail = snd_pcm_avail_update(pcm); //再次获取、更新avail}sigprocmask(SIG_UNBLOCK, &sset, NULL); //取消SIGIO信号屏蔽/* 显示帮助信息 */show_help();/* 等待获取用户输入 */char ch;long vol;for ( ; ; ) {ch = getchar(); //获取用户输入的控制字符switch (ch) {case 'q': //Q键退出程序sigprocmask(SIG_BLOCK, &sset, NULL);//屏蔽SIGIO信号goto err4;case ' ': //空格暂停/恢复switch (snd_pcm_state(pcm)) {case SND_PCM_STATE_PAUSED: //如果是暂停状态则恢复运行ret = snd_pcm_pause(pcm, 0);if (0 > ret)fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));break;case SND_PCM_STATE_RUNNING: //如果是运行状态则暂停ret = snd_pcm_pause(pcm, 1);if (0 > ret)fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));break;}break;case 'w': //音量增加if (playback_vol_elem) {//获取音量snd_mixer_selem_get_playback_volume(playback_vol_elem,SND_MIXER_SCHN_FRONT_LEFT, &vol);vol++;//设置音量snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);}break;case 's': //音量降低if (playback_vol_elem) {//获取音量snd_mixer_selem_get_playback_volume(playback_vol_elem,SND_MIXER_SCHN_FRONT_LEFT, &vol);vol--;//设置音量snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);}break;}}err4:snd_pcm_drop(pcm); //停止PCMtcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态free(buf); //释放内存
err3:snd_mixer_close(mixer); //关闭混音器
err2:snd_pcm_close(pcm); //关闭pcm设备
err1:close(fd); //关闭打开的音频文件exit(EXIT_FAILURE);
}