OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 6032|回复: 1

《I.MX6U嵌入式Linux C应用编程指南 V1.1》第二十九章 音频应用编程

[复制链接]

1118

主题

1129

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4672
金钱
4672
注册时间
2019-5-8
在线时间
1224 小时
发表于 2021-9-6 15:44:27 | 显示全部楼层 |阅读模式
1)实验平台:正点原子阿尔法Linux开发板
2)  章节摘自【正点原子】《I.MX6U嵌入式Linux C应用编程指南 V1.1》

3)购买链接:https://detail.tmall.com/item.htm?id=609033604451
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyz-i.mx6ull.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子阿尔法Linux交流群:1027879335







第二十九章 音频应用编程

ALPHA I.MX6U开发板支持音频,板上搭载了音频编解码芯片WM8960,支持播放以及录音功能!
本章我们来学习Linux下的音频应用编程,音频应用编程相比于前面几个章节所介绍的内容、其难度有所上升,但是笔者仅向大家介绍Linux音频应用编程中的基础知识,而更多细节、更加深入的内容需要大家自己去学习。
本章将会讨论如下主题内容。
Linux下ALSA框架概述;
alsa-lib库介绍;
alsa-lib库移植;
alsa-lib库的使用;
音频应用编程之播放;
音频应用编程之录音。




29.1ALSA概述
ALSA是Advanced Linux Sound Architecture(高级的Linux声音体系)的缩写,目前已经成为了linux下的主流音频体系架构,提供了音频和MIDI的支持,替代了原先旧版本中的OSS(开发声音系统);学习过Linux音频驱动开发的读者肯定知道这个;事实上,ALSA是Linux系统下一套标准的、先进的音频驱动框架,那么这套框架的设计本身是比较复杂的,采用分离、分层思想设计而成,具体的细节便不给大家介绍了!作为音频应用编程,我们不用去研究这个。
在应用层,ALSA为我们提供了一套标准的API,应用程序只需要调用这些API就可完成对底层音频硬件设备的控制,譬如播放、录音等,这一套API称为alsa-lib。如下图所示:
第二十九章 音频应用编程590.png
图 29.1.1 alsa音频示意图
29.2alsa-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/,进入到该链接地址后,如下所示:
第二十九章 音频应用编程1064.png
图 29.2.1 alsa-lib使用参考手册
alsa-lib库支持功能比较多,提供了丰富的API接口供应用程序开发人员调用,根据函数的功能、作用将这些API进行了分类,可以点击上图中Modules按钮查看其模块划分,如下所示:
第二十九章 音频应用编程1225.png
图 29.2.2 alsa-lib模块
一个分类就是一个模块(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设备(启动、暂停、恢复、写入/读取数据),该模块下还包含了一些子模块,如下所示:
第二十九章 音频应用编程2119.png
图 29.2.3 PCM Interface下的子模块
点击模块名称可以查看到该模块提供的API接口有哪些以及相应的函数说明,这里就不给大家演示了!
Error Interface
该模块提供了关于错误处理相关的接口,譬如函数调用发生错误时,可调用该模块下提供的函数打印错误描述信息。
Mixer Interface
提供了关于混音器相关的一系列操作接口,譬如音量、声道控制、增益等等。
29.3sound设备节点
在Linux内核设备驱动层、基于ALSA音频驱动框架注册的sound设备会在/dev/snd目录下生成相应的设备节点文件,譬如ALPHA I.MX6U开发板出厂系统/dev/snd目录下有如下文件:
第二十九章 音频应用编程2468.png
图 29.3.1 /dev/snd目录下的文件
Tips:注意,Mini I.MX6U开发板出厂系统/dev/snd目录下是没有这些文件的,因为Mini板不支持音频、没有板载音频编解码芯片,所以本章实验例程无法在Mini板上进行测试,请悉知!
从上图可以看到有如下设备文件:
controlC0:用于声卡控制的设备节点,譬如通道选择、混音器、麦克风的控制等,C0表示声卡0(card0);
pcmC0D0c:用于录音的PCM设备节点。其中C0表示card0,也就是声卡0;而D0表示device 0,也就是设备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目录下,有很多的文件,这些文件记录了系统中声卡相关的信息,如下所示:
第二十九章 音频应用编程3173.png
图 29.3.2 /proc/asound目录下的文件
cards:
通过"cat /proc/asound/cards"命令、查看cards文件的内容,可列出系统中可用的、注册的声卡,如下所示:
  1. cat /proc/asound/cards
复制代码


第二十九章 音频应用编程3340.png
图 29.3.3 查看系统中注册的所有声卡
我们的阿尔法板子上只有一个声卡(WM8960音频编解码器),所以它的编号为0,也就是card0。系统中注册的所有声卡都会在/proc/asound/目录下存在一个相应的目录,该目录的命名方式为cardX(X表示声卡的编号),譬如图 29.3.2中的card0;card0目录下记录了声卡0相关的信息,譬如声卡的名字以及声卡注册的PCM设备,如下所示:
第二十九章 音频应用编程3607.png
图 29.3.4 card0目录下的文件
devices:
列出系统中所有声卡注册的设备,包括control、pcm、timer、seq等等。如下所示:
  1. cat /proc/asound/devices
复制代码


第二十九章 音频应用编程3754.png
图 29.3.5 列出所有设备
pcm:
列出系统中的所有PCM设备,包括playback和capture:
  1. cat /proc/asound/pcm
复制代码


第二十九章 音频应用编程3875.png
图 29.3.6 列出系统中所有PCM设备
29.4alsa-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格式的音频文件,如下所示:
第二十九章 音频应用编程4421.png
图 29.4.1 使用aplay播放wav音乐
程序运行之后就会开始播放音乐,因为ALPHA开发板支持喇叭和耳机自动切换,如果不插耳机默认从喇叭播放音乐,插上耳机以后喇叭就会停止播放,切换为耳机播放音乐,这个大家可以自己进行测试。
需要注意的是,aplay工具只能解析wav格式音频文件,不支持mp3格式解码,所以无法使用aplay工具播放mp3音频文件。稍后笔者会向大家介绍如何基于alsa-lib编写一个简单地音乐播放器,实现与aplay相同的效果。
alsamixer
alsamixer是一个很重要的工具,用于配置声卡的混音器,它是一个字符图形化的配置工具,直接在开发板串口终端运行alsamixer命令,打开图形化配置界面,如下所示:
第二十九章 音频应用编程4789.png
图 29.4.2 alsamixer界面
alsamixer可对声卡的混音器进行配置,左上角“Card: wm8960-audio”表示当前配置的声卡为wm8960-audio,如果你的系统中注册了多个声卡,可以按F6进行选择。
按下H键可查看界面的操作说明,如下所示:
第二十九章 音频应用编程4970.png
图 29.4.3 alsamixer界面操作说明
不同声卡支持的混音器配置选项是不同的,这个与具体硬件相关,需要硬件上的支持!上图展示的便是开发板WM8960声卡所支持的配置项,包括Playback播放和Capture录音,左上角View处提示:
  1. View: F3:[Playback] F4: Capture  F5: All
复制代码


表示当前显示的是[Playback]的配置项,通过F4按键切换为Capture、或按F5显示所有配置项。
Tips:在终端按下F4或F5按键时,可能会直接退出配置界面,这个原因可能是F4或F5快捷键被其它程序给占用了,大家可以试试在Ubuntu系统下使用ssh远程登录开发板,然后在Ubuntu ssh终端执行alsamixer程序,笔者测试F4、F5都是正常的。
左上角Item处提示:
  1. 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工具完成的,稍后向大家介绍。
  1. alsactl
复制代码


配置好声卡之后,如果直接关机,下一次重启之后之前的设置都会消失,必须要重新设置,所以我们需要对配置进行保存,如何保存呢?可通过alsactl工具完成。
使用alsactl工具可以将当前声卡的配置保存在一个文件中,这个文件默认是/var/lib/alsa/asound.state,譬如使用alsactl工具将声卡配置保存在该文件中:
  1. alsactl -f /var/lib/alsa/asound.state store
复制代码


-f选项指定保存在哪一个文件中,当然也可以不用指定,如果不指定则使用alsactl默认的配置文件/var/lib/alsa/asound.state,store表示保存配置。保存成功以后就会生成/var/lib/alsa/asound.state这个文件,asound.state文件中保存了声卡的各种设置信息,大家可以打开此文件查看里面的内容,如下所示:
第二十九章 音频应用编程6704.png
图 29.4.4 asound.state文件部分内容
除了保存配置之外,还可以加载配置,譬如使用/var/lib/alsa/asound.state文件中的配置信息来配置声卡,可执行如下命令:
  1. 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工具加载(或保存)配置。
  1. amixer
复制代码


amixer工具也是一个声卡配置工具,与alsamixer功能相同,区别在于,alsamixer是一个基于字符图形化的配置工具、而amixer不是图形化配置工具,直接使用命令行配置即可,详细地用法大家可以执行"amixer --help"命令查看,下面笔者简单地提一下该工具怎么用:
执行命令"amixer scontrols"可以查看到有哪些配置项,如下所示:
第二十九章 音频应用编程7339.png
图 29.4.5 查看有哪些配置项
从打印信息可知,这里打印出来的配置项与alsamixer配置界面中所看到的配置项是相同的,那如何进去配置呢?不同的配置项对应的配置方法(配置值或值类型)是不一样的,可以先使用命令"amixer scontents"查看配置项的说明,如下所示:
  1. amixer scontents
复制代码


第二十九章 音频应用编程7541.png
图 29.4.6 每一个配置项的配置说明
“Headphone”配置项用于设置耳机音量,音量可调节范围为0-127,当前音量为115(左右声道都是115);有些设置项是bool类型,只有on和off两种状态。
譬如将耳机音量左右声道都设置为100,可执行如下命令进行设置:
  1. amixer sset Headphone 100,100
复制代码


譬如打开或关闭Headphone Playback ZC:
  1. amixer sset "Headphone Playback ZC" off                #关闭ZC
  2. amixer sset "Headphone Playback ZC" on                #打开ZC
复制代码


以上给大家举了两个例子,配置方法还是很简单地!
  1. arecord
复制代码


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


第二十九章 音频应用编程8054.png
图 29.4.7 使用arecord工具录音
-f选项指定音频格式,cd则表示cd级别音频,也就是“16 bit little endian, 44100, stereo”;-d选项指定音频录制时间长度,单位是秒;test.wav指定音频数据保存的文件。当录制完成之后,会生成test.wav文件,接着我们可以使用aplay工具播放这一段音频。
以上给大家介绍了alsa-utils提供的几个测试音频、配置声卡的工具,当然,本文也只是进行了简单地介绍,更加详细的使用方法还需要大家自己查看帮助信息。
29.5编写一个简单地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提供的一些示例代码,如下所示:
第二十九章 音频应用编程8918.png
图 29.5.1 ALSA提供的参考代码
点击对应源文件即可查看源代码。
以上便是ALSA提供的帮助文档以及参考代码,链接地址已经给出了,大家有兴趣可以看一下。
本小节笔者将向大家介绍如何基于alsa-lib编写一个简单地音频应用程序,譬如播放音乐、录音等;但在此之前,首先我们需要先来了解一些基本的概念,为后面的学习打下一个坚实的基础!
29.5.1一些基本概念
主要是与音频相关的基本概念,因为在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帧、一帧包含两个样本(左、右两个声道):
第二十九章 音频应用编程10357.png
图 29.5.2 buffer/period/frame/sample之间的关系示例图
音频设备底层驱动程序使用DMA来搬运数据,这个buffer中有4个period,每当DMA搬运完一个period的数据就会触发一次中断,因此搬运整个buffer中的数据将产生4次中断。ALSA为什么这样做?直接把整个buffer中的数据一次性搬运过去岂不是更快?情况并非如此,我们没有考虑到一个很重要的问题,那就是延迟;如果数据缓存区buffer很大,一次传输整个buffer中的数据可能会导致不可接受的延迟,因为一次搬运的数据量越大,所花费的时间就越长,那么必然会导致数据从传输开始到发出声音(以播放为例)这个过程所经历的时间就会越长,这就是延迟。为了解决这个问题,ALSA把缓存区拆分成多个周期,以周期为传输单元进行传输数据。
所以,周期不宜设置过大,周期过大会导致延迟过高;但周期也不能太小,周期太小会导致频繁触发中断,这样会使得CPU被频繁中断而无法执行其它的任务,使得效率降低!所以,周期大小要合适,在延迟可接受的情况下,尽量设置大一些,不过这个需要根据实际应用场合而定,有些应用场合,可能要求低延迟、实时性高,但有些应用场合没有这种需求。
数据之间的传输
这里再介绍一下数据之间传输的问题,这个问题很重要,大家一定要理解,这样会更好的帮助我们理解代码、理解代码的逻辑。
&#61548CM播放情况下
在播放情况下,buffer中存放了需要播放的PCM音频数据,由应用程序向buffer中写入音频数据,buffer中的音频数据由DMA传输给音频设备进行播放,所以应用程序向buffer写入数据、音频设备从buffer读取数据,这就是buffer中数据的传输情况。
图 29.5.2中标识有read pointer和write pointer指针,write pointer指向当前应用程序写buffer的位置、read pointer指向当前音频设备读buffer的位置。在数据传输之前(播放之前),buffer缓冲区是没有数据的,此时write/read pointer均指向了buffer的起始位置,也就是第一个周期的起始位置,如下所示:
第二十九章 音频应用编程11341.png
图 29.5.3 pointer指向buffer起始位置
应用程序向buffer写入多少帧数据,则write pointer指针向前移动多少帧,当应用程序向buffer中写入一个周期的数据时,write pointer指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以此类推!当write pointer移动到buffer末尾时,又会回到buffer的起始位置,以此循环!所以由此可知,这是一个环形缓冲区。
以上是应用程序写buffer的一个过程,接着再来看看音频设备读buffer(播放)的过程。在播放开始之前,read pointer指向了buffer的起始位置,也就是第一个周期的起始位置。音频设备每次只播放一个周期的数据(读取一个周期),每一次都是从read pointer所指位置开始读取;每读取一个周期,read pointer指针向前移动一个周期,同样,当read pointer指针移动到buffer末尾时,又会回到buffer的起始位置,以此构成一个循环!
应用程序需要向buffer中写入音频数据,音频设备才能读取数据进行播放,如果read pointer所指向的周期并没有填充音频数据,则无法播放!当buffer数据满时,应用程序将不能再写入数据,否则就会覆盖之前的数据,必须要等待音频设备播放完一个周期,音频设备每播放完一个周期,这个周期就变成空闲状态了,此时应用程序就可以写入一个周期的数据以填充这个空闲周期。
&#61548CM录音情况下
在录音情况下,buffer中存放了音频设备采集到的音频数据(外界模拟声音通过ADC转为数字声音),由音频设备向buffer中写入音频数据(DMA搬运),而应用程序从buffer中读取数据,所以音频设备向buffer写入数据、应用程序从buffer读取数据,这就是录音情况下buffer中数据的传输情况。
回到图 29.5.2中,此时write pointer指向音频设备写buffer的位置、read pointer指向应用程序读buffer的位置。在录音开始之前,buffer缓冲区是没有数据的,此时write/read pointer均指向了buffer的起始位置,也就是第一个周期的起始位置,如图 29.5.3中所示。
音频设备向buffer写入多少帧数据,则write pointer指针向前移动多少帧,音频设备每次只采集一个周期,将采集到的数据写入buffer中,从write pointer所指位置开始写入;当音频设备向buffer中写入一个周期的数据时,write pointer指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以此类推!当write pointer移动到buffer末尾时,又会回到buffer的起始位置,以此构成循环!
以上是音频设备写buffer的一个过程,接着再来看看应用程序读buffer的过程。在录音开始之前,read pointer指向了buffer的起始位置,也就是第一个周期的起始位置。同样,应用程序从buffer读取了多少帧数据,则read pointer指针向前移动多少帧;从read pointer所指位置开始读取,当read pointer指针移动到buffer末尾时,又会回到buffer的起始位置,以此构成一个循环!
音频设备需要向buffer中写入音频数据,应用程序才能从buffer中读取数据(录音),如果read pointer所指向的周期并没有填充音频数据,则无法读取!当buffer中没有数据时,需要等待音频设备向buffer中写入数据,音频设备每次写入一个周期,当应用程序读取完这个周期的数据后,这个周期又变成了空闲周期,需要等待音频设备写入数据。
Over and Under Run
当一个声卡处于工作状态时,环形缓冲区buffer中的数据总是连续地在音频设备和应用程序缓存区间传输,如下图所示:
第二十九章 音频应用编程13069.png
图 29.5.4 buffer中数据的传输
上图展示了声卡在工作状态下,buffer中数据的传输情况,总是连续地在音频设备和应用程序缓存区间传输,但事情并不总是那么完美、也会出现有例外;譬如在录音例子中,如果应用程序读取数据不够快,环形缓冲区buffer中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖;这种数据的丢失被称为overrun。在播放例子中,如果应用程序写入数据到环形缓冲区buffer中的速度不够快,缓存区将会“饿死”(缓冲区中无数据可播放);这样的错误被称为underrun(欠载)。在ALSA文档中,将这两种情形统称为"XRUN",适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。
29.5.2打开PCM设备
从本小节开始,将正式介绍如何编写一个音频应用程序,首先我们需要在应用程序中包含alsa-lib库的头文件<alsa/asoundlib.h>,这样才能在应用程序中调用alsa-lib库函数以及使用相关宏。
第一步需要打开PCM设备,调用函数snd_pcm_open(),该函数原型如下所示:
  1. 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设备,函数原型如下所示:
  1. int snd_pcm_close(snd_pcm_t *pcm);
复制代码


使用示例:
调用snd_pcm_open()函数打开声卡0的PCM播放设备0:
  1. snd_pcm_t *pcm_handle = NULL;
  2. int ret;

  3. ret = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
  4. if (0 > ret) {
  5.         fprintf(stderr, "snd_pcm_open error: %s\n", snd_strerror(ret));
  6.         return -1;
  7. }
复制代码


29.5.3设置硬件参数
打开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对象,如下所示:
  1. snd_pcm_hw_params_t *hwparams = NULL;

  2. snd_pcm_hw_params_malloc(&hwparams);
复制代码



  1. 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对象占用的内存空间。函数原型如下所示:
  1. 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对象,如下所示:
  1. 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设置访问类型,其函数原型如下所示:
  1. int snd_pcm_hw_params_set_access(snd_pcm_t *pcm,
  2.                 snd_pcm_hw_params_t * params,
  3.                 snd_pcm_access_t access
  4. )
复制代码


参数access指定设备的访问类型,是一个snd_pcm_access_t类型常量,这是一个枚举类型,如下所示:
  1. enum snd_pcm_access_t {
  2.         SND_PCM_ACCESS_MMAP_INTERLEAVED = 0,        //mmap access with simple interleaved channels
  3.         SND_PCM_ACCESS_MMAP_NONINTERLEAVED,        //mmap access with simple non interleaved channels
  4.         SND_PCM_ACCESS_MMAP_COMPLEX,                //mmap access with complex placement
  5.         SND_PCM_ACCESS_RW_INTERLEAVED,                //snd_pcm_readi/snd_pcm_writei access
  6.         SND_PCM_ACCESS_RW_NONINTERLEAVED,        //snd_pcm_readn/snd_pcm_writen access
  7.         SND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED
  8. };
复制代码


通常,将访问类型设置为SND_PCM_ACCESS_RW_INTERLEAVED,交错访问模式,通过snd_pcm_readi/snd_pcm_writei对PCM设备进行读/写操作。
函数调用成功返回0;失败将返回一个小于0的错误码,可通过snd_strerror()函数获取错误描述信息。
使用示例:
  1. ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  2. if (0 > ret)
  3.         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设备的数据格式,函数原型如下所示:
  1. int snd_pcm_hw_params_set_format(snd_pcm_t *pcm,
  2.         snd_pcm_hw_params_t *params,
  3.         snd_pcm_format_t format
  4. )
复制代码


参数format指定数据格式,该参数是一个snd_pcm_format_t类型常量,这是一个枚举类型,如下所示:
  1. enum snd_pcm_format_t {
  2.   SND_PCM_FORMAT_UNKNOWN = -1,
  3.   SND_PCM_FORMAT_S8 = 0,
  4.   SND_PCM_FORMAT_U8,
  5.   SND_PCM_FORMAT_S16_LE,
  6.   SND_PCM_FORMAT_S16_BE,
  7.   SND_PCM_FORMAT_U16_LE,
  8.   SND_PCM_FORMAT_U16_BE,
  9.   SND_PCM_FORMAT_S24_LE,
  10.   SND_PCM_FORMAT_S24_BE,
  11.   SND_PCM_FORMAT_U24_LE,
  12.   SND_PCM_FORMAT_U24_BE,
  13.   SND_PCM_FORMAT_S32_LE,
  14.   SND_PCM_FORMAT_S32_BE,
  15.   SND_PCM_FORMAT_U32_LE,
  16.   SND_PCM_FORMAT_U32_BE,
  17.   SND_PCM_FORMAT_FLOAT_LE,
  18.   SND_PCM_FORMAT_FLOAT_BE,
  19.   SND_PCM_FORMAT_FLOAT64_LE,
  20.   SND_PCM_FORMAT_FLOAT64_BE,
  21.   SND_PCM_FORMAT_IEC958_SUBFRAME_LE,
  22.   SND_PCM_FORMAT_IEC958_SUBFRAME_BE,
  23.   SND_PCM_FORMAT_MU_LAW,
  24.   SND_PCM_FORMAT_A_LAW,
  25.   SND_PCM_FORMAT_IMA_ADPCM,
  26.   SND_PCM_FORMAT_MPEG,
  27.   SND_PCM_FORMAT_GSM,
  28.   SND_PCM_FORMAT_S20_LE,
  29.   SND_PCM_FORMAT_S20_BE,
  30.   SND_PCM_FORMAT_U20_LE,
  31.   SND_PCM_FORMAT_U20_BE,
  32.   SND_PCM_FORMAT_SPECIAL = 31,
  33.   SND_PCM_FORMAT_S24_3LE = 32,
  34.   SND_PCM_FORMAT_S24_3BE,
  35.   SND_PCM_FORMAT_U24_3LE,
  36.   SND_PCM_FORMAT_U24_3BE,
  37.   SND_PCM_FORMAT_S20_3LE,
  38.   SND_PCM_FORMAT_S20_3BE,
  39.   SND_PCM_FORMAT_U20_3LE,
  40.   SND_PCM_FORMAT_U20_3BE,
  41.   SND_PCM_FORMAT_S18_3LE,
  42.   SND_PCM_FORMAT_S18_3BE,
  43.   SND_PCM_FORMAT_U18_3LE,
  44.   SND_PCM_FORMAT_U18_3BE,
  45.   SND_PCM_FORMAT_G723_24,
  46.   SND_PCM_FORMAT_G723_24_1B,
  47.   SND_PCM_FORMAT_G723_40,
  48.   SND_PCM_FORMAT_G723_40_1B,
  49.   SND_PCM_FORMAT_DSD_U8,
  50.   SND_PCM_FORMAT_DSD_U16_LE,
  51.   SND_PCM_FORMAT_DSD_U32_LE,
  52.   SND_PCM_FORMAT_DSD_U16_BE,
  53.   SND_PCM_FORMAT_DSD_U32_BE,
  54.   SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_DSD_U32_BE,
  55.   SND_PCM_FORMAT_S16 = SND_PCM_FORMAT_S16_LE,
  56.   SND_PCM_FORMAT_U16 = SND_PCM_FORMAT_U16_LE,
  57.   SND_PCM_FORMAT_S24 = SND_PCM_FORMAT_S24_LE,
  58.   SND_PCM_FORMAT_U24 = SND_PCM_FORMAT_U24_LE,
  59.   SND_PCM_FORMAT_S32 = SND_PCM_FORMAT_S32_LE,
  60.   SND_PCM_FORMAT_U32 = SND_PCM_FORMAT_U32_LE,
  61.   SND_PCM_FORMAT_FLOAT = SND_PCM_FORMAT_FLOAT_LE,
  62.   SND_PCM_FORMAT_FLOAT64 = SND_PCM_FORMAT_FLOAT64_LE,
  63.   SND_PCM_FORMAT_IEC958_SUBFRAME = SND_PCM_FORMAT_IEC958_SUBFRAME_LE,
  64.   SND_PCM_FORMAT_S20 = SND_PCM_FORMAT_S20_LE,
  65.   SND_PCM_FORMAT_U20 = SND_PCM_FORMAT_U20_LE
  66. };
复制代码


用的最多的格式是SND_PCM_FORMAT_S16_LE,有符号16位、小端模式。当然,音频设备不一定支持用户所指定的格式,在此之前,用户可以调用snd_pcm_hw_params_test_format()函数测试PCM设备是否支持某种格式,如下所示:
  1. if (snd_pcm_hw_params_test_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE)) {
  2.         // 返回一个非零值 表示不支持该格式
  3. }
  4. else {
  5.         // 返回0表示支持
  6. }
