Linux_应用篇(22) 音频应用编程

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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/34897.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【2024最新版】Windows11 23H2中文家庭版:免费下载!

Windows 11 23H2中文家庭版系统拥有稳定的性能、丰富的功能和卓越的安全性&#xff0c;很多用户都喜欢给自己的电脑安装上这个版本。但是&#xff0c;许多新手用户不清楚在哪里才能下载到Windows11家庭版&#xff1f;接下来小编给大家带来2024年最新的Windows 11 23H2中文家庭版…

精通pip:Python开发者的必备技能

目录 1. 安装 pip 2. 使用 pip 安装包 3. 卸载包 4. 更新包 5. 列出已安装的包 6. 搜索包 7. 使用 requirements.txt 文件安装多个包 8. 升级 pip 自身 9. 虚拟环境 10. 冻结依赖 11. 使用国内镜像源 12. 安装特定版本的包 13. 批量安装包 14. 显示帮助信息 15.…

Java字符串处理深度解析:String、StringBuffer与StringBuilder的奥秘

摘要&#xff1a; 本文将深入探讨Java语言中处理字符串的基础构件&#xff1a;String、StringBuffer和StringBuilder。我们将详细讲解它们的内部原理、适用场景、性能对比以及在现代开发实践中的使用策略。同时&#xff0c;结合当下编程行业的热点技术&#xff0c;如微服务架构…

【大数据技术原理与应用(概念、存储、处理、分析与应用)】第2章-大数据处理架构 Hadoop习题与知识回顾

文章目录 单选题多选题知识点回顾什么是Hadoop&#xff1f;Hadoop有哪些特性&#xff1f;Hadoop生态系统是怎么样的&#xff1f;(1) HDFS(2) HBase(3) MapReduce(4) Hive(5) Pig(6) Mahout(7) Zookeeper(8) Flume(9) Sqoop(10) Ambari 单选题 1、下列哪个不属于Hadoop的特性&am…

Oracle 23ai的Windows平台版本发布了

Oracle 23ai free的版本之前只有Linux平台的版本&#xff0c;刚刚增加了Windows平台的版本&#xff0c;这里尝一下鲜。 关于号主&#xff0c;姚远&#xff1a; Oracle ACE&#xff08;Oracle和MySQL数据库方向&#xff09;华为云最有价值专家《MySQL 8.0运维与优化》的作者拥有…

多路h265监控录放开发-(15)回放页面中的三个槽函数进行视频的录放(0.1版本项目完结篇)

xviewer.h 中的回放页面的三个槽函数&#xff1a; void SelectCamera(QModelIndex index);//选择摄像机129void SelectDate(QDate date); //选择日期129void PlayVideo(QModelIndex index); //选择时间播放视频129 SelectCamera槽函数解析&#xff1a; 点击相机列表日…

乐鑫ESP32-WROOM-32E模组设备低功耗控制方案,启明云端乐鑫代理商

在数字化浪潮的推动下&#xff0c;物联网&#xff08;IoT&#xff09;正迅速成为我们日常生活的一部分。而在这个领域中&#xff0c;ESP32-WROOM-32E模组以其卓越的性能和多功能性&#xff0c;成为了开发者和制造商的选择。 ESP32-WROOM-32E模组集成了ESP32-D0WD-V3芯片&#…

项目maven标志消失,pom文件显示为橙色/橘色标志

背景&#xff1a; 公司开发新的项目&#xff0c;我要拉一下item服务的工程进行开发&#xff0c;等我把代码拉下来发现我idea右侧边栏的maven没了&#xff0c;pox.xml文件也变成了这种橙色/橘色的标志。 分析&#xff1a; 这个是一个不正常的maven项目pom&#xff0c;可能是由于…

OpenVINO在iGPU上加载模型比CPU慢

官方解答地址&#xff1a; 为什么模型加载时间比 CPU 长&#xff1f; (intel.cn) 总结 快速步骤以改善 GPU 上的模型加载时间 说明 将输入模型的中间表示 &#xff08;IR&#xff09; 加载到 GPU 需要的时间比将相同模型加载到 CPU 还要长。 解决方法 在应用程序的工作目…

Springboot3+微服务实战12306高性能售票系统