复制代码


(3)设置声道数:snd_pcm_hw_params_set_channels()
调用snd_pcm_hw_params_set_channels()函数设置PCM设备的声道数,函数原型如下所示:
  1. int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm,
  2.         snd_pcm_hw_params_t *params,
  3.         unsigned int val
  4. )
复制代码


参数val指定声道数量,val=2表示双声道,也就是立体声。函数调用成功返回0,失败返回小于0的错误码。
使用示例:
  1. ret = snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 2);
  2. if (0 > ret)
  3.         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设置采样率大小,其函数原型如下所示:
  1. int snd_pcm_hw_params_set_rate(snd_pcm_t *pcm,
  2.         snd_pcm_hw_params_t *params,
  3.         unsigned int val,
  4.         int dir
  5. )
复制代码


参数val指定采样率大小,譬如44100;参数dir用于控制方向,若dir=-1,则实际采样率小于参数val指定的值;dir=0表示实际采样率等于参数val;dir=1表示实际采样率大于参数val。
函数调用成功返回0;失败将返回小于0的错误码。
使用示例:
  1. ret = snd_pcm_hw_params_set_rate(pcm_handle, hwparams, 44100, 0);
  2. if (0 > ret)
  3.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
复制代码


(5)设置周期大小:snd_pcm_hw_params_set_period_size()
这里说的周期,也就是29.5.1小节中向大家介绍的周期,一个周期的大小使用帧来衡量,譬如一个周期1024帧;调用snd_pcm_hw_params_set_period_size()函数设置周期大小,其函数原型如下所示:
  1. int snd_pcm_hw_params_set_period_size(snd_pcm_t *pcm,
  2.         snd_pcm_hw_params_t *params,
  3.         snd_pcm_uframes_t val,
  4.         int dir
  5. )
复制代码


alsa-lib使用snd_pcm_uframes_t类型表示帧的数量;参数dir与snd_pcm_hw_params_set_rate()函数的dir参数意义相同。
使用示例(将周期大小设置为1024帧):
  1. ret = snd_pcm_hw_params_set_period_size(pcm_handle, hwparams, 1024, 0);
  2. if (0 > ret)
  3.         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的大小,其函数原型如下所示:
  1. int snd_pcm_hw_params_set_buffer_size(snd_pcm_t *pcm,
  2.         snd_pcm_hw_params_t *params,
  3.         snd_pcm_uframes_t val
  4. )
复制代码


参数val指定buffer的大小,以帧为单位,通常buffer的大小是周期大小的整数倍,譬如16个周期;但函数snd_pcm_hw_params_set_buffer_size()是以帧为单位来表示buffer的大小,所以需要转换一下,譬如将buffer大小设置为16个周期,则参数val等于16 * 1024(假设一个周期为1024帧)=16384帧。
函数调用成功返回0;失败返回一个小于0的错误码。
使用示例:
  1. ret = snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, 16*1024);
  2. if (0 > ret)
  3.         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大小,其函数原型如下所示:
  1. int snd_pcm_hw_params_set_periods(snd_pcm_t *pcm,
  2.         snd_pcm_hw_params_t *params,
  3.         unsigned int val,
  4.         int dir
  5. )
复制代码


参数val指定了buffer的大小,该大小以周期为单位、并不是以帧为单位,注意区分!
参数dir与snd_pcm_hw_params_set_rate()函数的dir参数意义相同。
函数调用成功返回0;失败将返回一个小于0的错误码。
使用示例:
  1. ret = snd_pcm_hw_params_set_periods(pcm_handle, hwparams, 16, 0);                //buffer大小为16个周期
  2. if (0 > ret)
  3.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
复制代码


(7)安装/加载硬件配置参数:snd_pcm_hw_params()
参数设置完成之后,最后调用snd_pcm_hw_params()加载/安装配置、将配置参数写入硬件使其生效,其函数原型如下所示:
  1. 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状态(也就是处于一种准备好的状态)。
使用示例:
  1. ret = snd_pcm_hw_params(pcm_handle, hwparams);
  2. if (0 > ret)
  3.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
复制代码


29.5.4读/写数据
接下来就可以进行读/写数据了,如果是PCM播放,则调用snd_pcm_writei()函数向播放缓冲区buffer中写入音频数据;如果是PCM录音,则调用snd_pcm_readi()函数从录音缓冲区buffer中读取数据,它们的函数原型如下所示:
  1. snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm,
  2.         const void *buffer,
  3.         snd_pcm_uframes_t size
  4. )

  5. snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm,
  6.         void *buffer,
  7.         snd_pcm_uframes_t size
  8. )
复制代码


参数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()函数也不会阻塞、而是立即以错误形式返回。
  1. 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进行读写。
29.5.5示例代码之PCM播放
通过上小节的一个介绍,相信大家对alsa-lib音频应用编程已经有了基本的认识和理解,本小节我们来编写一个简单地音乐播放器,可以播放WAV音频文件,代码笔者已经写好了,如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_playback.c。
示例代码 29.5.1 一个简单地PCM播放示例程序
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_playback.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM播放示例代码--播放WAV音频文件
  7. 其他 : 无
  8. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  10. ***************************************************************/

  11. #include <stdio.h>
  12. #include <stdlib.h>
  13. #include <errno.h>
  14. #include <string.h>
  15. #include <alsa/asoundlib.h>

  16. /************************************
  17. 宏定义
  18. ************************************/
  19. #define PCM_PLAYBACK_DEV    "hw:0,0"

  20. /************************************
  21. WAV音频文件解析相关数据结构申明
  22. ************************************/
  23. typedef struct WAV_RIFF {
  24.     char ChunkID[4];                         /* "RIFF" */
  25.     u_int32_t ChunkSize;                /* 从下一个地址开始到文件末尾的总字节数 */
  26.     char Format[4];                     /* "WAVE" */
  27. } __attribute__ ((packed)) RIFF_t;

  28. typedef struct WAV_FMT {
  29.     char Subchunk1ID[4];                 /* "fmt " */
  30.     u_int32_t Subchunk1Size;              /* 16 for PCM */
  31.     u_int16_t AudioFormat;                /* PCM = 1*/
  32.     u_int16_t NumChannels;               /* Mono = 1, Stereo = 2, etc. */
  33.     u_int32_t SampleRate;                /* 8000, 44100, etc. */
  34.     u_int32_t ByteRate;                   /* = SampleRate * NumChannels * BitsPerSample/8 */
  35.     u_int16_t BlockAlign;                /* = NumChannels * BitsPerSample/8 */
  36.     u_int16_t BitsPerSample;              /* 8bits, 16bits, etc. */
  37. } __attribute__ ((packed)) FMT_t;
  38. static FMT_t wav_fmt;

  39. typedef struct WAV_DATA {
  40.     char Subchunk2ID[4];                 /* "data" */
  41.     u_int32_t Subchunk2Size;              /* data size */
  42. } __attribute__ ((packed)) DATA_t;

  43. /************************************
  44. static静态全局变量定义
  45. ************************************/
  46. static snd_pcm_t *pcm = NULL;                    //pcm句柄
  47. static unsigned int buf_bytes;                       //应用程序缓冲区的大小(字节为单位)
  48. static void *buf = NULL;                          //指向应用程序缓冲区的指针
  49. static int fd = -1;                                 //指向WAV音频文件的文件描述符
  50. static snd_pcm_uframes_t period_size = 1024;          //周期大小(单位: 帧)
  51. static unsigned int periods = 16;                     //周期数(设备驱动层buffer的大小)

  52. static int snd_pcm_init(void)
  53. {
  54.     snd_pcm_hw_params_t *hwparams = NULL;
  55.     int ret;

  56.     /* 打开PCM设备 */
  57.     ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
  58.     if (0 > ret) {
  59.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  60.                     PCM_PLAYBACK_DEV, snd_strerror(ret));
  61.         return -1;
  62.     }

  63.     /* 实例化hwparams对象 */
  64.     snd_pcm_hw_params_malloc(&hwparams);

  65.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  66.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  67.     if (0 > ret) {
  68.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  69.         goto err2;
  70.     }

  71.     /**************
  72.      设置参数
  73.     ***************/
  74.     /* 设置访问类型: 交错模式 */
  75.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  76.     if (0 > ret) {
  77.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  78.         goto err2;
  79.     }

  80.     /* 设置数据格式: 有符号16位、小端模式 */
  81.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  82.     if (0 > ret) {
  83.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  84.         goto err2;
  85.     }

  86.     /* 设置采样率 */
  87.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
  88.     if (0 > ret) {
  89.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  90.         goto err2;
  91.     }

  92.     /* 设置声道数: 双声道 */
  93.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
  94.     if (0 > ret) {
  95.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  96.         goto err2;
  97.     }

  98.     /* 设置周期大小: period_size */
  99.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  100.     if (0 > ret) {
  101.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  102.         goto err2;
  103.     }

  104.     /* 设置周期数(驱动层buffer的大小): periods */
  105.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  106.     if (0 > ret) {
  107.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  108.         goto err2;
  109.     }

  110.     /* 使配置生效 */
  111.     ret = snd_pcm_hw_params(pcm, hwparams);
  112.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  113.     if (0 > ret) {
  114.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  115.         goto err1;
  116.     }

  117.     buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小
  118.     return 0;

  119. err2:
  120.     snd_pcm_hw_params_free(hwparams);   //释放内存
  121. err1:
  122.     snd_pcm_close(pcm); //关闭pcm设备
  123.     return -1;
  124. }

  125. static int open_wav_file(const char *file)
  126. {
  127.     RIFF_t wav_riff;
  128.     DATA_t wav_data;
  129.     int ret;

  130.     fd = open(file, O_RDONLY);
  131.     if (0 > fd) {
  132.         fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
  133.         return -1;
  134.     }

  135.     /* 读取RIFF chunk */
  136.     ret = read(fd, &wav_riff, sizeof(RIFF_t));
  137.     if (sizeof(RIFF_t) != ret) {
  138.         if (0 > ret)
  139.             perror("read error");
  140.         else
  141.             fprintf(stderr, "check error: %s\n", file);
  142.         close(fd);
  143.         return -1;
  144.     }

  145.     if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
  146.         strncmp("WAVE", wav_riff.Format, 4)) {
  147.         fprintf(stderr, "check error: %s\n", file);
  148.         close(fd);
  149.         return -1;
  150.     }

  151.     /* 读取sub-chunk-fmt */
  152.     ret = read(fd, &wav_fmt, sizeof(FMT_t));
  153.     if (sizeof(FMT_t) != ret) {
  154.         if (0 > ret)
  155.             perror("read error");
  156.         else
  157.             fprintf(stderr, "check error: %s\n", file);
  158.         close(fd);
  159.         return -1;
  160.     }

  161.     if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
  162.         fprintf(stderr, "check error: %s\n", file);
  163.         close(fd);
  164.         return -1;
  165.     }

  166.     /* 打印音频文件的信息 */
  167.     printf("<<<<音频文件格式信息>>>>\n\n");
  168.     printf("  file name:     %s\n", file);
  169.     printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
  170.     printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
  171.     printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
  172.     printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
  173.     printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
  174.     printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
  175.     printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

  176.     /* sub-chunk-data */
  177.     if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
  178.                 SEEK_SET)) {
  179.         perror("lseek error");
  180.         close(fd);
  181.         return -1;
  182.     }

  183.     while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

  184.         /* 找到sub-chunk-data */
  185.         if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
  186.             return 0;

  187.         if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
  188.             perror("lseek error");
  189.             close(fd);
  190.             return -1;
  191.         }
  192.     }

  193.     fprintf(stderr, "check error: %s\n", file);
  194.     return -1;
  195. }

  196. /************************************
  197. main主函数
  198. ************************************/
  199. int main(int argc, char *argv[])
  200. {
  201.     int ret;

  202.     if (2 != argc) {
  203.         fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
  204.         exit(EXIT_FAILURE);
  205.     }

  206.     /* 打开WAV音频文件 */
  207.     if (open_wav_file(argv[1]))
  208.         exit(EXIT_FAILURE);

  209.     /* 初始化PCM Playback设备 */
  210.     if (snd_pcm_init())
  211.         goto err1;

  212.     /* 申请读缓冲区 */
  213.     buf = malloc(buf_bytes);
  214.     if (NULL == buf) {
  215.         perror("malloc error");
  216.         goto err2;
  217.     }

  218.     /* 播放 */
  219.     for ( ; ; ) {

  220.         memset(buf, 0x00, buf_bytes);   //buf清零
  221.         ret = read(fd, buf, buf_bytes); //从音频文件中读取数据
  222.         if (0 >= ret)   // 如果读取出错或文件读取完毕
  223.             goto err3;

  224.         ret = snd_pcm_writei(pcm, buf, period_size);
  225.         if (0 > ret) {
  226.             fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  227.             goto err3;
  228.         }
  229.         else if (ret < period_size) {//实际写入的帧数小于指定的帧数
  230.             //此时我们需要调整下音频文件的读位置
  231.             //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
  232.             //frame_bytes表示一帧的字节大小
  233.             if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  234.                 perror("lseek error");
  235.                 goto err3;
  236.             }
  237.         }
  238.     }

  239. err3:
  240.     free(buf);     //释放内存
  241. err2:
  242.     snd_pcm_close(pcm); //关闭pcm设备
  243. err1:
  244.     close(fd);      //关闭打开的音频文件
  245.     exit(EXIT_FAILURE);
  246. }
复制代码


本应用程序实现可以播放WAV音频文件,关于WAV文件格式的解析,本文档不作说明,WAV文件格式其实非常简单,大家自己百度了解。
在main()函数中,首先对参数进行了校验,执行测试程序需要用户传入一个参数,这个参数用于指定一个需要播放的WAV音频文件。接着调用自定义函数open_wav_file()对WAV文件进行解析,其实也就是对它的头部数据进行校验、解析,获取音频格式信息以及音频数据的位置偏移量。
接着调用自定义函数snd_pcm_init()对PCM设备进行初始化,在snd_pcm_init()函数中,首先调用alsa-lib库函数snd_pcm_open()打开PCM播放设备,接着对PCM设备硬件参数进行设置,包括:访问类型、数据格式、采样率、声道数、周期大小以及buffer的大小,这些内容前面已经给大家详细介绍过,这里不再重述!
回到main()函数,调用C库函数malloc()申请分配一个缓冲区,用于存放从音频文件中读取出来的音频数据。
一切准备好之后,就可以播放音频了,在for循环中,首先调用read()函数从音频文件中读取出音频数据,每次读取一个周期,将读取到的数据存放在buf指向的缓冲区中,接着调用alsa-lib库函数snd_pcm_writei()写入数据进行播放。示例程序中调用snd_pcm_open()时使用的是阻塞方式,当驱动层环形缓冲区buffer还未满时,调用snd_pcm_writei()并不会阻塞,而是会将数据写入到环形缓冲区中、然后返回;调用一次snd_pcm_writei()写入一个周期数据、调用一次再写入一个周期;当环形缓冲区数据满时,调用snd_pcm_writei()会阻塞,直到音频设备播放完一个周期、此时会出现一个空闲周期,接着snd_pcm_writei()将数据填充到这个空闲周期后返回。
以上对示例代码进行了一个简单地介绍,代码本身非常简单,没什么难点,代码中注释信息也已经描述地比较清楚了,相信大家都可以看懂。需要注意,必须要在源码中包含alsa-lib的头文件<alsa/asoundlib.h>!
编译示例代码
接下来编译上述示例代码,编译的方法非常简单,按照以前的惯例,编译时无非是要指定两个路径(alsa-lib头文件所在路径、alsa-lib库文件所在路径)以及链接库(需要链接的库文件名称),譬如:
  1. ${CC} -o testApp testApp.c -Ixxx -Lyyy -lzzz
复制代码


xxx表示头文件的路径,yyy表示库文件的路径,zzz表示链接库。
但是我们并没有自己移植alsa-lib,也就意味着我们在Ubuntu下并没有移植、安装alsa-lib,所以这些路径无法指定。其实,我们使用的交叉编译工具对应的安装目录下已经安装了alsa-lib,进入到交叉编译工具安装目录下的sysroots/cortexa7hf-neon-poky-linux-gnueabi目录,譬如笔者使用的Ubuntu系统,交叉编译工具安装路径为/opt/fsl-imx-x11/4.1.15-2.1.0。
第二十九章 音频应用编程33900.png
图 29.5.5 cortexa7hf-neon-poky-linux-gnueabi目录下的文件夹
该目录下有两个目录,lib和usr,这两个目录其实就是Linux系统根目录下的lib和usr;所以lib目录下存放了一些链接库文件,usr目录下包含了include和lib目录,分别存放了头文件和链接库文件。usr/include/alsa目录下存放了alsa-lib的头文件,如下所示:
第二十九章 音频应用编程34141.png
图 29.5.6 alsa-lib的头文件
我们需要包含的头文件asoundlib.h头文件就在该目录下。
usr/lib目录下包含了alsa-lib库文件,如下所示:
第二十九章 音频应用编程34271.png
图 29.5.7 alsa-lib库文件
alsa-lib链接库libasound.so就在该目录下。那既然找到了alsa-lib的头文件路径和库文件路径,编译应用程序时直接指定这些路径即可。但我们不需要自己手动指定这些路径,交叉编译器已经把这些路径添加到它的搜索路径中了,使用echo ${CC}查看环境变量CC的内容,如下所示:
第二十九章 音频应用编程34483.png
图 29.5.8 CC环境变量的内容
其中交叉编译器arm-poky-linux-gnueabi-gcc有一个选--sysroot,它指定了一个路径,这个路径就是交叉编译工具安装目录下的sysroots/cortexa7hf-neon-poky-linux-gnueabi目录,--sysroot选项用于设置目标平台的根目录,设置了平台根目录之后,当编译应用程序时,编译器会将根目录下的usr/include添加到头文件搜索路径中、将根目录下的lib和usr/lib添加到库文件搜索路径中。
所以由此可知,编译应用程序时,我们只需指定链接库即可,如下所示:
  1. ${CC} -o testApp testApp.c -lasound
复制代码


第二十九章 音频应用编程34843.png
图 29.5.9 编译应用程序
测试应用程序
将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,并拷贝一个WAV音频文件到/home/root目录下,如下所示:
第二十九章 音频应用编程34982.png
图 29.5.10 将测试程序和WAV音频文件拷贝到开发板家目录
接着进行测试,在测试之前,我们还需要对声卡混音器进行配置,当然,你也可以不配置,因为开发板出厂系统中声卡是已经配置好的。这里我们直接使用amixer工具进行配置,配置如下:
  1. # 打开耳机播放ZC
  2. amixer sset 'Headphone Playback ZC' on

  3. # 打开喇叭播放ZC
  4. amixer sset 'Speaker Playback ZC' on
  5. amixer sset 'Speaker AC' 3
  6. amixer sset 'Speaker DC' 3

  7. # 音量设置
  8. amixer sset Headphone 105,105                //耳机音量设置
  9. amixer sset Playback 230,230                        //播放音量设置
  10. amixer sset Speaker 118,118                        //喇叭音量设置

  11. # 打开左右声道
  12. amixer sset 'Right Output Mixer PCM' on                //打开右声道
  13. amixer sset 'Left Output Mixer PCM' on                //打开左声道
复制代码


第二十九章 音频应用编程35534.png
图 29.5.11 声卡设置
由于篇幅有限,打印信息不能给大家全部截取出来。声音的大小,大家根据情况进行调节。
声卡设置完成之后,接着运行测试程序,如下所示:
第二十九章 音频应用编程35659.png
图 29.5.12 执行测试程序
程序运行之后,对传入的WAV文件进行解析,并将其音频格式信息打印出来。
此时开发板喇叭便会开始播放音乐,如果连接了耳机,则会通过耳机播放音乐。
29.5.6示例代码值PCM录音
本小节我们来编写一个PCM音频录制(录音)的测试程序,示例代码笔者已经给出,如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_capture.c。
示例代码 29.5.2 一个简单地PCM录音示例程序
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_capture.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM音频采集示例代码--录音
  7. 其他 : 无
  8. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  10. ***************************************************************/

  11. #include <stdio.h>
  12. #include <stdlib.h>
  13. #include <errno.h>
  14. #include <string.h>
  15. #include <alsa/asoundlib.h>

  16. /************************************
  17. 宏定义
  18. ************************************/
  19. #define PCM_CAPTURE_DEV    "hw:0,0"

  20. /************************************
  21. static静态全局变量定义
  22. ************************************/
  23. static snd_pcm_t *pcm = NULL;                    //pcm句柄
  24. static snd_pcm_uframes_t period_size = 1024;         //周期大小(单位: 帧)
  25. static unsigned int periods = 16;                     //周期数(buffer的大小)
  26. static unsigned int rate = 44100;                     //采样率

  27. static int snd_pcm_init(void)
  28. {
  29.     snd_pcm_hw_params_t *hwparams = NULL;
  30.     int ret;

  31.     /* 打开PCM设备 */
  32.     ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
  33.     if (0 > ret) {
  34.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  35.                     PCM_CAPTURE_DEV, snd_strerror(ret));
  36.         return -1;
  37.     }

  38.     /* 实例化hwparams对象 */
  39.     snd_pcm_hw_params_malloc(&hwparams);

  40.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  41.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  42.     if (0 > ret) {
  43.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  44.         goto err2;
  45.     }

  46.     /**************
  47.      设置参数
  48.     ***************/
  49.     /* 设置访问类型: 交错模式 */
  50.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  51.     if (0 > ret) {
  52.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  53.         goto err2;
  54.     }

  55.     /* 设置数据格式: 有符号16位、小端模式 */
  56.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  57.     if (0 > ret) {
  58.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  59.         goto err2;
  60.     }

  61.     /* 设置采样率 */
  62.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
  63.     if (0 > ret) {
  64.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  65.         goto err2;
  66.     }

  67.     /* 设置声道数: 双声道 */
  68.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
  69.     if (0 > ret) {
  70.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  71.         goto err2;
  72.     }

  73.     /* 设置周期大小: period_size */
  74.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  75.     if (0 > ret) {
  76.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  77.         goto err2;
  78.     }

  79.     /* 设置周期数(buffer的大小): periods */
  80.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  81.     if (0 > ret) {
  82.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  83.         goto err2;
  84.     }

  85.     /* 使配置生效 */
  86.     ret = snd_pcm_hw_params(pcm, hwparams);
  87.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  88.     if (0 > ret) {
  89.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  90.         goto err1;
  91.     }

  92.     return 0;

  93. err2:
  94.     snd_pcm_hw_params_free(hwparams);   //释放内存
  95. err1:
  96.     snd_pcm_close(pcm); //关闭pcm设备
  97.     return -1;
  98. }

  99. /************************************
  100. main主函数
  101. ************************************/
  102. int main(int argc, char *argv[])
  103. {
  104.     unsigned char *buf = NULL;
  105.     unsigned int buf_bytes;
  106.     int fd = -1;
  107.     int ret;

  108.     if (2 != argc) {
  109.         fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
  110.         exit(EXIT_FAILURE);
  111.     }

  112.     /* 初始化PCM Capture设备 */
  113.     if (snd_pcm_init())
  114.         exit(EXIT_FAILURE);

  115.     /* 申请读缓冲区 */
  116.     buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
  117.     buf = malloc(buf_bytes);
  118.     if (NULL == buf) {
  119.         perror("malloc error");
  120.         goto err1;
  121.     }

  122.     /* 打开一个新建文件 */
  123.     fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
  124.     if (0 > fd) {
  125.         fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
  126.         goto err2;
  127.     }

  128.     /* 录音 */
  129.     for ( ; ; ) {

  130.         //memset(buf, 0x00, buf_bytes);   //buf清零
  131.         ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期
  132.         if (0 > ret) {
  133.             fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
  134.             goto err3;
  135.         }

  136.         // snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数
  137.         ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
  138.         if (0 >= ret)
  139.             goto err3;
  140.     }

  141. err3:
  142.     close(fd);  //关闭文件
  143. err2:
  144.     free(buf);     //释放内存
  145. err1:
  146.     snd_pcm_close(pcm); //关闭pcm设备
  147.     exit(EXIT_FAILURE);
  148. }
复制代码


在main()函数中,首先对参数进行了校验,执行测试程序需要用户传入一个参数,指定输出文件,因为示例程序中会将录制的音频数据保存到该文件中。
接着调用自定义函数snd_pcm_init()对PCM设备进行初始化,在snd_pcm_init()函数中,首先调用alsa-lib库函数snd_pcm_open()打开PCM录音设备,接着对PCM设备硬件参数进行设置,访问类型设置交错模式SND_PCM_ACCESS_RW_INTERLEAVED、数据格式设置为SND_PCM_FORMAT_S16_LE、采样率设置为44100、双声道、周期大小设置为1024帧、buffer大小设置为16个周期。
回到main()函数,调用C库函数malloc()申请分配一个缓冲区,用于存放从驱动层环形缓冲区buffer读取出来的音频数据。并打开一个新建文件(因为使用了O_CREAT | O_EXCL标志)。
一切准备好之后,就可以进行音频录制了,在for循环中,首先调用alsa-lib库函数snd_pcm_readi()从环形缓冲区中读取音频设备采集到的音频数据,读取出来之后调用write()函数将数据写入到文件中。示例程序中调用snd_pcm_open()时使用的是阻塞方式,当环形缓冲区buffer中有数据可读时,调用snd_pcm_readi()并不会阻塞,而是读取出数据、然后返回;调用一次snd_pcm_readi()读取一个周期、调用一次再读取一个周期;当环形缓冲区为空时,调用snd_pcm_readi()会阻塞,直到音频设备采集到一个周期数据、此时被阻塞snd_pcm_readi()调用被唤醒、读取这一个周期然后返回。
编译示例代码
接下来我们编译示例代码,如下所示:
  1. ${CC} -o testApp testApp.c -lasound
复制代码


第二十九章 音频应用编程41421.png
图 29.5.13 编译示例代码
测试应用程序
将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,在执行测试程序之前,我们需要对声卡进行配置,同样使用amixer工具进行配置,如下:
  1. <font size="4">amixer sset Capture 58,58                //录制音量大小
  2. amixer sset 'ADC PCM' 200,200        //PCM ADC

  3. # 左声道Mixer Boost管理
  4. amixer sset 'Left Input Mixer Boost' off
  5. amixer sset 'Left Boost Mixer LINPUT1' off
  6. amixer sset 'Left Input Boost Mixer LINPUT1' 0
  7. amixer sset 'Left Boost Mixer LINPUT2' off
  8. amixer sset 'Left Input Boost Mixer LINPUT2' 0
  9. amixer sset 'Left Boost Mixer LINPUT3' off
  10. amixer sset 'Left Input Boost Mixer LINPUT3' 0

  11. # 右声道Mixer Boost管理
  12. amixer sset 'Right Input Mixer Boost' on
  13. amixer sset 'Right Boost Mixer RINPUT1' on
  14. amixer sset 'Right Input Boost Mixer RINPUT1' 5
  15. amixer sset 'Right Boost Mixer RINPUT2' on
  16. amixer sset 'Right Input Boost Mixer RINPUT2' 5
  17. amixer sset 'Right Boost Mixer RINPUT3' off
  18. amixer sset 'Right Input Boost Mixer RINPUT3' 0
  19. </font>
复制代码

第二十九章 音频应用编程42313.png
图 29.5.14 声卡配置(录音)
左右声道的Mixer Boost(混音器增强)为什么要这样去配置?这个与硬件设计有关系,我们就不去解释这个了。具体详情可以参考《I.MX6U嵌入式Linux驱动开发指南》文档中音频驱动章节的内容。
接下来,执行测试程序进行录音,如下所示:
第二十九章 音频应用编程42497.png
图 29.5.15 录音
执行测试程序之后,就开始录音了,接着我们可以对着底板上的麦(MIC)说话,板载的MIC如下所示:
第二十九章 音频应用编程42604.png
图 29.5.16 板载麦克风
程序就会把我们说的话录进去;如果想要停止录音、只能终止进程,按Ctrl+C终止应用程序;此时在当前目录下会生成cap.wav音频文件,如下所示:
第二十九章 音频应用编程42738.png
图 29.5.17 生成cap.wav文件
生成的文件是一个纯音频数据的文件,并不是WAV格式的文件,因为这个文件没有头部信息,程序中如果检测到该文件不是WAV格式文件、会直接退出,所以不能直接使用上小节29.5.5的测试程序播放cap.wav文件,这里要注意!当然你可以对上小节的示例代码进行修改,也可直接使用aplay工具播放这段录制的音频,如下:
  1. aplay -f cd cap.wav
复制代码


第二十九章 音频应用编程43007.png
图 29.5.18 使用aplay播放录制的音频
如果录制正常,使用aplay播放出来的声音就是我们录制的声音!
LINE_IN测试
除了麦克风之外,开发板底板上还有一个LINE_IN接口,也就是线路输入,如下图所示:
第二十九章 音频应用编程43162.png
图 29.5.19 LINE_IN接口
上图中左边的是耳机接口、右边的是LINE_IN接口,支持音频输入,我们通过本测试程序对LINE_IN接口进行测试,采集LINE_IN接口输入的音频。测试时我们使用一根3.5mm公对公音频线,一头连接到手机或者电脑、另外一头连接到LINE_IN接口上,然后手机或电脑端播放音乐,那么音频数据就会通过LINE_IN接口输入到开发板被我们的应用程序采集(录制)。
在测试之前,我们需要对声卡进行配置,如下所示:
  1. amixer sset Capture 58,58                //录制音量大小
  2. amixer sset 'ADC PCM' 200,200        //PCM ADC

  3. # 左声道Mixer Boost管理
  4. amixer sset 'Left Input Mixer Boost' off
  5. amixer sset 'Left Boost Mixer LINPUT1' off
  6. amixer sset 'Left Input Boost Mixer LINPUT1' 0
  7. amixer sset 'Left Boost Mixer LINPUT2' on
  8. amixer sset 'Left Input Boost Mixer LINPUT2' 5
  9. amixer sset 'Left Boost Mixer LINPUT3' off
  10. amixer sset 'Left Input Boost Mixer LINPUT3' 0

  11. # 右声道Mixer Boost管理
  12. amixer sset 'Right Input Mixer Boost' on
  13. amixer sset 'Right Boost Mixer RINPUT1' off
  14. amixer sset 'Right Input Boost Mixer RINPUT1' 0
  15. amixer sset 'Right Boost Mixer RINPUT2' off
  16. amixer sset 'Right Input Boost Mixer RINPUT2' 0
  17. amixer sset 'Right Boost Mixer RINPUT3' on
  18. amixer sset 'Right Input Boost Mixer RINPUT3' 5
复制代码


配置好之后就可以进行测试了,执行程序之后,手机或电脑端播放音乐,开发板采集从LINE_IN接口输入的音频数据,测试方式跟MIC麦克风一样,大家自己去测试!
29.6使用异步方式
上小节中的示例代码 29.4.1和示例代码 29.4.2都是采用了同步方式进行读写,这样会使得应用程序无法做一些其它的事情,本小节我们来学习如何使用异步方式读写。
其实使用异步方式读写非常简单,只需要注册异步处理函数即可。
snd_async_add_pcm_handler()函数
alsa-lib提供了snd_async_add_pcm_handler()函数用于注册异步处理函数,其实我们只需要通过这个函数注册一个异步处理函数即可,其函数原型如下所示:
  1. int snd_async_add_pcm_handler(snd_async_handler_t **handler,
  2.         snd_pcm_t *pcm,
  3.         snd_async_callback_t callback,
  4.         void *private_data
  5. )
复制代码


调用该函数需要传入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函数指针如下所示:
  1. 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()函数即可,如下所示:
  1. 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()获取,如下所示:
  1. snd_pcm_t *pcm_handle = snd_async_handler_get_pcm(handler);
复制代码


实现异步I/O,应用程序通常需要完成这三件事情:
使能异步I/O;
设置异步I/O的所有者;
注册信号处理函数(譬如SIGIO信号或其它实时信号)。
这是内容在14.3小节给大家详细介绍过,这里不再啰嗦!所以由此可知,snd_async_add_pcm_handler函数中已经帮我们完成这些事情。
使用示例:
  1. static void snd_playback_async_callback(snd_async_handler_t *handler)
  2. {
  3.     snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄

  4.         ......
  5. }

  6. int main(void)
  7. {
  8.         ......

  9.         snd_async_handler_t *async_handler = NULL;

  10.         /* 注册异步处理函数 */
  11.         ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
  12.         if (0 > ret)
  13.                 fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));

  14.         ......
  15. }