yangzz 分享于 2023-08-14 查看次数&#xff1a; 170 次 所需&#xff1a; 10 积分 1-1 课前必读&#xff08;不读错过一个亿&#xff09; 视频&#xff1a; 1-2 课程导学 (14:21) 试看 视频&#xff1a; 1-3 为什么要选择最新版本SpringBoot3和JDK17&#xff1f; (07:0…

哪种领夹麦性价比高,哪款领夹麦克风好用,无线领夹麦克风推荐

​在这个多媒体时代&#xff0c;无线麦克风已经成为我们日常生活的一部分。无论是自媒体创作者、直播主播&#xff0c;还是日常拍摄记录&#xff0c;无线麦克风都扮演着重要角色。挑选无线麦克风时&#xff0c;收音效果和性价比是两大核心考量因素。因此&#xff0c;我特意为大…

html5+css简易实现图书网联系我们页面

html5css简易实现图书网联系我们页面 完整代码已资源绑定

毕业生离校系统

摘 要 随着信息技术的快速发展和普及&#xff0c;越来越多的高校开始利用信息化手段来提升管理和服务效率。毕业生离校是高校管理工作中的一个重要环节&#xff0c;涉及到毕业生的个人信息、学业成绩、离校手续等多个方面。传统的离校流程往往繁琐、耗时&#xff0c;且容易出现…

stm32学习笔记---TIM输出比较(代码部分)定时器定时中断/定时器外部时钟

目录 第一个代码&#xff1a;定时器定时中断 Timer.c 初始化函数 初始化定时器的步骤 定时器的库函数 TIM_DeInit TIM_TimeBaseInit TIM_TimeBaseStructInit TIM_Cmd TIM_ITConfig TIM_InternalClockConfig TIM_ITRxExternalClockConfig TIM_InputTriggerSource …

淘宝扭蛋机小程序开发,探索市场新的发展方向

如今&#xff0c;潮玩已经成为了年轻人娱乐消费的首选方式之一&#xff0c;发展态势也在不断上升&#xff0c;吸引了众多年轻人的关注。在小程序的发展下&#xff0c;也推动了扭蛋机市场的创新&#xff0c;淘宝扭蛋机小程序就是一个新的模式&#xff0c;为扭蛋机市场带来了新的…

时钟的抖动(Jitter)与偏移(Skew)

时钟的抖动&#xff08;Jitter&#xff09;与偏移&#xff08;Skew&#xff09;是数字系统时序分析中的两个重要概念&#xff0c;它们对系统的性能和稳定性有着显著的影响。以下是关于时钟抖动和偏移的详细解释&#xff1a; 时钟抖动&#xff08;Jitter&#xff09; 定义&…

VB求高于平均成绩的分数

有3个学生&#xff0c;每个学生4门课。 先求每个学生的平均成绩&#xff0c;然后展示高于平均成绩的分数。 Public Class Form1Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.ClickDim pj%, i%, n%, sum%, say$Dim cj(0 To 3) As Integeri 1…

Linux—LVM与磁盘配额

目录 一、LVM 1、LVM概念 2、LVM逻辑卷核心组件 3、LVM管理命令 二、LVM操作主要命令步骤 1、添加硬盘 2、新建分区&#xff0c;并修改分区类型 3、新建物理卷&#xff08;PV&#xff09; 4、新建卷组&#xff08;VG&#xff09; 5、新建逻辑卷&#xff08;LV&#xff0…

帮您理解PostgreSQL(WAL、XLOG、CheckPoint进程、LSN、PITR、SR)

文章目录 一、WAL、XLOG、LSN二、检查点进程与pg_control文件-负责脏页刷盘、数据库恢复三、基础备份与时间点恢复PITR四、原生复制功能与流复制&#xff08;SR Streaming Replication&#xff09; 一、WAL、XLOG、LSN 在计算机领域&#xff0c;WAL是Write Ahead Logging的缩写…

Typora配置自建的兰空图床

文章目录 Typora配置自建的兰空图床 - 前言先看效果1、搭建兰空图床 - docker2、配置兰空图床3、登录进入兰空图床后台4、Typora配置兰空图床安装兰空插件获取兰空图床的Token编辑PigGO的配置文件 使用 Typora配置自建的兰空图床 - 前言 Typora插入的图片默认存储在本地&#…