复制代码


调用snd_async_add_pcm_handler()注册了异步回调函数snd_playback_async_callback(),当环形缓冲区有空闲的周期可填充数据时(以播放为例),音频设备驱动程序会向应用程序发送信号(SIGIO),接着应用程序便会跳转到snd_playback_async_callback()函数执行。
而对于录音来说,当环形缓冲区中有数据可读时(譬如音频设备已经录制了一个周期、并将数据写入到了环形缓冲区),驱动程序便会向应用程序发送信号,接着应用程序跳转到回调函数执行。
在播放情况下,通常我们会先将环形缓冲区填满,当音频设备每播放完一个周期,就会产生一个空闲周期,此时应用程序会接收到信号,进而跳转到异步回调函数中执行。
snd_pcm_avail_update()函数
在异步处理函数中,我们通常会使用到这个函数,在录音情况下,应用程序调用snd_pcm_avail_update()函数用于获取当前可读取的帧数;在播放情况下,应用程序调用该函数用于获取当前可写入的帧数。换句话说,也就是驱动层环形缓冲区中当前有多少帧数据可读取(录音)或可写入多少帧数据(播放,环形缓冲区未满时、应用程序才可写入数据)。
该函数原型如下所示:
  1. snd_pcm_sframes_t snd_pcm_avail_update(snd_pcm_t *pcm);
复制代码


本小节主要给大家介绍这两个函数,因为后面的示例代码中会使用到。
29.6.1PCM播放示例-异步方式
通过上面的介绍,本小节我们来编写一个使用异步方式的PCM播放示例程序,直接基于示例代码 29.5.1进行修改,代码笔者已经写好了,如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_playback_async.c。
示例代码 29.6.1 一个简单地PCM播放示例程序(异步方式)
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_playback_async.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM播放示例代码--使用异步方式
  7. 其他 : 无
  8. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  10. ***************************************************************/

  11. #include <stdio.h>
  12. #include <stdlib.h>
  13. #include <errno.h>
  14. #include <string.h>
  15. #include <alsa/asoundlib.h>

  16. /************************************
  17. 宏定义
  18. ************************************/
  19. #define PCM_PLAYBACK_DEV    "hw:0,0"

  20. /************************************
  21. WAV音频文件解析相关数据结构申明
  22. ************************************/
  23. typedef struct WAV_RIFF {
  24.     char ChunkID[4];                   /* "RIFF" */
  25.     u_int32_t ChunkSize;                 /* 从下一个地址开始到文件末尾的总字节数 */
  26.     char Format[4];                      /* "WAVE" */
  27. } __attribute__ ((packed)) RIFF_t;

  28. typedef struct WAV_FMT {
  29.     char Subchunk1ID[4];                 /* "fmt " */
  30.     u_int32_t Subchunk1Size;              /* 16 for PCM */
  31.     u_int16_t AudioFormat;               /* PCM = 1*/
  32.     u_int16_t NumChannels;               /* Mono = 1, Stereo = 2, etc. */
  33.     u_int32_t SampleRate;                /* 8000, 44100, etc. */
  34.     u_int32_t ByteRate;                   /* = SampleRate * NumChannels * BitsPerSample/8 */
  35.     u_int16_t BlockAlign;                 /* = NumChannels * BitsPerSample/8 */
  36.     u_int16_t BitsPerSample;             /* 8bits, 16bits, etc. */
  37. } __attribute__ ((packed)) FMT_t;
  38. static FMT_t wav_fmt;

  39. typedef struct WAV_DATA {
  40.     char Subchunk2ID[4];                 /* "data" */
  41.     u_int32_t Subchunk2Size;             /* data size */
  42. } __attribute__ ((packed)) DATA_t;

  43. /************************************
  44. static静态全局变量定义
  45. ************************************/
  46. static snd_pcm_t *pcm = NULL;                    //pcm句柄
  47. static unsigned int buf_bytes;                        //应用程序缓冲区的大小(字节为单位)
  48. static void *buf = NULL;                          //指向应用程序缓冲区的指针
  49. static int fd = -1;                                 //指向WAV音频文件的文件描述符
  50. static snd_pcm_uframes_t period_size = 1024;         //周期大小(单位: 帧)
  51. static unsigned int periods = 16;                    //周期数(设备驱动层buffer的大小)

  52. /************************************
  53. static静态函数
  54. ************************************/
  55. static void snd_playback_async_callback(snd_async_handler_t *handler)
  56. {
  57.     snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄
  58.     snd_pcm_sframes_t avail;
  59.     int ret;

  60.     avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充
  61.     while (avail >= period_size) {  //我们一次写入一个周期

  62.         memset(buf, 0x00, buf_bytes);   //buf清零
  63.         ret = read(fd, buf, buf_bytes);
  64.         if (0 >= ret)
  65.             goto out;

  66.         ret = snd_pcm_writei(handle, buf, period_size);
  67.         if (0 > ret) {
  68.             fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  69.             goto out;
  70.         }
  71.         else if (ret < period_size) {//实际写入的帧数小于指定的帧数
  72.             //此时我们需要调整下音频文件的读位置 重新读取没有播放出去的数据
  73.             //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
  74.             //frame_bytes表示一帧的字节大小
  75.             if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  76.                 perror("lseek error");
  77.                 goto out;
  78.             }
  79.         }

  80.         avail = snd_pcm_avail_update(handle);   //再次获取、更新avail
  81.     }

  82.     return;
  83. out:
  84.     snd_pcm_close(handle); //关闭pcm设备
  85.     free(buf);
  86.     close(fd);      //关闭打开的音频文件
  87.     exit(EXIT_FAILURE); //退出程序
  88. }

  89. static int snd_pcm_init(void)
  90. {
  91.     snd_pcm_hw_params_t *hwparams = NULL;
  92.     snd_async_handler_t *async_handler = NULL;
  93.     int ret;

  94.     /* 打开PCM设备 */
  95.     ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
  96.     if (0 > ret) {
  97.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  98.                     PCM_PLAYBACK_DEV, snd_strerror(ret));
  99.         return -1;
  100.     }

  101.     /* 实例化hwparams对象 */
  102.     snd_pcm_hw_params_malloc(&hwparams);

  103.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  104.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  105.     if (0 > ret) {
  106.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  107.         goto err2;
  108.     }

  109.     /**************
  110.      设置参数
  111.     ***************/
  112.     /* 设置访问类型: 交错模式 */
  113.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  114.     if (0 > ret) {
  115.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  116.         goto err2;
  117.     }

  118.     /* 设置数据格式: 有符号16位、小端模式 */
  119.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  120.     if (0 > ret) {
  121.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  122.         goto err2;
  123.     }

  124.     /* 设置采样率 */
  125.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
  126.     if (0 > ret) {
  127.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  128.         goto err2;
  129.     }

  130.     /* 设置声道数: 双声道 */
  131.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
  132.     if (0 > ret) {
  133.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  134.         goto err2;
  135.     }

  136.     /* 设置周期大小: period_size */
  137.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  138.     if (0 > ret) {
  139.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  140.         goto err2;
  141.     }

  142.     /* 设置周期数(驱动层环形缓冲区buffer的大小): periods */
  143.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  144.     if (0 > ret) {
  145.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  146.         goto err2;
  147.     }

  148.     /* 使配置生效 */
  149.     ret = snd_pcm_hw_params(pcm, hwparams);
  150.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  151.     if (0 > ret) {
  152.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  153.         goto err1;
  154.     }

  155.     buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小

  156.     /* 注册异步处理函数 */
  157.     ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
  158.     if (0 > ret) {
  159.         fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
  160.         goto err1;
  161.     }

  162.     return 0;

  163. err2:
  164.     snd_pcm_hw_params_free(hwparams);   //释放内存
  165. err1:
  166.     snd_pcm_close(pcm); //关闭pcm设备
  167.     return -1;
  168. }

  169. static int open_wav_file(const char *file)
  170. {
  171.     RIFF_t wav_riff;
  172.     DATA_t wav_data;
  173.     int ret;

  174.     fd = open(file, O_RDONLY);
  175.     if (0 > fd) {
  176.         fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
  177.         return -1;
  178.     }

  179.     /* 读取RIFF chunk */
  180.     ret = read(fd, &wav_riff, sizeof(RIFF_t));
  181.     if (sizeof(RIFF_t) != ret) {
  182.         if (0 > ret)
  183.             perror("read error");
  184.         else
  185.             fprintf(stderr, "check error: %s\n", file);
  186.         close(fd);
  187.         return -1;
  188.     }

  189.     if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
  190.         strncmp("WAVE", wav_riff.Format, 4)) {
  191.         fprintf(stderr, "check error: %s\n", file);
  192.         close(fd);
  193.         return -1;
  194.     }

  195.     /* 读取sub-chunk-fmt */
  196.     ret = read(fd, &wav_fmt, sizeof(FMT_t));
  197.     if (sizeof(FMT_t) != ret) {
  198.         if (0 > ret)
  199.             perror("read error");
  200.         else
  201.             fprintf(stderr, "check error: %s\n", file);
  202.         close(fd);
  203.         return -1;
  204.     }

  205.     if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
  206.         fprintf(stderr, "check error: %s\n", file);
  207.         close(fd);
  208.         return -1;
  209.     }

  210.     /* 打印音频文件的信息 */
  211.     printf("<<<<音频文件格式信息>>>>\n\n");
  212.     printf("  file name:     %s\n", file);
  213.     printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
  214.     printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
  215.     printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
  216.     printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
  217.     printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
  218.     printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
  219.     printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

  220.     /* sub-chunk-data */
  221.     if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
  222.                 SEEK_SET)) {
  223.         perror("lseek error");
  224.         close(fd);
  225.         return -1;
  226.     }

  227.     while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

  228.         /* 找到sub-chunk-data */
  229.         if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
  230.             return 0;

  231.         if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
  232.             perror("lseek error");
  233.             close(fd);
  234.             return -1;
  235.         }
  236.     }

  237.     fprintf(stderr, "check error: %s\n", file);
  238.     return -1;
  239. }

  240. /************************************
  241. main主函数
  242. ************************************/
  243. int main(int argc, char *argv[])
  244. {
  245.     snd_pcm_sframes_t avail;
  246.     int ret;

  247.     if (2 != argc) {
  248.         fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
  249.         exit(EXIT_FAILURE);
  250.     }

  251.     /* 打开WAV音频文件 */
  252.     if (open_wav_file(argv[1]))
  253.         exit(EXIT_FAILURE);

  254.     /* 初始化PCM Playback设备 */
  255.     if (snd_pcm_init())
  256.         goto err1;

  257.     /* 申请读缓冲区 */
  258.     buf = malloc(buf_bytes);
  259.     if (NULL == buf) {
  260.         perror("malloc error");
  261.         goto err2;
  262.     }

  263.     /* 播放:先将环形缓冲区填满数据 */
  264.     avail = snd_pcm_avail_update(pcm);  //获取环形缓冲区中有多少帧数据需要填充
  265.     while (avail >= period_size) {  //我们一次写入一个周期

  266.         memset(buf, 0x00, buf_bytes);   //buf清零
  267.         ret = read(fd, buf, buf_bytes);
  268.         if (0 >= ret)
  269.             goto err3;

  270.         ret = snd_pcm_writei(pcm, buf, period_size);//向环形缓冲区中写入数据
  271.         if (0 > ret) {
  272.             fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  273.             goto err3;
  274.         }
  275.         else if (ret < period_size) {//实际写入的帧数小于指定的帧数
  276.             //此时我们需要调整下音频文件的读位置
  277.             //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
  278.             //frame_bytes表示一帧的字节大小
  279.             if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  280.                 perror("lseek error");
  281.                 goto err3;
  282.             }
  283.         }

  284.         avail = snd_pcm_avail_update(pcm);  //再次获取、更新avail
  285.     }

  286.     for ( ; ; ) {
  287.         /* 主程序可以做一些其它的事,当环形缓冲区有空闲周期需要写入数据时
  288.          * 音频设备驱动程序会向应用程序发送SIGIO信号
  289.          * 接着应用程序跳转到snd_playback_async_callback()函数执行 */
  290.         //do_something();
  291.         sleep(1);
  292.     }

  293. err3:
  294.     free(buf);     //释放内存
  295. err2:
  296.     snd_pcm_close(pcm); //关闭pcm设备
  297. err1:
  298.     close(fd);      //关闭打开的音频文件
  299.     exit(EXIT_FAILURE);
  300. }
复制代码


在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()死循环之前,我们先将环形缓冲区填满,执行的代码与回调函数中的代码相同,这里就不再说明了!
编译示例代码
在Ubuntu系统下执行命令,编译示例代码:
  1. ${CC} -o testApp testApp.c -lasound
复制代码


第二十九章 音频应用编程57657.png
图 29.6.1 编译示例代码
测试应用程序
将上面编译得到的可执行文件拷贝开发板Linux系统/home/root目录下,然后在开发板上测试,大家自己去测!
29.6.2PCM录音示例-异步方式
本小节编写使用异步方式的PCM录音的示例程序,代码笔者已经写好了,如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_capture_async.c。
示例代码 29.6.2 一个简单地PCM录音示例程序(异步方式)
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_capture_async.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM音频采集示例代码--异步方式
  7. 其他 : 无
  8. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  10. ***************************************************************/

  11. #include <stdio.h>
  12. #include <stdlib.h>
  13. #include <errno.h>
  14. #include <string.h>
  15. #include <alsa/asoundlib.h>

  16. /************************************
  17. 宏定义
  18. ************************************/
  19. #define PCM_CAPTURE_DEV    "hw:0,0"

  20. /************************************
  21. static静态全局变量定义
  22. ************************************/
  23. static snd_pcm_t *pcm = NULL;                    //pcm句柄
  24. static unsigned int buf_bytes;                      //应用层缓冲区的大小(字节为单位)
  25. static void *buf = NULL;                          //指向应用层缓冲区的指针
  26. static int fd = -1;                                 //输出文件的文件描述符
  27. static snd_pcm_uframes_t period_size = 1024;         //周期大小(单位: 帧)
  28. static unsigned int periods = 16;                     //周期数(驱动层环形缓冲区的大小)
  29. static unsigned int rate = 44100;                     //采样率

  30. /************************************
  31. static静态函数
  32. ************************************/
  33. static void snd_capture_async_callback(snd_async_handler_t *handler)
  34. {
  35.     snd_pcm_t *handle = snd_async_handler_get_pcm(handler);
  36.     snd_pcm_sframes_t avail;
  37.     int ret;

  38.     avail = snd_pcm_avail_update(handle);   //检查有多少帧数据可读
  39.     while (avail >= period_size) {  //每次读取一个周期

  40.         //memset(buf, 0x00, buf_bytes);   //buf清零
  41.         ret = snd_pcm_readi(handle, buf, period_size);//读取PCM数据 一个周期
  42.         if (0 > ret) {
  43.             fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
  44.             goto out;
  45.         }

  46.         // snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数
  47.         ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
  48.         if (0 >= ret)
  49.             goto out;

  50.         avail = snd_pcm_avail_update(handle);   //再次读取、更新avail
  51.     }

  52.     return;
  53. out:
  54.     snd_pcm_close(handle); //关闭pcm设备
  55.     free(buf);
  56.     close(fd);      //关闭打开的音频文件
  57.     exit(EXIT_FAILURE); //退出程序
  58. }

  59. static int snd_pcm_init(void)
  60. {
  61.     snd_pcm_hw_params_t *hwparams = NULL;
  62.     snd_async_handler_t *async_handler = NULL;
  63.     int ret;

  64.     /* 打开PCM设备 */
  65.     ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
  66.     if (0 > ret) {
  67.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  68.                     PCM_CAPTURE_DEV, snd_strerror(ret));
  69.         return -1;
  70.     }

  71.     /* 实例化hwparams对象 */
  72.     snd_pcm_hw_params_malloc(&hwparams);

  73.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  74.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  75.     if (0 > ret) {
  76.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  77.         goto err2;
  78.     }

  79.     /**************
  80.      设置参数
  81.     ***************/
  82.     /* 设置访问类型: 交错模式 */
  83.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  84.     if (0 > ret) {
  85.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  86.         goto err2;
  87.     }

  88.     /* 设置数据格式: 有符号16位、小端模式 */
  89.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  90.     if (0 > ret) {
  91.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  92.         goto err2;
  93.     }

  94.     /* 设置采样率 */
  95.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
  96.     if (0 > ret) {
  97.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  98.         goto err2;
  99.     }

  100.     /* 设置声道数: 双声道 */
  101.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
  102.     if (0 > ret) {
  103.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  104.         goto err2;
  105.     }

  106.     /* 设置周期大小: period_size */
  107.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  108.     if (0 > ret) {
  109.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  110.         goto err2;
  111.     }

  112.     /* 设置周期数(buffer的大小): periods */
  113.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  114.     if (0 > ret) {
  115.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  116.         goto err2;
  117.     }

  118.     /* 使配置生效 */
  119.     ret = snd_pcm_hw_params(pcm, hwparams);
  120.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  121.     if (0 > ret) {
  122.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  123.         goto err1;
  124.     }

  125.     /* 注册异步处理函数 */
  126.     ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_capture_async_callback, NULL);
  127.     if (0 > ret) {
  128.         fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
  129.         goto err1;
  130.     }

  131.     return 0;

  132. err2:
  133.     snd_pcm_hw_params_free(hwparams);   //释放内存
  134. err1:
  135.     snd_pcm_close(pcm); //关闭pcm设备
  136.     return -1;
  137. }

  138. /************************************
  139. main主函数
  140. ************************************/
  141. int main(int argc, char *argv[])
  142. {
  143.     int ret;

  144.     if (2 != argc) {
  145.         fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
  146.         exit(EXIT_FAILURE);
  147.     }

  148.     /* 初始化PCM Capture设备 */
  149.     if (snd_pcm_init())
  150.         exit(EXIT_FAILURE);

  151.     /* 申请读缓冲区 */
  152.     buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
  153.     buf = malloc(buf_bytes);
  154.     if (NULL == buf) {
  155.         perror("malloc error");
  156.         goto err1;
  157.     }

  158.     /* 打开一个新建文件 */
  159.     fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
  160.     if (0 > fd) {
  161.         fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
  162.         goto err2;
  163.     }

  164.     /* 录音 */
  165.     ret = snd_pcm_start(pcm);       //开始录音
  166.     if (0 > ret) {
  167.         fprintf(stderr, "snd_pcm_start error: %s\n", snd_strerror(ret));
  168.         goto err3;
  169.     }

  170.     for ( ; ; ) {
  171.         /* 主程序可以做一些其它的事,当环形缓冲区有数据可读时
  172.          * 音频设备驱动程序会向应用程序发送SIGIO信号
  173.          * 接着应用程序跳转到snd_capture_async_callback()函数执行、读取数据 */
  174.         //do_something();
  175.         sleep(1);
  176.     }

  177. err3:
  178.     close(fd);  //关闭文件
  179. err2:
  180.     free(buf);     //释放内存
  181. err1:
  182.     snd_pcm_close(pcm); //关闭pcm设备
  183.     exit(EXIT_FAILURE);
  184. }
复制代码


这份代码基于示例代码 29.5.2改写,使用异步方式读取录制的音频数据。
代码不再解释了,值得注意的是,在main()函数中我们调用了snd_pcm_start()函数,这个函数前面没给大家介绍过,该函数的作用其实如它命名那般,用于启动PCM设备,譬如在录音情况下,调用该函数开始录音;在播放情况下,调用该函数开始播放。
前面的几个示例代码中,为啥没有调用该函数呢?这个问题我们先留着、稍后再给大家介绍!
编译示例代码
执行命令编译示例代码:
  1. ${CC} -o testApp testApp.c -lasound
复制代码


测试应用程序
将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,然后进行测试,测试方法与示例代码 29.5.2对应的测试程序相同,这里不再重述,大家自己去测!
29.7使用poll()函数
上小节我们使用了异步I/O方式读写PCM设备,本小节我们来学习如何使用poll I/O多路复用来实现读写数据。
29.7.1使用poll I/O多路复用实现读写
I/O多路复用是一种高级I/O,在第一篇14.2小节给大家进行了详细地介绍,可通过select()或poll()函数来实现I/O多路复用,本小节我们使用poll()函数来实现I/O多路复用,接下来将向大家介绍!
获取计数:snd_pcm_poll_descriptors_count
该函数用于获取PCM句柄的轮询描述符计数,其函数原型如下所示:
  1. int snd_pcm_poll_descriptors_count(snd_pcm_t *pcm);
复制代码


调用该函数返回PCM句柄的轮询描述符计数。
分配struct pollfd对象
为每一个轮询描述符分配一个struct pollfd对象,譬如:
  1. struct pollfd *pfds = NULL;
  2. int count;

  3. /* 获取PCM句柄的轮询描述符计数 */
  4. count = snd_pcm_poll_descriptors_count(pcm);
  5. if (0 >= count) {
  6.         fprintf(stderr, "Invalid poll descriptors count\n");
  7.         return -1;
  8. }

  9. /* 分配内存 */
  10. pfds = calloc(count, sizeof(struct pollfd));
  11. if (NULL == pfds) {
  12.         perror("calloc error");
  13.         return -1;
复制代码


}
填充struct pollfd:snd_pcm_poll_descriptors
接下来调用snd_pcm_poll_descriptors()函数对struct pollfd对象进行填充(初始化),其函数原型如下所示:
  1. int snd_pcm_poll_descriptors(
  2.         snd_pcm_t *pcm,
  3.         struct pollfd *pfds,
  4.         unsigned int space
  5. );
复制代码


参数space表示pfds数组中的元素个数。
/
  1. * 填充pfds */
  2. ret = snd_pcm_poll_descriptors(pcm, pfds, count);
  3. if (0 > ret)
  4.         return -1;
  5. poll+snd_pcm_poll_descriptors_revents
复制代码

一切准备完成之后,就可以调用poll()函数来监视PCM设备是否有数据可读或可写,当有数据可读或可写时,poll()函数返回,此时我们可以调用snd_pcm_poll_descriptors_revents()函数获取文件描述符中返回的事件类型,并与poll的events标志进行比较,以确定是否可读或可写,snd_pcm_poll_descriptors_revents()函数原型如下所示:
  1. int snd_pcm_poll_descriptors_revents(
  2. snd_pcm_t *pcm,
  3. struct pollfd *pfds,
  4. unsigned int nfds,
  5. unsigned short *revents
  6. )
复制代码


参数nfds表示pfds数组中元素的个数,调用该函数获取文件描述符中返回的事件,通过参数revents返回出来;注意,不要直接读取struct pollfd对象中的revents成员变量,因为snd_pcm_poll_descriptors_revents()函数会对poll()系统调用返回的revents掩码进行“分解”以纠正语义(POLLIN = 读取,POLLOUT = 写入)。
使用示例:
  1. for ( ; ; ) {

  2.         ret = poll(pfds, count, -1);//调用poll
  3.         if (0 > ret) {
  4.                 perror("poll error");
  5.                 return -1;
  6.         }

  7.         ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
  8.         if (0 > ret)
  9.                 return -1;
  10.         if (revents & POLLERR)        //发生I/O错误
  11.                 return -1;
  12.         if (revents & POLLIN) {//表示可读取数据
  13.                 // 从PCM设备读取数据
  14.         }
  15.         if (revents & POLLOUT) {//表示可写入数据
  16.                 // 将数据写入PCM设备
  17.         }
  18. }
复制代码


29.7.2PCM播放示例代码
对示例代码 29.5.1进行修改,使用poll I/O多路复用,示例代码如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_playback_poll.c。
示例代码 29.7.1 PCM播放示例程序—poll
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_playback_poll.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM播放示例代码--使用I/O多路复用(poll)写数据
  7. 其他 : 无
  8. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  10. ***************************************************************/

  11. #include <stdio.h>
  12. #include <stdlib.h>
  13. #include <errno.h>
  14. #include <string.h>
  15. #include <poll.h>
  16. #include <alsa/asoundlib.h>

  17. /************************************
  18. 宏定义
  19. ************************************/
  20. #define PCM_PLAYBACK_DEV    "hw:0,0"

  21. /************************************
  22. WAV音频文件解析相关数据结构申明
  23. ************************************/
  24. typedef struct WAV_RIFF {
  25.     char ChunkID[4];                /* "RIFF" */
  26.     u_int32_t ChunkSize;            /* 从下一个地址开始到文件末尾的总字节数 */
  27.     char Format[4];                 /* "WAVE" */
  28. } __attribute__ ((packed)) RIFF_t;

  29. typedef struct WAV_FMT {
  30.     char Subchunk1ID[4];            /* "fmt " */
  31.     u_int32_t Subchunk1Size;        /* 16 for PCM */
  32.     u_int16_t AudioFormat;          /* PCM = 1*/
  33.     u_int16_t NumChannels;          /* Mono = 1, Stereo = 2, etc. */
  34.     u_int32_t SampleRate;           /* 8000, 44100, etc. */
  35.     u_int32_t ByteRate;             /* = SampleRate * NumChannels * BitsPerSample/8 */
  36.     u_int16_t BlockAlign;           /* = NumChannels * BitsPerSample/8 */
  37.     u_int16_t BitsPerSample;        /* 8bits, 16bits, etc. */
  38. } __attribute__ ((packed)) FMT_t;
  39. static FMT_t wav_fmt;

  40. typedef struct WAV_DATA {
  41.     char Subchunk2ID[4];            /* "data" */
  42.     u_int32_t Subchunk2Size;        /* data size */
  43. } __attribute__ ((packed)) DATA_t;

  44. /************************************
  45. static静态全局变量定义
  46. ************************************/
  47. static snd_pcm_t *pcm = NULL;               //pcm句柄
  48. static unsigned int buf_bytes;                  //应用程序缓冲区的大小(字节为单位)
  49. static void *buf = NULL;                    //指向应用程序缓冲区的指针
  50. static int fd = -1;                             //指向WAV音频文件的文件描述符
  51. static snd_pcm_uframes_t period_size = 1024;    //周期大小(单位: 帧)
  52. static unsigned int periods = 16;               //周期数(设备驱动层buffer的大小)

  53. static struct pollfd *pfds = NULL;
  54. static int count;

  55. static int snd_pcm_init(void)
  56. {
  57.     snd_pcm_hw_params_t *hwparams = NULL;
  58.     int ret;

  59.     /* 打开PCM设备 */
  60.     ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
  61.     if (0 > ret) {
  62.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  63.                     PCM_PLAYBACK_DEV, snd_strerror(ret));
  64.         return -1;
  65.     }

  66.     /* 实例化hwparams对象 */
  67.     snd_pcm_hw_params_malloc(&hwparams);

  68.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  69.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  70.     if (0 > ret) {
  71.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  72.         goto err2;
  73.     }

  74.     /**************
  75.      设置参数
  76.     ***************/
  77.     /* 设置访问类型: 交错模式 */
  78.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  79.     if (0 > ret) {
  80.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  81.         goto err2;
  82.     }

  83.     /* 设置数据格式: 有符号16位、小端模式 */
  84.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  85.     if (0 > ret) {
  86.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  87.         goto err2;
  88.     }

  89.     /* 设置采样率 */
  90.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
  91.     if (0 > ret) {
  92.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  93.         goto err2;
  94.     }

  95.     /* 设置声道数: 双声道 */
  96.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
  97.     if (0 > ret) {
  98.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  99.         goto err2;
  100.     }

  101.     /* 设置周期大小: period_size */
  102.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  103.     if (0 > ret) {
  104.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  105.         goto err2;
  106.     }

  107.     /* 设置周期数(驱动层buffer的大小): periods */
  108.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  109.     if (0 > ret) {
  110.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  111.         goto err2;
  112.     }

  113.     /* 使配置生效 */
  114.     ret = snd_pcm_hw_params(pcm, hwparams);
  115.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  116.     if (0 > ret) {
  117.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  118.         goto err1;
  119.     }

  120.     buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小
  121.     return 0;

  122. err2:
  123.     snd_pcm_hw_params_free(hwparams);   //释放内存
  124. err1:
  125.     snd_pcm_close(pcm); //关闭pcm设备
  126.     return -1;
  127. }

  128. static int open_wav_file(const char *file)
  129. {
  130.     RIFF_t wav_riff;
  131.     DATA_t wav_data;
  132.     int ret;

  133.     fd = open(file, O_RDONLY);
  134.     if (0 > fd) {
  135.         fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
  136.         return -1;
  137.     }

  138.     /* 读取RIFF chunk */
  139.     ret = read(fd, &wav_riff, sizeof(RIFF_t));
  140.     if (sizeof(RIFF_t) != ret) {
  141.         if (0 > ret)
  142.             perror("read error");
  143.         else
  144.             fprintf(stderr, "check error: %s\n", file);
  145.         close(fd);
  146.         return -1;
  147.     }

  148.     if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
  149.         strncmp("WAVE", wav_riff.Format, 4)) {
  150.         fprintf(stderr, "check error: %s\n", file);
  151.         close(fd);
  152.         return -1;
  153.     }

  154.     /* 读取sub-chunk-fmt */
  155.     ret = read(fd, &wav_fmt, sizeof(FMT_t));
  156.     if (sizeof(FMT_t) != ret) {
  157.         if (0 > ret)
  158.             perror("read error");
  159.         else
  160.             fprintf(stderr, "check error: %s\n", file);
  161.         close(fd);
  162.         return -1;
  163.     }

  164.     if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
  165.         fprintf(stderr, "check error: %s\n", file);
  166.         close(fd);
  167.         return -1;
  168.     }

  169.     /* 打印音频文件的信息 */
  170.     printf("<<<<音频文件格式信息>>>>\n\n");
  171.     printf("  file name:     %s\n", file);
  172.     printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
  173.     printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
  174.     printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
  175.     printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
  176.     printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
  177.     printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
  178.     printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

  179.     /* sub-chunk-data */
  180.     if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
  181.                 SEEK_SET)) {
  182.         perror("lseek error");
  183.         close(fd);
  184.         return -1;
  185.     }

  186.     while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

  187.         /* 找到sub-chunk-data */
  188.         if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
  189.             return 0;

  190.         if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
  191.             perror("lseek error");
  192.             close(fd);
  193.             return -1;
  194.         }
  195.     }

  196.     fprintf(stderr, "check error: %s\n", file);
  197.     return -1;
  198. }

  199. static int snd_pcm_poll_init(void)
  200. {
  201.     int ret;

  202.     /* 获取PCM句柄的轮询描述符计数 */
  203.     count = snd_pcm_poll_descriptors_count(pcm);
  204.     if (0 >= count) {
  205.         fprintf(stderr, "Invalid poll descriptors count\n");
  206.         return -1;
  207.     }

  208.     /* 分配内存 */
  209.     pfds = calloc(count, sizeof(struct pollfd));
  210.     if (NULL == pfds) {
  211.         perror("calloc error");
  212.         return -1;
  213.     }

  214.     /* 填充pfds */
  215.     ret = snd_pcm_poll_descriptors(pcm, pfds, count);
  216.     if (0 > ret)
  217.         return -1;

  218.     return 0;
  219. }

  220. /************************************
  221. main主函数
  222. ************************************/
  223. int main(int argc, char *argv[])
  224. {
  225.     unsigned short revents;
  226.     snd_pcm_sframes_t avail;
  227.     int ret;

  228.     if (2 != argc) {
  229.         fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
  230.         exit(EXIT_FAILURE);
  231.     }

  232.     /* 打开WAV音频文件 */
  233.     if (open_wav_file(argv[1]))
  234.         exit(EXIT_FAILURE);

  235.     /* 初始化PCM Playback设备 */
  236.     if (snd_pcm_init())
  237.         goto err1;

  238.     /* 申请读缓冲区 */
  239.     buf = malloc(buf_bytes);
  240.     if (NULL == buf) {
  241.         perror("malloc error");
  242.         goto err2;
  243.     }

  244.     /* I/O多路复用poll初始化 */
  245.     if (snd_pcm_poll_init())
  246.         goto err3;

  247.     for (;;) {

  248.         ret = poll(pfds, count, -1);//调用poll
  249.         if (0 > ret) {
  250.             perror("poll error");
  251.             goto err3;
  252.         }

  253.         ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
  254.         if (0 > ret)
  255.             goto err3;
  256.         if (revents & POLLERR)
  257.             goto err3;
  258.         if (revents & POLLOUT) {    //可写数据
  259.             avail = snd_pcm_avail_update(pcm);//获取环形缓冲区中有多少帧数据需要填充
  260.             while (avail >= period_size) {  //我们一次写入一个周期

  261.                 memset(buf, 0x00, buf_bytes);   //buf清零
  262.                 ret = read(fd, buf, buf_bytes);
  263.                 if (0 >= ret)
  264.                     goto err3;

  265.                 ret = snd_pcm_writei(pcm, buf, period_size);
  266.                 if (0 > ret) {
  267.                     fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  268.                     goto err3;
  269.                 }
  270.                 else if (ret < period_size) {
  271.                     if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  272.                         perror("lseek error");
  273.                         goto err3;
  274.                     }
  275.                 }

  276.                 avail = snd_pcm_avail_update(pcm);   //再次获取、更新avail
  277.             }
  278.         }
  279.     }

  280. err3:
  281.     free(buf);     //释放内存
  282. err2:
  283.     snd_pcm_close(pcm); //关闭pcm设备
  284. err1:
  285.     close(fd);      //关闭打开的音频文件
  286.     exit(EXIT_FAILURE);
  287. }
复制代码


29.7.3PCM录音示例代码
对示例代码 29.5.2进行修改,使用poll I/O多路复用,示例代码如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_capture_poll.c。
示例代码 29.7.2 PCM录音示例程序—poll
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_capture_poll.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM音频采集示例代码--使用I/O多路复用(poll)读数据
  7. 其他 : 无
  8. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  10. ***************************************************************/

  11. #include <stdio.h>
  12. #include <stdlib.h>
  13. #include <errno.h>
  14. #include <string.h>
  15. #include <poll.h>
  16. #include <alsa/asoundlib.h>

  17. /************************************
  18. 宏定义
  19. ************************************/
  20. #define PCM_CAPTURE_DEV    "hw:0,0"

  21. /************************************
  22. static静态全局变量定义
  23. ************************************/
  24. static snd_pcm_t *pcm = NULL;               //pcm句柄
  25. static snd_pcm_uframes_t period_size = 1024;    //周期大小(单位: 帧)
  26. static unsigned int periods = 16;               //周期数(buffer的大小)
  27. static unsigned int rate = 44100;               //采样率

  28. static struct pollfd *pfds = NULL;
  29. static int count;

  30. static int snd_pcm_init(void)
  31. {
  32.     snd_pcm_hw_params_t *hwparams = NULL;
  33.     int ret;

  34.     /* 打开PCM设备 */
  35.     ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
  36.     if (0 > ret) {
  37.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  38.                     PCM_CAPTURE_DEV, snd_strerror(ret));
  39.         return -1;
  40.     }

  41.     /* 实例化hwparams对象 */
  42.     snd_pcm_hw_params_malloc(&hwparams);

  43.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  44.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  45.     if (0 > ret) {
  46.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  47.         goto err2;
  48.     }

  49.     /**************
  50.      设置参数
  51.     ***************/
  52.     /* 设置访问类型: 交错模式 */
  53.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  54.     if (0 > ret) {
  55.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  56.         goto err2;
  57.     }

  58.     /* 设置数据格式: 有符号16位、小端模式 */
  59.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  60.     if (0 > ret) {
  61.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  62.         goto err2;
  63.     }

  64.     /* 设置采样率 */
  65.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
  66.     if (0 > ret) {
  67.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  68.         goto err2;
  69.     }

  70.     /* 设置声道数: 双声道 */
  71.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
  72.     if (0 > ret) {
  73.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  74.         goto err2;
  75.     }

  76.     /* 设置周期大小: period_size */
  77.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  78.     if (0 > ret) {
  79.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  80.         goto err2;
  81.     }

  82.     /* 设置周期数(buffer的大小): periods */
  83.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  84.     if (0 > ret) {
  85.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  86.         goto err2;
  87.     }

  88.     /* 使配置生效 */
  89.     ret = snd_pcm_hw_params(pcm, hwparams);
  90.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  91.     if (0 > ret) {
  92.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  93.         goto err1;
  94.     }

  95.     return 0;

  96. err2:
  97.     snd_pcm_hw_params_free(hwparams);   //释放内存
  98. err1:
  99.     snd_pcm_close(pcm); //关闭pcm设备
  100.     return -1;
  101. }

  102. static int snd_pcm_poll_init(void)
  103. {
  104.     int ret;

  105.     /* 获取PCM句柄的轮询描述符计数 */
  106.     count = snd_pcm_poll_descriptors_count(pcm);
  107.     if (0 >= count) {
  108.         fprintf(stderr, "Invalid poll descriptors count\n");
  109.         return -1;
  110.     }

  111.     /* 分配内存 */
  112.     pfds = calloc(count, sizeof(struct pollfd));
  113.     if (NULL == pfds) {
  114.         perror("calloc error");
  115.         return -1;
  116.     }

  117.     /* 填充pfds */
  118.     ret = snd_pcm_poll_descriptors(pcm, pfds, count);
  119.     if (0 > ret)
  120.         return -1;

  121.     return 0;
  122. }

  123. /************************************
  124. main主函数
  125. ************************************/
  126. int main(int argc, char *argv[])
  127. {
  128.     unsigned char *buf = NULL;
  129.     unsigned int buf_bytes;
  130.     unsigned short revents;
  131.     snd_pcm_sframes_t avail;
  132.     int fd = -1;
  133.     int ret;

  134.     if (2 != argc) {
  135.         fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
  136.         exit(EXIT_FAILURE);
  137.     }

  138.     /* 初始化PCM Capture设备 */
  139.     if (snd_pcm_init())
  140.         exit(EXIT_FAILURE);

  141.     /* 申请读缓冲区 */
  142.     buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
  143.     buf = malloc(buf_bytes);
  144.     if (NULL == buf) {
  145.         perror("malloc error");
  146.         goto err1;
  147.     }

  148.     /* 打开一个新建文件 */
  149.     fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
  150.     if (0 > fd) {
  151.         fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
  152.         goto err2;
  153.     }

  154.     /* I/O多路复用poll初始化 */
  155.     if (snd_pcm_poll_init())
  156.         goto err3;

  157.     /* 开始录音 */
  158.     ret = snd_pcm_start(pcm);
  159.     if (0 > ret) {
  160.         fprintf(stderr, "snd_pcm_start error: %s\n", snd_strerror(ret));
  161.         goto err3;
  162.     }

  163.     for (;;) {

  164.         ret = poll(pfds, count, -1);//调用poll
  165.         if (0 > ret) {
  166.             perror("poll error");
  167.             goto err3;
  168.         }

  169.         ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
  170.         if (0 > ret)
  171.             goto err3;
  172.         if (revents & POLLERR)
  173.             goto err3;
  174.         if (revents & POLLIN) { //可读数据
  175.             avail = snd_pcm_avail_update(pcm);   //检查有多少帧数据可读
  176.             while (avail >= period_size) {  //每次读取一个周期

  177.                 ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期
  178.                 if (0 > ret) {
  179.                     fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
  180.                     goto err3;
  181.                 }

  182.                 ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
  183.                 if (0 >= ret)
  184.                     goto err3;

  185.                 avail = snd_pcm_avail_update(pcm);   //再次读取、更新avail
  186.             }
  187.         }
  188.     }

  189. err3:
  190.     close(fd);  //关闭文件
  191. err2:
  192.     free(buf);     //释放内存
  193. err1:
  194.     snd_pcm_close(pcm); //关闭pcm设备
  195.     exit(EXIT_FAILURE);
  196. }
复制代码


29.8PCM设备的状态
本小节向大家介绍PCM设备的状态有哪些,alsa-lib提供了函数snd_pcm_state()用于获取PCM设备当前的状态,其函数原型如下所示:
  1. snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm);
复制代码


可以看到它的返回值是一个snd_pcm_state_t类型的变量,snd_pcm_state_t其实是一个枚举类型,描述了PCM设备包含的所有状态,如下所示:
  1. enum snd_pcm_state_t {
  2.         SND_PCM_STATE_OPEN = 0,
  3.         SND_PCM_STATE_SETUP,
  4.         SND_PCM_STATE_PREPARED,
  5.         SND_PCM_STATE_RUNNING,
  6.         SND_PCM_STATE_XRUN,
  7.         SND_PCM_STATE_DRAINING,
  8.         SND_PCM_STATE_PAUSED,
  9.         SND_PCM_STATE_SUSPENDED,
  10.         SND_PCM_STATE_DISCONNECTED,
  11.         SND_PCM_STATE_LAST = SND_PCM_STATE_DISCONNECTED,
  12.         SND_PCM_STATE_PRIVATE1 = 1024
  13. }
  14. SND_PCM_STATE_OPEN
复制代码


该状态表示PCM设备处于打开状态,譬如当调用snd_pcm_open()后,PCM设备就处于该状态。
  1. SND_PCM_STATE_SETUP
复制代码


alsa-lib文档中的解释为“Setup installed”!该状态表示设备已经初始化完成了,参数已经配置好了。
  1. SND_PCM_STATE_PREPARED
复制代码


该状态表示设备已经准备好了,可以开始了“Ready to start”!譬如可以开始播放了、可以开始录音了。
前面提到了这个状态,当应用程序调用snd_pcm_hw_params()函数之后,设备就处于SND_PCM_STATE_PREPARED状态了。应用程序中,可以调用snd_pcm_prepare()函数使设备处于SND_PCM_STATE_PREPARED状态,该函数原型如下所示:
  1. 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状态。
  1. 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进行音频采集。
所以这就是为什么示例代码 29.5.1、示例代码 29.5.2、示例代码 29.6.3这几个示例中并没有调用snd_pcm_start()函数的原因。
当设备处于运行状态时,应用程序可调用snd_pcm_drop()或snd_pcm_drain()函数使设备停止运行,譬如停止播放、停止音频采集;它们的函数原型如下所示:
  1. int snd_pcm_drain(snd_pcm_t *pcm);
  2. 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状态。
  1. SND_PCM_STATE_XRUN
复制代码


当发生XRUN时,设备会处于SND_PCM_STATE_XRUN状态,XRUN前面给大家解释过了,这里不再重述!当处于SND_PCM_STATE_XRUN状态时,应用程序可以调用snd_pcm_prepare()使设备恢复,使其回到SND_PCM_STATE_PREPARED状态。
  1. SND_PCM_STATE_DRAINING
复制代码


这个状态笔者没弄清楚,alsa-lib文档中的解释为“Draining: running (playback) or stopped (capture)”。
SND_PCM_STATE_PAUSED
pause就是暂停的意思,所以该状态表示设备处于暂停状态。譬如当设备正在运行时(也就是处于SND_PCM_STATE_RUNNING状态),应用程序调用snd_pcm_pause()函数可让设备暂停,其函数原型如下所示:
  1. 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()函数来判断设备是否支持暂停,其函数原型如下所示:
  1. int snd_pcm_hw_params_can_pause(const snd_pcm_hw_params_t *params);
复制代码


函数返回1表示硬件支持暂停;返回0表示硬件不支持暂停。
  1. SND_PCM_STATE_SUSPENDED
复制代码


该状态表示硬件已经挂起suspended,如果硬件发生了挂起,应用程序可以调用snd_pcm_resume()函数从挂起中恢复,并确保不会丢失样本数据(精细恢复)。snd_pcm_resume()函数原型如下所示:
  1. 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表示不支持。
  1. 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种状态之间的转换关系,如下图所示:
第二十九章 音频应用编程86132.png
图 29.8.1 PCM设备状态之间的转换关系示意图
笔者尽力了!这图画的还是有点乱,不过没关系,状态转换还是描述清楚了。其实这个状态之间的转换关系不难理解,哪种状态能转哪种状态、哪种状态不能转哪种状态,这个还是很容易理解的。这里笔者就不再多说了。
29.8.1PCM播放示例代码-加入状态控制
通过上面的介绍,我们已经知道了PCM设备不同状态之间转换,譬如播放音乐时,如何暂停、如何停止、又如何恢复。本小节我们来编写一个PCM播放程序,在示例代码 29.6.3的基础上,加入对播放过程的控制,譬如用户按下空格键可以暂停播放、再次按下空格则恢复播放。
示例代码笔者已经写好,如下所示。
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_playback_ctl.c。
示例代码 29.8.1 PCM播放示例程序(加入状态控制)
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_playback_ctl.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM播放示例代码--使用异步方式、用户可通过按键
  7.         对播放过程进行控制。
  8. 其他 : 无
  9. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  10. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  11. ***************************************************************/

  12. #include <stdio.h>
  13. #include <stdlib.h>
  14. #include <errno.h>
  15. #include <string.h>
  16. #include <termios.h>
  17. #include <signal.h>
  18. #include <alsa/asoundlib.h>

  19. /************************************
  20. 宏定义
  21. ************************************/
  22. #define PCM_PLAYBACK_DEV    "hw:0,0"

  23. /************************************
  24. WAV音频文件解析相关数据结构申明
  25. ************************************/
  26. typedef struct WAV_RIFF {
  27.     char ChunkID[4];                    /* "RIFF" */
  28.     u_int32_t ChunkSize;                 /* 从下一个地址开始到文件末尾的总字节数 */
  29.     char Format[4];                     /* "WAVE" */
  30. } __attribute__ ((packed)) RIFF_t;

  31. typedef struct WAV_FMT {
  32.     char Subchunk1ID[4];                    /* "fmt " */
  33.     u_int32_t Subchunk1Size;                     /* 16 for PCM */
  34.     u_int16_t AudioFormat;                  /* PCM = 1*/
  35.     u_int16_t NumChannels;                  /* Mono = 1, Stereo = 2, etc. */
  36.     u_int32_t SampleRate;                   /* 8000, 44100, etc. */
  37.     u_int32_t ByteRate;                     /* = SampleRate * NumChannels * BitsPerSample/8 */
  38.     u_int16_t BlockAlign;                   /* = NumChannels * BitsPerSample/8 */
  39.     u_int16_t BitsPerSample;                /* 8bits, 16bits, etc. */
  40. } __attribute__ ((packed)) FMT_t;
  41. static FMT_t wav_fmt;

  42. typedef struct WAV_DATA {
  43.     char Subchunk2ID[4];                    /* "data" */
  44.     u_int32_t Subchunk2Size;                /* data size */
  45. } __attribute__ ((packed)) DATA_t;

  46. /************************************
  47. static静态全局变量定义
  48. ************************************/
  49. static snd_pcm_t *pcm = NULL;                       //pcm句柄
  50. static unsigned int buf_bytes;                          //应用程序缓冲区的大小(字节为单位)
  51. static void *buf = NULL;                            //指向应用程序缓冲区的指针
  52. static int fd = -1;                                     //指向WAV音频文件的文件描述符
  53. static snd_pcm_uframes_t period_size = 1024;                 //周期大小(单位: 帧)
  54. static unsigned int periods = 16;                       //周期数(设备驱动层buffer的大小)
  55. static struct termios old_cfg;                          //用于保存终端当前的配置参数

  56. /************************************
  57. static静态函数
  58. ************************************/
  59. static void snd_playback_async_callback(snd_async_handler_t *handler)
  60. {
  61.     snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄
  62.     snd_pcm_sframes_t avail;
  63.     int ret;

  64.     avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充
  65.     while (avail >= period_size) {  //我们一次写入一个周期

  66.         memset(buf, 0x00, buf_bytes);   //buf清零
  67.         ret = read(fd, buf, buf_bytes);
  68.         if (0 >= ret)
  69.             goto out;

  70.         ret = snd_pcm_writei(handle, buf, period_size);
  71.         if (0 > ret) {
  72.             fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  73.             goto out;
  74.         }
  75.         else if (ret < period_size) {//实际写入的帧数小于指定的帧数
  76.             //此时我们需要调整下音频文件的读位置
  77.             //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
  78.             //frame_bytes表示一帧的字节大小
  79.             if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  80.                 perror("lseek error");
  81.                 goto out;
  82.             }
  83.         }

  84.         avail = snd_pcm_avail_update(handle);   //再次获取、更新avail
  85.     }

  86.     return;
  87. out:
  88.     snd_pcm_drain(pcm); //停止PCM
  89.     snd_pcm_close(handle); //关闭pcm设备
  90.     tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
  91.     free(buf);
  92.     close(fd);      //关闭打开的音频文件
  93.     exit(EXIT_FAILURE); //退出程序
  94. }

  95. static int snd_pcm_init(void)
  96. {
  97.     snd_pcm_hw_params_t *hwparams = NULL;
  98.     snd_async_handler_t *async_handler = NULL;
  99.     int ret;

  100.     /* 打开PCM设备 */
  101.     ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
  102.     if (0 > ret) {
  103.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  104.                     PCM_PLAYBACK_DEV, snd_strerror(ret));
  105.         return -1;
  106.     }

  107.     /* 实例化hwparams对象 */
  108.     snd_pcm_hw_params_malloc(&hwparams);

  109.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  110.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  111.     if (0 > ret) {
  112.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  113.         goto err2;
  114.     }

  115.     /**************
  116.      设置参数
  117.     ***************/
  118.     /* 设置访问类型: 交错模式 */
  119.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  120.     if (0 > ret) {
  121.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  122.         goto err2;
  123.     }

  124.     /* 设置数据格式: 有符号16位、小端模式 */
  125.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  126.     if (0 > ret) {
  127.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  128.         goto err2;
  129.     }

  130.     /* 设置采样率 */
  131.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
  132.     if (0 > ret) {
  133.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  134.         goto err2;
  135.     }

  136.     /* 设置声道数: 双声道 */
  137.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
  138.     if (0 > ret) {
  139.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  140.         goto err2;
  141.     }

  142.     /* 设置周期大小: period_size */
  143.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  144.     if (0 > ret) {
  145.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  146.         goto err2;
  147.     }

  148.     /* 设置周期数(驱动层环形缓冲区buffer的大小): periods */
  149.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  150.     if (0 > ret) {
  151.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  152.         goto err2;
  153.     }

  154.     /* 使配置生效 */
  155.     ret = snd_pcm_hw_params(pcm, hwparams);
  156.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  157.     if (0 > ret) {
  158.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  159.         goto err1;
  160.     }

  161.     buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小

  162.     /* 注册异步处理函数 */
  163.     ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
  164.     if (0 > ret) {
  165.         fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
  166.         goto err1;
  167.     }

  168.     return 0;

  169. err2:
  170.     snd_pcm_hw_params_free(hwparams);   //释放内存
  171. err1:
  172.     snd_pcm_close(pcm); //关闭pcm设备
  173.     return -1;
  174. }

  175. static int open_wav_file(const char *file)
  176. {
  177.     RIFF_t wav_riff;
  178.     DATA_t wav_data;
  179.     int ret;

  180.     fd = open(file, O_RDONLY);
  181.     if (0 > fd) {
  182.         fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
  183.         return -1;
  184.     }

  185.     /* 读取RIFF chunk */
  186.     ret = read(fd, &wav_riff, sizeof(RIFF_t));
  187.     if (sizeof(RIFF_t) != ret) {
  188.         if (0 > ret)
  189.             perror("read error");
  190.         else
  191.             fprintf(stderr, "check error: %s\n", file);
  192.         close(fd);
  193.         return -1;
  194.     }

  195.     if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
  196.         strncmp("WAVE", wav_riff.Format, 4)) {
  197.         fprintf(stderr, "check error: %s\n", file);
  198.         close(fd);
  199.         return -1;
  200.     }

  201.     /* 读取sub-chunk-fmt */
  202.     ret = read(fd, &wav_fmt, sizeof(FMT_t));
  203.     if (sizeof(FMT_t) != ret) {
  204.         if (0 > ret)
  205.             perror("read error");
  206.         else
  207.             fprintf(stderr, "check error: %s\n", file);
  208.         close(fd);
  209.         return -1;
  210.     }

  211.     if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
  212.         fprintf(stderr, "check error: %s\n", file);
  213.         close(fd);
  214.         return -1;
  215.     }

  216.     /* 打印音频文件的信息 */
  217.     printf("<<<<音频文件格式信息>>>>\n\n");
  218.     printf("  file name:     %s\n", file);
  219.     printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
  220.     printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
  221.     printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
  222.     printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
  223.     printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
  224.     printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
  225.     printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

  226.     /* sub-chunk-data */
  227.     if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
  228.                 SEEK_SET)) {
  229.         perror("lseek error");
  230.         close(fd);
  231.         return -1;
  232.     }

  233.     while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

  234.         /* 找到sub-chunk-data */
  235.         if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
  236.             return 0;

  237.         if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
  238.             perror("lseek error");
  239.             close(fd);
  240.             return -1;
  241.         }
  242.     }

  243.     fprintf(stderr, "check error: %s\n", file);
  244.     return -1;
  245. }

  246. /************************************
  247. main主函数
  248. ************************************/
  249. int main(int argc, char *argv[])
  250. {
  251.     snd_pcm_sframes_t avail;
  252.     struct termios new_cfg;
  253.     sigset_t sset;
  254.     int ret;

  255.     if (2 != argc) {
  256.         fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
  257.         exit(EXIT_FAILURE);
  258.     }

  259.     /* 屏蔽SIGIO信号 */
  260.     sigemptyset(&sset);
  261.     sigaddset(&sset, SIGIO);
  262.     sigprocmask(SIG_BLOCK, &sset, NULL);

  263.     /* 打开WAV音频文件 */
  264.     if (open_wav_file(argv[1]))
  265.         exit(EXIT_FAILURE);

  266.     /* 初始化PCM Playback设备 */
  267.     if (snd_pcm_init())
  268.         goto err1;

  269.     /* 申请读缓冲区 */
  270.     buf = malloc(buf_bytes);
  271.     if (NULL == buf) {
  272.         perror("malloc error");
  273.         goto err2;
  274.     }

  275.     /* 终端配置 */
  276.     tcgetattr(STDIN_FILENO, &old_cfg);  //获取终端<标准输入-标准输出构成了一套终端>
  277.     memcpy(&new_cfg, &old_cfg, sizeof(struct termios));//备份
  278.     new_cfg.c_lflag &= ~ICANON; //将终端设置为非规范模式
  279.     new_cfg.c_lflag &= ~ECHO;   //禁用回显
  280.     tcsetattr(STDIN_FILENO, TCSANOW, &new_cfg);//使配置生效

  281.     /* 播放:先将环形缓冲区填满数据 */
  282.     avail = snd_pcm_avail_update(pcm);  //获取环形缓冲区中有多少帧数据需要填充
  283.     while (avail >= period_size) {  //我们一次写入一个周期

  284.         memset(buf, 0x00, buf_bytes);   //buf清零
  285.         ret = read(fd, buf, buf_bytes);
  286.         if (0 >= ret)
  287.             goto err3;

  288.         ret = snd_pcm_writei(pcm, buf, period_size);//向环形缓冲区中写入数据
  289.         if (0 > ret) {
  290.             fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  291.             goto err3;
  292.         }
  293.         else if (ret < period_size) {//实际写入的帧数小于指定的帧数
  294.             //此时我们需要调整下音频文件的读位置
  295.             //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
  296.             //frame_bytes表示一帧的字节大小
  297.             if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  298.                 perror("lseek error");
  299.                 goto err3;
  300.             }
  301.         }

  302.         avail = snd_pcm_avail_update(pcm);  //再次获取、更新avail
  303.     }

  304.     sigprocmask(SIG_UNBLOCK, &sset, NULL);  //取消SIGIO信号屏蔽

  305.     char ch;
  306.     for ( ; ; ) {

  307.         ch = getchar(); //获取用户输入的控制字符
  308.         switch (ch) {
  309.         case 'q':   //Q键退出程序
  310.             sigprocmask(SIG_BLOCK, &sset, NULL);//屏蔽SIGIO信号
  311.             goto err3;
  312.         case ' ':   //空格暂停/恢复
  313.             switch (snd_pcm_state(pcm)) {

  314.             case SND_PCM_STATE_PAUSED:  //如果是暂停状态则恢复运行
  315.                 ret = snd_pcm_pause(pcm, 0);
  316.                 if (0 > ret)
  317.                     fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
  318.                 break;
  319.             case SND_PCM_STATE_RUNNING: //如果是运行状态则暂停
  320.                 ret = snd_pcm_pause(pcm, 1);
  321.                 if (0 > ret)
  322.                     fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
  323.                 break;
  324.             }
  325.             break;
  326.         }
  327.     }

  328. err3:
  329.     snd_pcm_drop(pcm); //停止PCM
  330.     tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
  331.     free(buf);     //释放内存
  332. err2:
  333.     snd_pcm_close(pcm); //关闭pcm设备
  334. err1:
  335.     close(fd);      //关闭打开的音频文件
  336.     exit(EXIT_FAILURE);
  337. }
复制代码


上述示例程序是在示例代码 29.6.1基础上进行修改了,加入了用户控制单元,程序设定:
q:在终端按下q键退出应用程序;
终端按下空格键暂停播放,再次按下恢复播放。
下面给大家简单地介绍下上述示例代码的设计,在main()函数中,我们首先屏蔽了SIGIO信号,如下:
  1. /* 屏蔽SIGIO信号 */
  2. sigemptyset(&sset);
  3. sigaddset(&sset, SIGIO);
  4. sigprocmask(SIG_BLOCK, &sset, NULL);
复制代码


这主要是为了程序设计上的安全考虑,等把环形缓冲区填满数据之后,再取消SIGIO信号屏蔽。当然,你也可以不这样做。
接着打开用户传入的音频文件、初始化PCM播放设备、申请应用程序所需的缓冲区:
  1. /* 打开WAV音频文件 */
  2. if (open_wav_file(argv[1]))
  3.     exit(EXIT_FAILURE);

  4. /* 初始化PCM Playback设备 */
  5. if (snd_pcm_init())
  6.     goto err1;

  7. /* 申请读缓冲区 */
  8. buf = malloc(buf_bytes);
  9. if (NULL == buf) {
  10.     perror("malloc error");
  11.     goto err2;
  12. }
复制代码


接着对终端进行设置,将终端配置为非规范模式、取消回显,配置为非规范模式之后,用户输入的字符会直接被应用程序读取到,而无需按下回车键;取消回显,意味着用户输入的字符,在终端不会显示出来,这些内容在串口应用编程章节给大家详细介绍过,这里就不再啰嗦!
  1. /* 终端配置 */
  2. tcgetattr(STDIN_FILENO, &old_cfg);        //获取终端<标准输入-标准输出构成了一套终端>
  3. memcpy(&new_cfg, &old_cfg, sizeof(struct termios));//备份
  4. new_cfg.c_lflag &= ~ICANON; //将终端设置为非规范模式
  5. new_cfg.c_lflag &= ~ECHO;   //禁用回显
  6. tcsetattr(STDIN_FILENO, TCSANOW, &new_cfg);//使配置生效
复制代码


接下来将数据写入环形缓冲区,开始播放。
取消SIGIO信号信号屏蔽。
最后进入for()循环中,通过getchar()读取用户输入的字符,用户输入q时退出程序,这里需要注意,退出程序时需要调用tcsetattr()将终端配置参数恢复到之前的状态,否则你的终端将可能会出现下面这种情况:
第二十九章 音频应用编程99421.png
这个时候你就只能重启了。
用户输入空格暂停或恢复,调用snd_pcm_pause()实现暂停/恢复。
代码比较简单,笔者也就不再多说了!
编译示例代码
执行命令编译应用程序:
  1. ${CC} -o testApp testApp.c -lasound
复制代码


测试应用程序
将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,并准备一个WAV音频文件,接着我们执行测试程序:
第二十九章 音频应用编程99617.png
图 29.8.2 运行测试程序
运行之后,开始播放音乐,此时我们可以通过空格键来暂停播放、再按空格键恢复播放,按q键退出程序,大家自己去测试。
Tips:本测试程序不能放在后台运行,一旦放入后台,程序将停止(不是终止、是暂停运行),因为这个程序在设计逻辑上就不符合放置在后台,因为程序中会读取用户从终端(标准输入)输入的字符,如果放入后台,那用户输入的字符就不可能被该程序所读取到,这是其一;其二,程序中修改了终端的配置。
29.8.2snd_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函数的不同错误返回值,表示不同的含义,如下所示:
第二十九章 音频应用编程100186.png
图 29.8.3 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函数出错时的一些情况以及可以采取的一些措施!
29.9混音器设置
前面给大家介绍了alsa-utils提供的两个声卡配置工具:alsamixer和amixer。这两个工具同样是基于alsa-lib库函数编写的,本小节我们来学习如何在自己的应用程序中通过调用alsa-lib库函数对声卡混音器进行配置,譬如音量调节。
混音器相关的接口在alsa-lib的Mixer Interface模块中有介绍,点击图 29.2.2中“Mixer Interface”可查看混音器相关接口的介绍,如下所示:
第二十九章 音频应用编程101014.png
图 29.9.1 Mixer Interface模块
大家可以简单地浏览下该模块下提供了那些函数,点击函数名可以查看该函数的简单介绍信息。
29.9.1打开混音器:snd_mixer_open
在使用混音器之后,需要打开混音器,调用snd_mixer_open()函数打开一个空的混音器,其函数原型如下所示:
  1. 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的错误码。
使用示例:
  1. snd_mixer_t *mixer = NULL;
  2. int ret;

  3. ret = snd_mixer_open(&mixer, 0);
  4. if (0 > ret)
  5.         fprintf(stderr, "snd_mixer_open error: %s\n", snd_strerror(ret));
复制代码


29.9.2Attach关联设备: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的错误码。
使用示例:
  1. ret = snd_mixer_attach(mixer, "hw:0");
  2. if (0 > ret)
  3.         fprintf(stderr, "snd_mixer_attach error: %s\n", snd_strerror(ret));
复制代码


29.9.3注册:snd_mixer_selem_register
调用snd_mixer_selem_register()函数注册混音器,其函数原型如下所示:
  1. int snd_mixer_selem_register(
  2.         snd_mixer_t *mixer,
  3.         struct snd_mixer_selem_regopt *options,
  4.         snd_mixer_class_t **classp);
复制代码


参数options和参数classp直接设置为NULL即可。
函数调用成功返回0;失败返回一个小于0的错误码。
使用示例:
  1. ret = snd_mixer_selem_register(mixer, NULL, NULL);
  2. if (0 > ret)
  3.         fprintf(stderr, "snd_mixer_selem_register error: %s\n", snd_strerror(ret));
复制代码


29.9.4加载:snd_mixer_load
最后需要加载混音器,调用snd_mixer_load()函数完成加载,函数原型如下所示:
  1. int snd_mixer_load(snd_mixer_t * mixer);
复制代码


函数调用成功返回0;失败返回小于0的错误码。
使用示例:
  1. ret = snd_mixer_load(mixer);
  2. if (0 > ret)
  3.         fprintf(stderr, "snd_mixer_load error: %s\n", snd_strerror(ret));
复制代码


29.9.5查找元素
经过上面一系列步骤之后,接下来就可以使用混音器了,alsa-lib中把混音器的配置项称为元素(element),譬如耳机音量调节Headphone是一个元素、'Headphone Playback ZC'是一个元素、'Right Output Mixer PCM'也是一个元素。
  1. snd_mixer_first_elem和snd_mixer_last_elem
复制代码


alsa-lib使用数据结构snd_mixer_elem_t来描述一个元素,所以一个snd_mixer_elem_t对象就是一个元素。混音器有很多的元素(也就是有很多配置项),通过snd_mixer_first_elem()函数可以找到混音器的第一个元素,其函数原型如下所示:
  1. snd_mixer_elem_t *snd_mixer_first_elem(snd_mixer_t *mixer);
复制代码


通过snd_mixer_last_elem()函数可找到混音器的最后一个元素,如下:
  1. 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()函数可获取指定元素的下一个元素和上一个元素:
  1. snd_mixer_elem_t *snd_mixer_elem_next(snd_mixer_elem_t *elem);
  2. 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()就可以遍历整个混音器中的所有元素,如下所示:
  1. snd_mixer_elem_t *elem = NULL;

  2. elem = snd_mixer_first_elem(mixer);//找到第一个元素
  3. while (elem) {

  4.         ......
  5.         ......

  6.         snd_mixer_elem_next(elem);        //找到下一个元素
  7. }
  8. snd_mixer_selem_get_name
复制代码


调用snd_mixer_selem_get_name()函数可获取指定元素的名字,如下所示:
  1. const char *snd_mixer_selem_get_name(snd_mixer_elem_t *elem);
复制代码


获取元素的名字之后,进行对比,以确定是否是我们要找的元素:
  1. const char *name = snd_mixer_selem_get_name(elem);
  2. if (!strcmp(name, "Headphone")) {
  3.         //该配置项是"Headphone"
  4. }
  5. else {
  6.         //该配置项不是"Headphone"
  7. }
复制代码


29.9.6获取/更改元素的配置值
前面给大家提到了混音器的配置值有两种类型,第一种它的配置值是在一个范围内的数值,譬如音量大小的调节;另一种则是bool类型,用于控制开启或关闭,譬如0表示关闭配置、1表示使能配置。
  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类型,也就是上面说的第一种情况。函数原型如下所示:
  1. int snd_mixer_selem_has_playback_volume(snd_mixer_elem_t *elem);
  2. int snd_mixer_selem_has_capture_volume(snd_mixer_elem_t *elem);
复制代码


函数返回0表示不是volume类型;返回1表示是volume类型。
  1. 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类型,也就是上面说的第二种情况。函数原型如下所示:
  1. int snd_mixer_selem_has_playback_switch(snd_mixer_elem_t *elem);
  2. int snd_mixer_selem_has_capture_switch(snd_mixer_elem_t *elem);
复制代码


函数返回0表示不是switch类型;返回1表示是switch类型。
  1. snd_mixer_selem_has_playback_channel/snd_mixer_selem_has_capture_channel
复制代码


通过snd_mixer_selem_has_playback_channel(播放)或snd_mixer_selem_has_capture_channel(录音)函数可判断指定元素是否包含指定通道,其函数原型如下所示:
  1. int snd_mixer_selem_has_playback_channel(
  2.         snd_mixer_elem_t *elem,
  3.         snd_mixer_selem_channel_id_t channel
  4. );

  5. int snd_mixer_selem_has_capture_channel(
  6.         snd_mixer_elem_t *elem,
  7.         snd_mixer_selem_channel_id_t channel
  8. );
复制代码


参数channel用于指定一个通道,snd_mixer_selem_channel_id_t是一个枚举类型,如下所示:
  1. enum snd_mixer_selem_channel_id_t {
  2.         SND_MIXER_SCHN_UNKNOWN = -1,
  3.         SND_MIXER_SCHN_FRONT_LEFT = 0,                        //左前
  4.         SND_MIXER_SCHN_FRONT_RIGHT,                        //右前
  5.         SND_MIXER_SCHN_REAR_LEFT,                                //左后
  6.         SND_MIXER_SCHN_REAR_RIGHT,                                //右后
  7.         SND_MIXER_SCHN_FRONT_CENTER,                        //前中
  8.         SND_MIXER_SCHN_WOOFER,                                        //低音喇叭
  9.         SND_MIXER_SCHN_SIDE_LEFT,                                //左侧
  10.         SND_MIXER_SCHN_SIDE_RIGHT,                                //右侧
  11.         SND_MIXER_SCHN_REAR_CENTER,                        //后中
  12.         SND_MIXER_SCHN_LAST = 31,
  13.         SND_MIXER_SCHN_MONO = SND_MIXER_SCHN_FRONT_LEFT        //单声道
  14. };
复制代码


如果元素是双声道元素,通常只包含左前(SND_MIXER_SCHN_FRONT_LEFT)和右前(SND_MIXER_SCHN_FRONT_RIGHT)两个声道。如果是单声道设备,通常只包含SND_MIXER_SCHN_MONO,其数值等于
  1. SND_MIXER_SCHN_FRONT_LEFT。
复制代码

可以调用snd_mixer_selem_is_playback_mono(播放)或snd_mixer_selem_is_capture_mono(录音)函数判断一个指定的元素是否是单声道元素,其函数原型如下所示:
  1. int snd_mixer_selem_is_playback_mono(snd_mixer_elem_t *elem);
  2. int snd_mixer_selem_is_capture_mono(snd_mixer_elem_t *elem);
  3. snd_mixer_selem_get_playback_volume/snd_mixer_selem_get_capture_volume
复制代码


调用snd_mixer_selem_get_playback_volume(播放)或snd_mixer_selem_get_capture_volume(录音)获取指定元素的音量大小,其函数原型如下所示:
  1. int snd_mixer_selem_get_playback_volume(
  2.         snd_mixer_elem_t *elem,
  3.         snd_mixer_selem_channel_id_t channel,
  4.         long *value
  5. );

  6. int snd_mixer_selem_get_capture_volume(
  7.         snd_mixer_elem_t *elem,
  8.         snd_mixer_selem_channel_id_t channel,
  9.         long *value
  10. );
复制代码


参数elem指定对应的元素,参数channel指定该元素的某个声道。调用snd_mixer_selem_get_playback_volume()函数可获取elem元素的channel声道对应的音量大小,并将获取到的音量值通过value返回出来。
函数调用成功返回0,失败返回一个小于0的错误码。
譬如,获取左前声道的音量(播放):
  1. long value;

  2. snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &value);
  3. snd_mixer_selem_set_playback_volume/snd_mixer_selem_set_capture_volume
复制代码


设置指定元素的音量值,其函数原型如下所示:
  1. int snd_mixer_selem_set_playback_volume(
  2.         snd_mixer_elem_t *elem,
  3.         snd_mixer_selem_channel_id_t channel,
  4.         long value
  5. );

  6. int snd_mixer_selem_set_capture_volume(
  7.         snd_mixer_elem_t *elem,
  8.         snd_mixer_selem_channel_id_t channel,
  9.         long value
  10. );
复制代码


调用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可一次性设置指定元素所有声道的音量,函数原型如下所示:
  1. int snd_mixer_selem_set_playback_volume_all(
  2.         snd_mixer_elem_t *elem,
  3.         long value
  4. );

  5. int snd_mixer_selem_set_capture_volume_all(
  6.         snd_mixer_elem_t *elem,
  7.         long value
  8. );
复制代码


snd_mixer_selem_get_playback_volume_range/snd_mixer_selem_get_capture_volume_range
获取指定元素的音量范围,其函数原型如下所示:
  1. int snd_mixer_selem_get_playback_volume_range(
  2.         snd_mixer_elem_t *elem,
  3.         long *min,
  4.         long *max
  5. );

  6. int snd_mixer_selem_get_capture_volume_range(
  7.         snd_mixer_elem_t *elem,
  8.         long *min,
  9.         long *max
  10. );
复制代码


29.9.7示例程序
本小节我们将对示例代码 29.7.1进行修改,添加音量控制,示例代码如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_playback_mixer.c。
示例代码 29.9.1 PCM播放示例程序(加入状态控制)
  1. /***************************************************************
  2. Copyright &#169; ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
  3. 文件名 : pcm_playback_mixer.c
  4. 作者 : 邓涛
  5. 版本 : V1.0
  6. 描述 : 一个简单地PCM播放示例代码--使用异步方式、加入混音器设置
  7. 其他 : 无
  8. 论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9. 日志 : 初版 V1.0 2021/7/20 邓涛创建
  10. ***************************************************************/

  11. #include <stdio.h>
  12. #include <stdlib.h>
  13. #include <errno.h>
  14. #include <string.h>
  15. #include <termios.h>
  16. #include <signal.h>
  17. #include <alsa/asoundlib.h>

  18. /************************************
  19. 宏定义
  20. ************************************/
  21. #define PCM_PLAYBACK_DEV    "hw:0,0"
  22. #define MIXER_DEV           "hw:0"

  23. /************************************
  24. WAV音频文件解析相关数据结构申明
  25. ************************************/
  26. typedef struct WAV_RIFF {
  27.     char ChunkID[4];                        /* "RIFF" */
  28.     u_int32_t ChunkSize;                    /* 从下一个地址开始到文件末尾的总字节数 */
  29.     char Format[4];                         /* "WAVE" */
  30. } __attribute__ ((packed)) RIFF_t;

  31. typedef struct WAV_FMT {
  32.     char Subchunk1ID[4];                     /* "fmt " */
  33.     u_int32_t Subchunk1Size;                  /* 16 for PCM */
  34.     u_int16_t AudioFormat;                    /* PCM = 1*/
  35.     u_int16_t NumChannels;                   /* Mono = 1, Stereo = 2, etc. */
  36.     u_int32_t SampleRate;                     /* 8000, 44100, etc. */
  37.     u_int32_t ByteRate;                       /* = SampleRate * NumChannels * BitsPerSample/8 */
  38.     u_int16_t BlockAlign;                     /* = NumChannels * BitsPerSample/8 */
  39.     u_int16_t BitsPerSample;                  /* 8bits, 16bits, etc. */
  40. } __attribute__ ((packed)) FMT_t;
  41. static FMT_t wav_fmt;

  42. typedef struct WAV_DATA {
  43.     char Subchunk2ID[4];                     /* "data" */
  44.     u_int32_t Subchunk2Size;                  /* data size */
  45. } __attribute__ ((packed)) DATA_t;

  46. /************************************
  47. static静态全局变量定义
  48. ************************************/
  49. static snd_pcm_t *pcm = NULL;                           //pcm句柄
  50. static snd_mixer_t *mixer = NULL;                          //混音器句柄
  51. static snd_mixer_elem_t *playback_vol_elem = NULL;         //播放<音量控制>元素
  52. static unsigned int buf_bytes;                              //应用程序缓冲区的大小(字节为单位)
  53. static void *buf = NULL;                                //指向应用程序缓冲区的指针
  54. static int fd = -1;                                         //指向WAV音频文件的文件描述符
  55. static snd_pcm_uframes_t period_size = 1024;                //周期大小(单位: 帧)
  56. static unsigned int periods = 16;                           //周期数(设备驱动层buffer的大小)
  57. static struct termios old_cfg;                              //用于保存终端当前的配置参数

  58. /************************************
  59. static静态函数
  60. ************************************/
  61. static void snd_playback_async_callback(snd_async_handler_t *handler)
  62. {
  63.     snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄
  64.     snd_pcm_sframes_t avail;
  65.     int ret;

  66.     avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充
  67.     while (avail >= period_size) {  //我们一次写入一个周期

  68.         memset(buf, 0x00, buf_bytes);   //buf清零
  69.         ret = read(fd, buf, buf_bytes);
  70.         if (0 >= ret)
  71.             goto out;

  72.         ret = snd_pcm_writei(handle, buf, period_size);
  73.         if (0 > ret) {
  74.             fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  75.             goto out;
  76.         }
  77.         else if (ret < period_size) {//实际写入的帧数小于指定的帧数
  78.             //此时我们需要调整下音频文件的读位置
  79.             //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
  80.             //frame_bytes表示一帧的字节大小
  81.             if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  82.                 perror("lseek error");
  83.                 goto out;
  84.             }
  85.         }

  86.         avail = snd_pcm_avail_update(handle);   //再次获取、更新avail
  87.     }

  88.     return;
  89. out:
  90.     snd_pcm_drain(pcm); //停止PCM
  91.     snd_mixer_close(mixer); //关闭混音器
  92.     snd_pcm_close(handle); //关闭pcm设备
  93.     tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
  94.     free(buf);
  95.     close(fd);      //关闭打开的音频文件
  96.     exit(EXIT_FAILURE); //退出程序
  97. }

  98. static int snd_pcm_init(void)
  99. {
  100.     snd_pcm_hw_params_t *hwparams = NULL;
  101.     snd_async_handler_t *async_handler = NULL;
  102.     int ret;

  103.     /* 打开PCM设备 */
  104.     ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
  105.     if (0 > ret) {
  106.         fprintf(stderr, "snd_pcm_open error: %s: %s\n",
  107.                     PCM_PLAYBACK_DEV, snd_strerror(ret));
  108.         return -1;
  109.     }

  110.     /* 实例化hwparams对象 */
  111.     snd_pcm_hw_params_malloc(&hwparams);

  112.     /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
  113.     ret = snd_pcm_hw_params_any(pcm, hwparams);
  114.     if (0 > ret) {
  115.         fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
  116.         goto err2;
  117.     }

  118.     /**************
  119.      设置参数
  120.     ***************/
  121.     /* 设置访问类型: 交错模式 */
  122.     ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  123.     if (0 > ret) {
  124.         fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
  125.         goto err2;
  126.     }

  127.     /* 设置数据格式: 有符号16位、小端模式 */
  128.     ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
  129.     if (0 > ret) {
  130.         fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
  131.         goto err2;
  132.     }

  133.     /* 设置采样率 */
  134.     ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
  135.     if (0 > ret) {
  136.         fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
  137.         goto err2;
  138.     }

  139.     /* 设置声道数: 双声道 */
  140.     ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
  141.     if (0 > ret) {
  142.         fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
  143.         goto err2;
  144.     }

  145.     /* 设置周期大小: period_size */
  146.     ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
  147.     if (0 > ret) {
  148.         fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
  149.         goto err2;
  150.     }

  151.     /* 设置周期数(驱动层环形缓冲区buffer的大小): periods */
  152.     ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
  153.     if (0 > ret) {
  154.         fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
  155.         goto err2;
  156.     }

  157.     /* 使配置生效 */
  158.     ret = snd_pcm_hw_params(pcm, hwparams);
  159.     snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
  160.     if (0 > ret) {
  161.         fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
  162.         goto err1;
  163.     }

  164.     buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小

  165.     /* 注册异步处理函数 */
  166.     ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
  167.     if (0 > ret) {
  168.         fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
  169.         goto err1;
  170.     }

  171.     return 0;

  172. err2:
  173.     snd_pcm_hw_params_free(hwparams);   //释放内存
  174. err1:
  175.     snd_pcm_close(pcm); //关闭pcm设备
  176.     return -1;
  177. }

  178. static int snd_mixer_init(void)
  179. {
  180.     snd_mixer_elem_t *elem = NULL;
  181.     const char *elem_name;
  182.     long minvol, maxvol;
  183.     int ret;

  184.     /* 打开混音器 */
  185.     ret = snd_mixer_open(&mixer, 0);
  186.     if (0 > ret) {
  187.         fprintf(stderr, "snd_mixer_open error: %s\n", snd_strerror(ret));
  188.         return -1;
  189.     }

  190.     /* 关联一个声卡控制设备 */
  191.     ret = snd_mixer_attach(mixer, MIXER_DEV);
  192.     if (0 > ret) {
  193.         fprintf(stderr, "snd_mixer_attach error: %s\n", snd_strerror(ret));
  194.         goto err;
  195.     }

  196.     /* 注册混音器 */
  197.     ret = snd_mixer_selem_register(mixer, NULL, NULL);
  198.     if (0 > ret) {
  199.         fprintf(stderr, "snd_mixer_selem_register error: %s\n", snd_strerror(ret));
  200.         goto err;
  201.     }

  202.     /* 加载混音器 */
  203.     ret = snd_mixer_load(mixer);
  204.     if (0 > ret) {
  205.         fprintf(stderr, "snd_mixer_load error: %s\n", snd_strerror(ret));
  206.         goto err;
  207.     }

  208.     /* 遍历混音器中的元素 */
  209.     elem = snd_mixer_first_elem(mixer);//找到第一个元素
  210.     while (elem) {

  211.         elem_name = snd_mixer_selem_get_name(elem);//获取元素的名称
  212.         /* 针对开发板出厂系统:WM8960声卡设备 */
  213.         if(!strcmp("Speaker", elem_name) ||  //耳机音量<对喇叭外音输出有效>
  214.            !strcmp("Headphone", elem_name) ||//喇叭音量<对耳机输出有效>
  215.            !strcmp("Playback", elem_name)) {//播放音量<总的音量控制,对喇叭和耳机输出都有效>
  216.             if (snd_mixer_selem_has_playback_volume(elem)) {//是否是音量控制元素
  217.                 snd_mixer_selem_get_playback_volume_range(elem, &minvol, &maxvol);//获取音量可设置范围
  218.                 snd_mixer_selem_set_playback_volume_all(elem, (maxvol-minvol)*0.9 + minvol);//全部设置为90%

  219.                 if (!strcmp("Playback", elem_name))
  220.                     playback_vol_elem = elem;
  221.             }
  222.         }

  223.         elem = snd_mixer_elem_next(elem);
  224.     }

  225.     return 0;

  226. err:
  227.     snd_mixer_close(mixer);
  228.     return -1;
  229. }

  230. static int open_wav_file(const char *file)
  231. {
  232.     RIFF_t wav_riff;
  233.     DATA_t wav_data;
  234.     int ret;

  235.     fd = open(file, O_RDONLY);
  236.     if (0 > fd) {
  237.         fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
  238.         return -1;
  239.     }

  240.     /* 读取RIFF chunk */
  241.     ret = read(fd, &wav_riff, sizeof(RIFF_t));
  242.     if (sizeof(RIFF_t) != ret) {
  243.         if (0 > ret)
  244.             perror("read error");
  245.         else
  246.             fprintf(stderr, "check error: %s\n", file);
  247.         close(fd);
  248.         return -1;
  249.     }

  250.     if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
  251.         strncmp("WAVE", wav_riff.Format, 4)) {
  252.         fprintf(stderr, "check error: %s\n", file);
  253.         close(fd);
  254.         return -1;
  255.     }

  256.     /* 读取sub-chunk-fmt */
  257.     ret = read(fd, &wav_fmt, sizeof(FMT_t));
  258.     if (sizeof(FMT_t) != ret) {
  259.         if (0 > ret)
  260.             perror("read error");
  261.         else
  262.             fprintf(stderr, "check error: %s\n", file);
  263.         close(fd);
  264.         return -1;
  265.     }

  266.     if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
  267.         fprintf(stderr, "check error: %s\n", file);
  268.         close(fd);
  269.         return -1;
  270.     }

  271.     /* 打印音频文件的信息 */
  272.     printf("<<<<音频文件格式信息>>>>\n\n");
  273.     printf("  file name:     %s\n", file);
  274.     printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
  275.     printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
  276.     printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
  277.     printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
  278.     printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
  279.     printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
  280.     printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

  281.     /* sub-chunk-data */
  282.     if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
  283.                 SEEK_SET)) {
  284.         perror("lseek error");
  285.         close(fd);
  286.         return -1;
  287.     }

  288.     while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

  289.         /* 找到sub-chunk-data */
  290.         if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
  291.             return 0;

  292.         if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
  293.             perror("lseek error");
  294.             close(fd);
  295.             return -1;
  296.         }
  297.     }

  298.     fprintf(stderr, "check error: %s\n", file);
  299.     return -1;
  300. }

  301. static void show_help(void)
  302. {
  303.     printf("<<<<<<<基于alsa-lib音乐播放器>>>>>>>>>\n\n"
  304.            "操作菜单:\n"
  305.            "  q             退出程序\n"
  306.            "  space<空格>   暂停播放/恢复播放\n"
  307.            "  w             音量增加++\n"
  308.            "  s             音量减小--\n\n");
  309. }

  310. /************************************
  311. main主函数
  312. ************************************/
  313. int main(int argc, char *argv[])
  314. {
  315.     snd_pcm_sframes_t avail;
  316.     struct termios new_cfg;
  317.     sigset_t sset;
  318.     int ret;

  319.     if (2 != argc) {
  320.         fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
  321.         exit(EXIT_FAILURE);
  322.     }

  323.     /* 屏蔽SIGIO信号 */
  324.     sigemptyset(&sset);
  325.     sigaddset(&sset, SIGIO);
  326.     sigprocmask(SIG_BLOCK, &sset, NULL);

  327.     /* 打开WAV音频文件 */
  328.     if (open_wav_file(argv[1]))
  329.         exit(EXIT_FAILURE);

  330.     /* 初始化PCM Playback设备 */
  331.     if (snd_pcm_init())
  332.         goto err1;

  333.     /* 初始化混音器 */
  334.     if (snd_mixer_init())
  335.         goto err2;

  336.     /* 申请读缓冲区 */
  337.     buf = malloc(buf_bytes);
  338.     if (NULL == buf) {
  339.         perror("malloc error");
  340.         goto err3;
  341.     }

  342.     /* 终端配置 */
  343.     tcgetattr(STDIN_FILENO, &old_cfg);  //获取终端<标准输入-标准输出构成了一套终端>
  344.     memcpy(&new_cfg, &old_cfg, sizeof(struct termios));//备份
  345.     new_cfg.c_lflag &= ~ICANON; //将终端设置为非规范模式
  346.     new_cfg.c_lflag &= ~ECHO;   //禁用回显
  347.     tcsetattr(STDIN_FILENO, TCSANOW, &new_cfg);//使配置生效

  348.     /* 播放:先将环形缓冲区填满数据 */
  349.     avail = snd_pcm_avail_update(pcm);  //获取环形缓冲区中有多少帧数据需要填充
  350.     while (avail >= period_size) {  //我们一次写入一个周期

  351.         memset(buf, 0x00, buf_bytes);   //buf清零
  352.         ret = read(fd, buf, buf_bytes);
  353.         if (0 >= ret)
  354.             goto err4;

  355.         ret = snd_pcm_writei(pcm, buf, period_size);//向环形缓冲区中写入数据
  356.         if (0 > ret) {
  357.             fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
  358.             goto err4;
  359.         }
  360.         else if (ret < period_size) {//实际写入的帧数小于指定的帧数
  361.             //此时我们需要调整下音频文件的读位置
  362.             //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
  363.             //frame_bytes表示一帧的字节大小
  364.             if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
  365.                 perror("lseek error");
  366.                 goto err4;
  367.             }
  368.         }

  369.         avail = snd_pcm_avail_update(pcm);  //再次获取、更新avail
  370.     }

  371.     sigprocmask(SIG_UNBLOCK, &sset, NULL);  //取消SIGIO信号屏蔽

  372.     /* 显示帮助信息 */
  373.     show_help();

  374.     /* 等待获取用户输入 */
  375.     char ch;
  376.     long vol;
  377.     for ( ; ; ) {

  378.         ch = getchar(); //获取用户输入的控制字符
  379.         switch (ch) {
  380.         case 'q':   //Q键退出程序
  381.             sigprocmask(SIG_BLOCK, &sset, NULL);//屏蔽SIGIO信号
  382.             goto err4;
  383.         case ' ':   //空格暂停/恢复
  384.             switch (snd_pcm_state(pcm)) {

  385.             case SND_PCM_STATE_PAUSED:  //如果是暂停状态则恢复运行
  386.                 ret = snd_pcm_pause(pcm, 0);
  387.                 if (0 > ret)
  388.                     fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
  389.                 break;
  390.             case SND_PCM_STATE_RUNNING: //如果是运行状态则暂停
  391.                 ret = snd_pcm_pause(pcm, 1);
  392.                 if (0 > ret)
  393.                     fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
  394.                 break;
  395.             }
  396.             break;
  397.         case 'w':           //音量增加
  398.             if (playback_vol_elem) {
  399.                 //获取音量
  400.                 snd_mixer_selem_get_playback_volume(playback_vol_elem,
  401.                         SND_MIXER_SCHN_FRONT_LEFT, &vol);
  402.                 vol++;
  403.                 //设置音量
  404.                 snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
  405.             }
  406.             break;
  407.         case 's':           //音量降低
  408.             if (playback_vol_elem) {
  409.                 //获取音量
  410.                 snd_mixer_selem_get_playback_volume(playback_vol_elem,
  411.                         SND_MIXER_SCHN_FRONT_LEFT, &vol);
  412.                 vol--;
  413.                 //设置音量
  414.                 snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
  415.             }
  416.             break;
  417.         }
  418.     }

  419. err4:
  420.     snd_pcm_drop(pcm); //停止PCM
  421.     tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
  422.     free(buf);     //释放内存
  423. err3:
  424.     snd_mixer_close(mixer); //关闭混音器
  425. err2:
  426.     snd_pcm_close(pcm); //关闭pcm设备
  427. err1:
  428.     close(fd);      //关闭打开的音频文件
  429.     exit(EXIT_FAILURE);
  430. }
复制代码


main()函数中调用了自定义函数snd_mixer_init()对声卡混音器进行了初始化,snd_mixer_init()函数中做的事情,也就是上面给大家所介绍的流程:首先打开一个空的混音器、attach关联一个声卡控制设备、注册混音器、加载混音器,整个这一套操作完成之后,就可以去使用混音器了;查找混音器中的元素,对元素进行配置。
在snd_mixer_init()函数中,我们对WM8960声卡的"Speaker"元素(喇叭输出音量)、"Headphone"元素(耳机输出音量)以及"layback"元素(播放音量)进行了配置,将它们都设置为90%;之后将"layback"元素的句柄赋值给全局静态变量playback_vol_elem。
回到main()函数,在for循环中,获取用户输入的控制字符,在这里我们添加了w和s,当用户按下w键时增加音量、按下s键时降低音量,这里控制的音量是WM8960的"layback"音量(播放音量)。
编译应用程序
编译上述示例代码:
  1. ${CC} -o testApp testApp.c -lasound
复制代码


第二十九章 音频应用编程123526.png
图 29.9.2 编译示例代码
测试应用程序
将编译得到的可执行文件拷贝到开发板Linux系统的/home/root目录下,准备一个WAV格式的音频文件,执行测试程序:
  1. ./testApp ./EXO-Overdose.wav
复制代码


第二十九章 音频应用编程123685.png
图 29.9.3 执行测试程序
命令执行之后会显示操作方式,大家可以根据提示自己测试!
29.10回环测试例程
alsa-utils提供了一个用于回环测试的工具alsaloop,可以实现边录音、边播放,该程序用法比较简单,执行"alsaloop --help"可以查看alsaloop测试程序的使用帮助信息,如下所示:
第二十九章 音频应用编程123885.png
图 29.10.1 alsaloop工具使用帮助信息
譬如直接运行"alsaloop -t 1000"可以进行测试,大家可以自己亲自测试下。
回环测试原理上很简单,录制音频、然后再播放出来,但是事实上并不如此,还需要考虑到很多的因素,因为对于录音和播放来说,录制一个周期和播放一个周期,硬件上处理这一个周期所花费的时间并不相同,一个是ADC过程、而一个是DAC过程,所以往往很容易出现XRUN,所以如何有效合理地设计你的应用程序将变得很重要、以最大限度降低XRUN情况的发生。
笔者测试过alsaloop工具,虽然也会出现XRUN,但比较少;如果对此有兴趣的读者,可以参考alsaloop程序的源代码,直接下载alsa-util源码包,在alsa-util源码包中就可以找到alsaloop程序的源码,如下所示:
第二十九章 音频应用编程124287.png
图 29.10.2 alsaloop源码
除了alsaloop的源码之外,还包括前面所介绍的aplay、alsamixer、amixer、alsactl等这些工具的源码都在这里,有兴趣的读者可以看看。
29.11总结
本章我们学习了Linux下的音频应用编程,应用程序基于alsa-lib库实现播放、录音等功能,本章并没有做过多深入的学习,仅仅只是给大家介绍了alsa-lib库函数中一些基本的API接口,其中还有绝大部分的接口并没有给大家介绍,如果大家有兴趣,可以自己深入研究、学习!
本小节我们来聊一聊ALSA插件。
29.11.1ALSA插件(plugin)
ALSA提供了一些PCM插件,以扩展PCM设备的功能和特性,插件负责各种样本转换、通道之间的样本复制等。
调用snd_pcm_open()函数时,需要填写PCM设备名,alsa-lib库使用逻辑设备名而不是设备节点名。前面编写的示例程序中,我们使用了"hw:i,j"这种格式的名字,这其实指定的是一个名为hw的插件,而冒号后面的两个数字i和j表示两个参数,也就是使用该插件时传入的两个参数(第一个参数表示声卡号,第二个参数表示设备号)。
开发板Linux系统的/usr/share/alsa/目录下有一个名为alsa.conf的文件,如下所示:
第二十九章 音频应用编程124876.png
图 29.11.1 alsa.conf文件
该文件是alsa-lib库的配置文件,调用snd_pcm_open()函数时会加载/usr/share/alsa/alsa.conf文件并解析,从上图中可知,/usr/share/alsa/alsa.conf文件中会加载并解析/etc/asound.conf和~/.asoundrc这两个配置文件,在我们的开发板出厂系统中,有/etc/asound.conf配置文件、但并没有~/.asoundrc文件。
/usr/share/alsa/alsa.conf配置文件作为alsa-lib库函数的主要入口点,对alsa-lib进行了配置并定义了一些基本、通用的PCM插件;而.asoundrc和asound.conf文件的引入提供用户定制化需求,用户可以在这两个文件中根据自己的需求定义插件。
关于插件的定义以及相关的解释说明,大家可以参考以下两份ALSA提供的文档:
https://www.alsa-project.org/main/index.php/Asoundrc
https://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html
譬如开发板出厂系统/etc/asound.conf文件中定义很多的PCM插件,如下所示:
第二十九章 音频应用编程125637.png
图 29.11.2 /etc/asound.conf文件中定义的插件
上图中的每一个pcm.name { }就定义了一个插件,name表示插件的名字,譬如dmix_48000、dmix_44100、dmix_32000等;而点号前面的pcm表示name是一个PCM插件,用于PCM设备;中括号{ }里边的内容则是对插件的属性定义。
中括号{ }中,type字段指定了插件的类型,alsa-lib支持多种不同的插件类型,譬如hw、plughw、mmap_emul、shm、linear、plug、multi、share、dmix、dsnoop、softvol等等,不同的类型的插件支持不同的功能、特性,下面给大家简单地进行介绍。
hw插件
该插件直接与ALSA内核驱动程序通信,这是一种没有任何转换的原始通信。应用程序调用alsa-lib库函数直接操作了底层音频硬件设置,譬如对PCM设备的配置、直接作用于硬件。
plughw插件
该插件能够提供诸如采样率转换这样的软件特性,硬件本身并不支持这样的特性。譬如,应用程序播放的音频文件是48000采样率,但是底层音频硬件本身并不支持这种采样率,所以调用snd_pcm_hw_params_set_rate()函数将PCM设备的采样率设置为48000时会导致错误!
这时可以使用plughw插件,它支持采样率转换这样的软件特性。
dmix插件
该支持混音,将多个应用程序的音频数据进行混合。
softvol插件
支持软件音量。
关于这些插件更加的详细地介绍说明,请查看ALSA提供的文档。





正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

0

主题

201

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
2552
金钱
2552
注册时间
2019-12-5
在线时间
352 小时
发表于 2021-9-13 14:12:08 | 显示全部楼层
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2024-11-25 16:43

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表