当前位置: 首页 > news >正文

土建设计网站贵阳网站搜索优化

土建设计网站,贵阳网站搜索优化,舟山百度seo,黄骅市天气预报最新目录 ALSA 概述alsa-lib 简介sound 设备节点alsa-lib 移植编写一个简单地alsa-lib 应用程序一些基本概念打开PCM 设备设置硬件参数读/写数据示例代码之PCM 播放示例代码值PCM 录音 使用异步方式PCM 播放示例-异步方式PCM 录音示例-异步方式 使用poll()函数使用poll I/O 多路复用… 目录 ALSA 概述alsa-lib 简介sound 设备节点alsa-lib 移植编写一个简单地alsa-lib 应用程序一些基本概念打开PCM 设备设置硬件参数读/写数据示例代码之PCM 播放示例代码值PCM 录音 使用异步方式PCM 播放示例-异步方式PCM 录音示例-异步方式 使用poll()函数使用poll I/O 多路复用实现读写PCM 播放示例代码PCM 录音示例代码 PCM 设备的状态PCM 播放示例代码-加入状态控制snd_pcm_readi/snd_pcm_writei 错误处理 混音器设置打开混音器snd_mixer_openAttach 关联设备snd_mixer_attach注册snd_mixer_selem_register加载snd_mixer_load查找元素获取/更改元素的配置值示例程序 回环测试例程总结ALSA 插件plugin ALPHA I.MX6U 开发板支持音频板上搭载了音频编解码芯片WM8960支持播放以及录音功能 本章我们来学习Linux 下的音频应用编程音频应用编程相比于前面几个章节所介绍的内容、其难度有所上升但是笔者仅向大家介绍Linux 音频应用编程中的基础知识而更多细节、更加深入的内容需要大家自己去学习。 ALSA 概述 ALSA 是Advanced Linux Sound Architecture高级的Linux 声音体系的缩写目前已经成为了linux 下的主流音频体系架构提供了音频和MIDI 的支持替代了原先旧版本中的OSS开发声音系统学习过Linux 音频驱动开发的读者肯定知道这个事实上ALSA 是Linux 系统下一套标准的、先进的音频驱动框架那么这套框架的设计本身是比较复杂的采用分离、分层思想设计而成具体的细节便不给大家介绍了作为音频应用编程我们不用去研究这个。 在应用层ALSA 为我们提供了一套标准的API应用程序只需要调用这些API 就可完成对底层音频硬件设备的控制譬如播放、录音等这一套API 称为alsa-lib。如下图所示 alsa-lib 简介 如上所述alsa-lib 是一套Linux 应用层的C 语言函数库为音频应用程序开发提供了一套统一、标准的接口应用程序只需调用这一套API 即可完成对底层声卡设备的操控譬如播放与录音。 用户空间的alsa-lib 对应用程序提供了统一的API 接口这样可以隐藏驱动层的实现细节简化了应用程序的实现难度、无需应用程序开发人员直接去读写音频设备节点。所以本章对于我们来说学习音频应用编程其实就是学习alsa-lib 库函数的使用、如何基于alsa-lib 库函数开发音频应用程序。 ALSA 提供了关于alsa-lib 的使用说明文档其链接地址为https://www.alsa-project.org/alsa-doc/alsa-lib/进入到该链接地址后如下所示 alsa-lib 库支持功能比较多提供了丰富的API 接口供应用程序开发人员调用根据函数的功能、作用将这些API 进行了分类可以点击上图中Modules 按钮查看其模块划分如下所示 一个分类就是一个模块module有些模块下可能该包含了子模块譬如上图中模块名称前面有三角箭头的表示该模块包含有子模块。 ⚫ Global defines and functions包括一些全局的定义譬如函数、宏等 ⚫ Constants for Digital Audio Interfaces数字音频接口相关的常量 ⚫ Input Interface输入接口 ⚫ Output Interface输出接口 ⚫ Error handling错误处理相关接口 ⚫ Configuration Interface配置接口 ⚫ Control Interface控制接口 ⚫ PCM InterfacePCM 设备接口 ⚫ RawMidi InterfaceRawMidi 接口 ⚫ Timer Interface定时器接口 ⚫ Hardware Dependant Interface硬件相关接口 ⚫ MIDI SequencerMIDI 音序器 ⚫ 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 设备启动、暂停、恢复、写入/读取数据该模块下还包含了一些子模块如下所示 点击模块名称可以查看到该模块提供的API 接口有哪些以及相应的函数说明这里就不给大家演示了 Error Interface 该模块提供了关于错误处理相关的接口譬如函数调用发生错误时可调用该模块下提供的函数打印错误描述信息。 Mixer Interface 提供了关于混音器相关的一系列操作接口譬如音量、声道控制、增益等等。 sound 设备节点 在Linux 内核设备驱动层、基于ALSA 音频驱动框架注册的sound 设备会在/dev/snd 目录下生成相应的设备节点文件譬如ALPHA I.MX6U 开发板出厂系统/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 目录下有很多的文件这些文件记录了系统中声卡相关的信息如下所示 cards 通过cat /proc/asound/cards命令、查看cards 文件的内容可列出系统中可用的、注册的声卡如下所示 cat /proc/asound/cards我们的阿尔法板子上只有一个声卡WM8960 音频编解码器所以它的编号为0也就是card0。系统中注册的所有声卡都会在/proc/asound/目录下存在一个相应的目录该目录的命名方式为cardXX 表示声卡的编号譬如图28.3.2 中的card0card0 目录下记录了声卡0 相关的信息譬如声卡的名字以及声卡注册的PCM 设备如下所示 devices 列出系统中所有声卡注册的设备包括control、pcm、timer、seq 等等。如下所示 cat /proc/asound/devicespcm 列出系统中的所有PCM 设备包括playback 和capture cat /proc/asound/pcmalsa-lib 移植 因为alsa-lib 是ALSA 提供的一套Linux 下的C 语言函数库需要将alsa-lib 移植到开发板上这样基于alsa-lib 编写的应用程序才能成功运行除了移植alsa-lib 库之外通常还需要移植alsa-utilsalsa-utils 包含了一些用于测试、配置声卡的工具。 事实上ALPHA I.MX6U 开发板出厂系统中已经移植了alsa-lib 和alsa-utils本章我们直接使用出厂系统移植好的alsa-lib 和alsa-utils 进行测试笔者也就不再介绍移植过程了。其实它们的移植方法也非常简单如果你想自己尝试移植网上有很多参考大家可以自己去看看。 alsa-utils 提供了一些用于测试、配置声卡的工具譬如aplay、arecord、alsactl、alsaloop、alsamixer、 amixer 等在开发板出厂系统上可以直接使用这些工具这些应用程序也都是基于alsa-lib 编写的。 aplay aplay 是一个用于测试音频播放功能程序可以使用aplay 播放wav 格式的音频文件如下所示 程序运行之后就会开始播放音乐因为ALPHA 开发板支持喇叭和耳机自动切换如果不插耳机默认从喇叭播放音乐插上耳机以后喇叭就会停止播放切换为耳机播放音乐这个大家可以自己进行测试。 需要注意的是aplay 工具只能解析wav 格式音频文件不支持mp3 格式解码所以无法使用aplay 工具播放mp3 音频文件。稍后笔者会向大家介绍如何基于alsa-lib 编写一个简单地音乐播放器实现与aplay 相同的效果。 alsamixer alsamixer 是一个很重要的工具用于配置声卡的混音器它是一个字符图形化的配置工具直接在开发板串口终端运行alsamixer 命令打开图形化配置界面如下所示 alsamixer 可对声卡的混音器进行配置左上角“Card: wm8960-audio”表示当前配置的声卡为wm8960- audio如果你的系统中注册了多个声卡可以按F6 进行选择。 按下H 键可查看界面的操作说明如下所示 不同声卡支持的混音器配置选项是不同的这个与具体硬件相关需要硬件上的支持上图展示的便是开发板WM8960 声卡所支持的配置项包括Playback 播放和Capture 录音左上角View 处提示 View: F3:[Playback] F4: Capture F5: All 表示当前显示的是[Playback]的配置项通过F4 按键切换为Capture、或按F5 显示所有配置项。 Tips在终端按下F4 或F5 按键时可能会直接退出配置界面这个原因可能是F4 或F5 快捷键被其它程序给占用了大家可以试试在Ubuntu 系统下使用ssh 远程登录开发板然后在Ubuntu ssh 终端执行alsamixer 程序笔者测试F4、F5 都是正常的。 左上角Item 处提示 Item: Headphone [dB gain: -8.00, -8.00] 表示当前选择的是Headphone 配置项可通过键盘上的LEFT向左和RIGHT向右按键切换到其它配置项。当用户对配置项进行修改时只能修改被选中的配置项而中括号[dB gain: -7.00, -7.00]中的内容显示了该配置项当前的配置值。 上图中只是列出了其中一部分还有一部分配置项并未显示出来可以通过左右按键移动查看到其余配置项。WM8960 声卡所支持的配置项特别多包括播放音量、耳机音量、喇叭音量、capture 录音音量、通道使能、ZC、AC、DC、ALC、3D 等配置项特别多很多配置项笔者也不懂。以下列出了其中一些配置项及其说明 Headphone耳机音量使用上音量增加、下音量降低按键可以调节播放时耳机输出的音量大小当然可以通过Q左声道音量增加、Z左声道音量降低按键单独调节左声道音量或通过E右声道音量增加、C右声道音量降低按键单独调节右声道音量。 Headphone Playback ZC耳机播放ZC交流通过M 键打开或关闭ZC。 Speaker喇叭播放音量音量调节方法与Headphon 相同。 Speaker AC喇叭ZC通过上下按键可调节大小。 Speaker DC喇叭DC通过上下按键可调节大小。 Speaker Playback ZC喇叭播放ZC通过M 键打开或关闭ZC。 Playback播放音量播放音量作用于喇叭、也能作用于耳机能同时控制喇叭和耳机的输出音量。调节方法与Headphon 相同。 Capture采集音量也就是录音时的音量大小调节方法与Headphon 相同。 其它的配置项就不再介绍了笔者也看不懂后面会用到时再给大家解释 开发板出厂系统中有一个配置文件/var/lib/alsa/asound.state这其实就是WM8960 声卡的配置文件每当开发板启动进入系统时会自动读取该文件加载声卡配置而每次系统关机时又会将声卡当前的配置写入到该文件中进行保存以便下一次启动时加载。加载与保存操作其实是通过alsactl 工具完成的稍后向大家介绍。 alsactl 配置好声卡之后如果直接关机下一次重启之后之前的设置都会消失必须要重新设置所以我们需要对配置进行保存如何保存呢可通过alsactl 工具完成。 使用alsactl 工具可以将当前声卡的配置保存在一个文件中这个文件默认是/var/lib/alsa/asound.state譬如使用alsactl 工具将声卡配置保存在该文件中 alsactl -f /var/lib/alsa/asound.state store-f 选项指定保存在哪一个文件中当然也可以不用指定如果不指定则使用alsactl 默认的配置文件 /var/lib/alsa/asound.statestore 表示保存配置。保存成功以后就会生成/var/lib/alsa/asound.state 这个文件 asound.state 文件中保存了声卡的各种设置信息大家可以打开此文件查看里面的内容如下所示 除了保存配置之外还可以加载配置譬如使用/var/lib/alsa/asound.state 文件中的配置信息来配置声卡可执行如下命令 alsactl -f /var/lib/alsa/asound.state restorerestore 表示加载配置读取/var/lib/alsa/asound.state 文件中的配置信息并对声卡进行设置。关于alsactl 的详细使用方法可以执行alsactl -h进行查看。 开发板出厂系统每次开机启动时便会自动从/var/lib/alsa/asound.state 文件中读取配置信息并配置声卡而每次关机时譬如执行reset 或poweroff 命令又会将声卡当前的配置写入到该文件中进行保存以便下一次启动时加载。其实也就是在系统启动或关机时通过alsactl 工具加载或保存配置。 amixer amixer 工具也是一个声卡配置工具与alsamixer 功能相同区别在于alsamixer 是一个基于字符图形化的配置工具、而amixer 不是图形化配置工具直接使用命令行配置即可详细地用法大家可以执行amixer --help命令查看下面笔者简单地提一下该工具怎么用 执行命令amixer scontrols可以查看到有哪些配置项如下所示 从打印信息可知这里打印出来的配置项与alsamixer 配置界面中所看到的配置项是相同的那如何进去配置呢不同的配置项对应的配置方法配置值或值类型是不一样的可以先使用命令amixer scontents 查看配置项的说明如下所示 amixer scontents“Headphone”配置项用于设置耳机音量音量可调节范围为0-127当前音量为115左右声道都是 115有些设置项是bool 类型只有on 和off 两种状态。 譬如将耳机音量左右声道都设置为100可执行如下命令进行设置 amixer sset Headphone 100,100譬如打开或关闭Headphone Playback ZC amixer sset Headphone Playback ZC off #关闭ZC amixer sset Headphone Playback ZC on #打开ZC以上给大家举了两个例子配置方法还是很简单地 arecord arecord 工具是一个用于录音测试的应用程序这里笔者简单地给大家介绍一下工具的使用方法详细的使用方法大家可以执行arecord --help命令查看帮助信息。譬如使用arecord 录制一段10 秒钟的音频可以执行如下命令 arecord -f cd -d 10 test.wav-f 选项指定音频格式cd 则表示cd 级别音频也就是“16 bit little endian, 44100, stereo”-d 选项指定音频录制时间长度单位是秒test.wav 指定音频数据保存的文件。当录制完成之后会生成test.wav 文件接着我们可以使用aplay 工具播放这一段音频。 以上给大家介绍了alsa-utils 提供的几个测试音频、配置声卡的工具当然本文也只是进行了简单地介绍更加详细的使用方法还需要大家自己查看帮助信息。 编写一个简单地alsa-lib 应用程序 本小节开始我们来学习如何基于alsa-lib 编写音频应用程序alsa-lib 提供的库函数也别多笔者肯定不会全部给大家介绍只介绍基础的使用方法关于更加深入、更加详细的使用方法需要大家自己去研究、学习。 对于alsa-lib 库的使用ALSA 提供了一些参考资料来帮助应用程序开发人员快速上手alsa-lib、基于 alsa-lib 进行应用编程以下笔者给出了链接 https://users.suse.com/~mana/alsa090_howto.html https://www.alsa-project.org/alsa-doc/alsa-lib/examples.html 第一份文档向用户介绍了如何使用alsa-lib 编写简单的音频应用程序包括PCM 播放音频、PCM 录音等笔者也是参考了这份文档来编写本章教程对应初学者建议大家看一看。 第二个链接地址是ALSA 提供的一些示例代码如下所示 点击对应源文件即可查看源代码。 以上便是ALSA 提供的帮助文档以及参考代码链接地址已经给出了大家有兴趣可以看一下。 本小节笔者将向大家介绍如何基于alsa-lib 编写一个简单地音频应用程序譬如播放音乐、录音等但在此之前首先我们需要先来了解一些基本的概念为后面的学习打下一个坚实的基础 一些基本概念 主要是与音频相关的基本概念因为在alsa-lib 应用编程中会涉及到这些概念所以先给大家进行一个简单地介绍。 样本长度Sample 样本是记录音频数据最基本的单元样本长度就是采样位数也称为位深度Bit Depth、Sample Size、 Sample Width。是指计算机在采集和播放声音文件时所使用数字声音信号的二进制位数或者说每个采样样本所包含的位数计算机对每个通道采样量化时数字比特位数通常有8bit、16bit、24bit 等。 声道数channel 分为单声道(Mono)和双声道/立体声(Stereo)。1 表示单声道、2 表示立体声。 帧frame 帧记录了一个声音单元其长度为样本长度与声道数的乘积一段音频数据就是由苦干帧组成的。 把所有声道中的数据加在一起叫做一帧对于单声道一帧 样本长度* 1双声道一帧 样本长度* 2。譬如对于样本长度为16bit 的双声道来说一帧的大小等于16 * 2 / 8 4 个字节。 采样率Sample rate 也叫采样频率是指每秒钟采样次数该次数是针对桢而言。譬如常见的采样率有 8KHz - 电话所用采样率 22.05KHz - FM 调频广播所用采样率 44.1KHz - 音频CD也常用于MPEG-1 音频VCD、SVCD、MP3所用采样率 48KHz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率。 交错模式interleaved 交错模式是一种音频数据的记录方式分为交错模式和非交错模式。在交错模式下数据以连续桢的形式存放即首先记录完桢1 的左声道样本和右声道样本假设为立体声格式再记录桢2 的左声道样本和右声道样本。而在非交错模式下首先记录的是一个周期内所有桢的左声道样本再记录右声道样本数据是以连续通道的方式存储。不过多数情况下我们一般都是使用交错模式。 周期period 周期是音频设备处理读、写数据的单位换句话说也就是音频设备读写数据的单位是周期每一次读或写一个周期的数据一个周期包含若干个帧譬如周期的大小为1024 帧则表示音频设备进行一次读或写操作的数据量大小为1024 帧假设一帧为4 个字节那么也就是1024*44096 个字节数据。 一个周期其实就是两次硬件中断之间的帧数音频设备每处理读或写完一个周期的数据就会产生一个中断所以两个中断之间相差一个周期关于中断的问题稍后再向大家介绍 缓冲区buffer 数据缓冲区一个缓冲区包含若干个周期所以buffer 是由若干个周期所组成的一块空间。下面一张图直观地表示了buffer、period、frame、sample样本长度之间的关系假设一个buffer 包含4 个周期、而一个周包含1024 帧、一帧包含两个样本左、右两个声道 音频设备底层驱动程序使用DMA 来搬运数据这个buffer 中有4 个period每当DMA 搬运完一个 period 的数据就会触发一次中断因此搬运整个buffer 中的数据将产生4 次中断。ALSA 为什么这样做直接把整个buffer 中的数据一次性搬运过去岂不是更快情况并非如此我们没有考虑到一个很重要的问题那就是延迟如果数据缓存区buffer 很大一次传输整个buffer 中的数据可能会导致不可接受的延迟因为一次搬运的数据量越大所花费的时间就越长那么必然会导致数据从传输开始到发出声音以播放为例这个过程所经历的时间就会越长这就是延迟。为了解决这个问题ALSA 把缓存区拆分成多个周期以周期为传输单元进行传输数据。 所以周期不宜设置过大周期过大会导致延迟过高但周期也不能太小周期太小会导致频繁触发中断这样会使得CPU 被频繁中断而无法执行其它的任务使得效率降低所以周期大小要合适在延迟可接受的情况下尽量设置大一些不过这个需要根据实际应用场合而定有些应用场合可能要求低延迟、实时性高但有些应用场合没有这种需求。 数据之间的传输 这里再介绍一下数据之间传输的问题这个问题很重要大家一定要理解这样会更好的帮助我们理解代码、理解代码的逻辑。 ⚫ PCM 播放情况下 在播放情况下buffer 中存放了需要播放的PCM 音频数据由应用程序向buffer 中写入音频数据buffer 中的音频数据由DMA 传输给音频设备进行播放所以应用程序向buffer 写入数据、音频设备从buffer 读取数据这就是buffer 中数据的传输情况。 图28.5.2 中标识有read pointer 和write pointer 指针write pointer 指向当前应用程序写buffer 的位置、 read pointer 指向当前音频设备读buffer 的位置。在数据传输之前播放之前buffer 缓冲区是没有数据的此时write/read pointer 均指向了buffer 的起始位置也就是第一个周期的起始位置如下所示 应用程序向buffer 写入多少帧数据则write pointer 指针向前移动多少帧当应用程序向buffer 中写入一个周期的数据时write pointer 指针将向前移动一个周期接着再写入一个周期指针再向前移动一个周期以此类推当write pointer 移动到buffer 末尾时又会回到buffer 的起始位置以此循环所以由此可知这是一个环形缓冲区。 以上是应用程序写buffer 的一个过程接着再来看看音频设备读buffer播放的过程。在播放开始之前read pointer 指向了buffer 的起始位置也就是第一个周期的起始位置。音频设备每次只播放一个周期的数据读取一个周期每一次都是从read pointer 所指位置开始读取每读取一个周期read pointer 指针向前移动一个周期同样当read pointer 指针移动到buffer 末尾时又会回到buffer 的起始位置以此构成一个循环 应用程序需要向buffer 中写入音频数据音频设备才能读取数据进行播放如果read pointer 所指向的周期并没有填充音频数据则无法播放当buffer 数据满时应用程序将不能再写入数据否则就会覆盖之前的数据必须要等待音频设备播放完一个周期音频设备每播放完一个周期这个周期就变成空闲状态了此时应用程序就可以写入一个周期的数据以填充这个空闲周期。 ⚫ PCM 录音情况下 在录音情况下buffer 中存放了音频设备采集到的音频数据外界模拟声音通过ADC 转为数字声音由音频设备向buffer 中写入音频数据DMA 搬运而应用程序从buffer 中读取数据所以音频设备向 buffer 写入数据、应用程序从buffer 读取数据这就是录音情况下buffer 中数据的传输情况。 回到图28.5.2 中此时write pointer 指向音频设备写buffer 的位置、read pointer 指向应用程序读buffer 的位置。在录音开始之前buffer 缓冲区是没有数据的此时write/read pointer 均指向了buffer 的起始位置也就是第一个周期的起始位置如图28.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 中的数据总是连续地在音频设备和应用程序缓存区间传输如下图所示 上图展示了声卡在工作状态下buffer 中数据的传输情况总是连续地在音频设备和应用程序缓存区间传输但事情并不总是那么完美、也会出现有例外譬如在录音例子中如果应用程序读取数据不够快环形缓冲区buffer 中的数据已经被音频设备写满了、而应用程序还未来得及读走那么数据将会被覆盖这种数据的丢失被称为overrun。在播放例子中如果应用程序写入数据到环形缓冲区buffer 中的速度不够快 缓存区将会“饿死”缓冲区中无数据可播放这样的错误被称为underrun欠载。在ALSA 文档中将这两种情形统称为XRUN适当地设计应用程序可以最小化XRUN 并且可以从中恢复过来。 打开PCM 设备 从本小节开始将正式介绍如何编写一个音频应用程序首先我们需要在应用程序中包含alsa-lib 库的头文件alsa/asoundlib.h这样才能在应用程序中调用alsa-lib 库函数以及使用相关宏。 第一步需要打开PCM 设备调用函数snd_pcm_open()该函数原型如下所示 int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)该函数一共有4 个参数如下所示 ⚫ pcmpsnd_pcm_t 用于描述一个PCM 设备所以一个snd_pcm_t 对象表示一个PCM 设备 snd_pcm_open 函数会打开参数name 所指定的设备实例化snd_pcm_t 对象并将对象的指针也就是PCM 设备的句柄通过pcmp 返回出来。 ⚫ name参数name 指定PCM 设备的名字。alsa-lib 库函数中使用逻辑设备名而不是设备文件名命名方式为hw:i,ji 表示声卡的卡号j 则表示这块声卡上的设备号譬如hw:0,0表示声卡0 上的 PCM 设备0 在播放情况下这其实就对应/dev/snd/pcmC0D0p 如果是录音则对应 /dev/snd/pcmC0D0c。除了使用hw:i,j这种方式命名之外还有其它两种常用的命名方式譬如 “plughw:i,j”、default等关于这些名字的不同本章最后再向大家进行简单地介绍这里暂时先不去理会这个问题。 ⚫ stream参数stream 指定流类型有两种不同类型SND_PCM_STREAM_PLAYBACK 和 SND_PCM_STREAM_CAPTURE SND_PCM_STREAM_PLAYBACK 表示播放 SND_PCM_STREAM_CAPTURE 则表示采集。 ⚫ mode最后一个参数mode 指定了open 模式通常情况下我们会将其设置为0表示默认打开模式默认情况下使用阻塞方式打开设备当然也可将其设置为SND_PCM_NONBLOCK表示以非阻塞方式打开设备。 设备打开成功snd_pcm_open 函数返回0打开失败返回一个小于0 的错误编号可以使用alsa-lib 提供的库函数snd_strerror()来得到对应的错误描述信息该函数与C 库函数strerror()用法相同。 与snd_pcm_open 相对应的是snd_pcm_close()函数snd_pcm_close()用于关闭PCM 设备函数原型如下所示 int snd_pcm_close(snd_pcm_t *pcm);使用示例 调用snd_pcm_open()函数打开声卡0 的PCM 播放设备0 snd_pcm_t *pcm_handle NULL; int ret; ret snd_pcm_open(pcm_handle, hw:0,0, SND_PCM_STREAM_PLAYBACK, 0); if (0 ret) {fprintf(stderr, snd_pcm_open error: %s\n, snd_strerror(ret));return -1; }设置硬件参数 打开PCM 设备之后接着我们需要对设备进行设置包括硬件配置和软件配置。软件配置就不再介绍了使用默认配置即可我们主要是对硬件参数进行配置譬如采样率、声道数、格式、访问类型、period 周期大小、buffer 大小等。 实例化snd_pcm_hw_params_t 对象 alsa-lib 使用snd_pcm_hw_params_t 数据类型来描述PCM 设备的硬件配置参数在配置参数之前我们需要实例化一个snd_pcm_hw_params_t 对象使用snd_pcm_hw_params_malloc 或 snd_pcm_hw_params_alloca()来实例化一个snd_pcm_hw_params_t 对象如下所示 snd_pcm_hw_params_t *hwparams NULL; snd_pcm_hw_params_malloc(hwparams); 或 snd_pcm_hw_params_alloca(hwparams);它们之间的区别也就是C 库函数malloc 和alloca 之间的区别。当然你也可以直接使用malloc()或 alloca() 来分配一个snd_pcm_hw_params_t 对象亦或者直接定义全局变量或栈自动变量。与 snd_pcm_hw_params_malloc/snd_pcm_hw_params_alloca 相对应的是snd_pcm_hw_params_free snd_pcm_hw_params_free()函数用于释放snd_pcm_hw_params_t 对象占用的内存空间。函数原型如下所示 void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj)初始化snd_pcm_hw_params_t 对象 snd_pcm_hw_params_t 对象实例化完成之后接着我们需要对其进行初始化操作调用 snd_pcm_hw_params_any()对snd_pcm_hw_params_t 对象进行初始化操作调用该函数会使用PCM 设备当前的配置参数去初始化snd_pcm_hw_params_t 对象如下所示 snd_pcm_hw_params_any(pcm_handle, hwparams);第一个参数为PCM 设备的句柄第二个参数传入snd_pcm_hw_params_t 对象的指针。 对硬件参数进行设置 alsa-lib 提供了一系列的snd_pcm_hw_params_set_xxx 函数用于设置PCM 设备的硬件参数同样也提供了一系列的snd_pcm_hw_params_get_xxx 函数用于获取硬件参数。 (1)设置access 访问类型snd_pcm_hw_params_set_access() 调用snd_pcm_hw_params_set_access 设置访问类型其函数原型如下所示 int snd_pcm_hw_params_set_access(snd_pcm_t *pcm,snd_pcm_hw_params_t * params,snd_pcm_access_t access )参数access 指定设备的访问类型是一个snd_pcm_access_t 类型常量这是一个枚举类型如下所示 参数access 指定设备的访问类型是一个snd_pcm_access_t 类型常量这是一个枚举类型如下所示 enum snd_pcm_access_t {SND_PCM_ACCESS_MMAP_INTERLEAVED 0, // mmap access with simple interleaved channelsSND_PCM_ACCESS_MMAP_NONINTERLEAVED, // mmap access with simple non interleaved channelsSND_PCM_ACCESS_MMAP_COMPLEX, // mmap access with complex placementSND_PCM_ACCESS_RW_INTERLEAVED, // snd_pcm_readi/snd_pcm_writei accessSND_PCM_ACCESS_RW_NONINTERLEAVED, // snd_pcm_readn/snd_pcm_writen accessSND_PCM_ACCESS_LAST SND_PCM_ACCESS_RW_NONINTERLEAVED };通常将访问类型设置为SND_PCM_ACCESS_RW_INTERLEAVED 交错访问模式通过 snd_pcm_readi/snd_pcm_writei 对PCM 设备进行读/写操作。 函数调用成功返回0失败将返回一个小于0 的错误码可通过snd_strerror()函数获取错误描述信息。 使用示例 ret snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED); if (0 ret) fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));(2)设置数据格式snd_pcm_hw_params_set_format() 调用snd_pcm_hw_params_set_format()函数设置PCM 设备的数据格式函数原型如下所示 int snd_pcm_hw_params_set_format(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,snd_pcm_format_t format )参数format 指定数据格式该参数是一个snd_pcm_format_t 类型常量这是一个枚举类型如下所示 enum snd_pcm_format_t {SND_PCM_FORMAT_UNKNOWN -1,SND_PCM_FORMAT_S8 0,SND_PCM_FORMAT_U8,SND_PCM_FORMAT_S16_LE,SND_PCM_FORMAT_S16_BE,SND_PCM_FORMAT_U16_LE,SND_PCM_FORMAT_U16_BE,SND_PCM_FORMAT_S24_LE,SND_PCM_FORMAT_S24_BE,SND_PCM_FORMAT_U24_LE,SND_PCM_FORMAT_U24_BE,SND_PCM_FORMAT_S32_LE,SND_PCM_FORMAT_S32_BE,SND_PCM_FORMAT_U32_LE,SND_PCM_FORMAT_U32_BE,SND_PCM_FORMAT_FLOAT_LE,SND_PCM_FORMAT_FLOAT_BE,SND_PCM_FORMAT_FLOAT64_LE,SND_PCM_FORMAT_FLOAT64_BE,SND_PCM_FORMAT_IEC958_SUBFRAME_LE,SND_PCM_FORMAT_IEC958_SUBFRAME_BE,SND_PCM_FORMAT_MU_LAW,SND_PCM_FORMAT_A_LAW,SND_PCM_FORMAT_IMA_ADPCM,SND_PCM_FORMAT_MPEG,SND_PCM_FORMAT_GSM,SND_PCM_FORMAT_S20_LE,SND_PCM_FORMAT_S20_BE,SND_PCM_FORMAT_U20_LE,SND_PCM_FORMAT_U20_BE,SND_PCM_FORMAT_SPECIAL 31,SND_PCM_FORMAT_S24_3LE 32,SND_PCM_FORMAT_S24_3BE,SND_PCM_FORMAT_U24_3LE,SND_PCM_FORMAT_U24_3BE,SND_PCM_FORMAT_S20_3LE,SND_PCM_FORMAT_S20_3BE,SND_PCM_FORMAT_U20_3LE,SND_PCM_FORMAT_U20_3BE,SND_PCM_FORMAT_S18_3LE,SND_PCM_FORMAT_S18_3BE,SND_PCM_FORMAT_U18_3LE,SND_PCM_FORMAT_U18_3BE,SND_PCM_FORMAT_G723_24,SND_PCM_FORMAT_G723_24_1B,SND_PCM_FORMAT_G723_40,SND_PCM_FORMAT_G723_40_1B,SND_PCM_FORMAT_DSD_U8,SND_PCM_FORMAT_DSD_U16_LE,SND_PCM_FORMAT_DSD_U32_LE,SND_PCM_FORMAT_DSD_U16_BE,SND_PCM_FORMAT_DSD_U32_BE,SND_PCM_FORMAT_LAST SND_PCM_FORMAT_DSD_U32_BE,SND_PCM_FORMAT_S16 SND_PCM_FORMAT_S16_LE,SND_PCM_FORMAT_U16 SND_PCM_FORMAT_U16_LE,SND_PCM_FORMAT_S24 SND_PCM_FORMAT_S24_LE,SND_PCM_FORMAT_U24 SND_PCM_FORMAT_U24_LE,SND_PCM_FORMAT_S32 SND_PCM_FORMAT_S32_LE,SND_PCM_FORMAT_U32 SND_PCM_FORMAT_U32_LE,SND_PCM_FORMAT_FLOAT SND_PCM_FORMAT_FLOAT_LE,SND_PCM_FORMAT_FLOAT64 SND_PCM_FORMAT_FLOAT64_LE,SND_PCM_FORMAT_IEC958_SUBFRAME SND_PCM_FORMAT_IEC958_SUBFRAME_LE,SND_PCM_FORMAT_S20 SND_PCM_FORMAT_S20_LE,SND_PCM_FORMAT_U20 SND_PCM_FORMAT_U20_LE };用的最多的格式是SND_PCM_FORMAT_S16_LE有符号16 位、小端模式。当然音频设备不一定支持用户所指定的格式在此之前用户可以调用snd_pcm_hw_params_test_format()函数测试PCM 设备是否支持某种格式如下所示 if (snd_pcm_hw_params_test_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE)) { // 返回一个非零值表示不支持该格式 } else { // 返回0 表示支持 }(3)设置声道数snd_pcm_hw_params_set_channels() 调用snd_pcm_hw_params_set_channels()函数设置PCM 设备的声道数函数原型如下所示 int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int val )参数val 指定声道数量val2 表示双声道也就是立体声。函数调用成功返回0失败返回小于0 的错误码。 使用示例 ret snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 2); if (0 ret) fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));(4)设置采样率大小snd_pcm_hw_params_set_rate() 调用snd_pcm_hw_params_set_rate 设置采样率大小其函数原型如下所示 int snd_pcm_hw_params_set_rate(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int val, int dir )参数val 指定采样率大小譬如44100参数dir 用于控制方向若dir-1则实际采样率小于参数val 指定的值dir0 表示实际采样率等于参数valdir1 表示实际采样率大于参数val。 函数调用成功返回0失败将返回小于0 的错误码。 使用示例 ret snd_pcm_hw_params_set_rate(pcm_handle, hwparams, 44100, 0); if (0 ret) fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));(5)设置周期大小snd_pcm_hw_params_set_period_size() 这里说的周期也就是28.5.1 小节中向大家介绍的周期一个周期的大小使用帧来衡量譬如一个周期 1024 帧调用snd_pcm_hw_params_set_period_size()函数设置周期大小其函数原型如下所示 int snd_pcm_hw_params_set_period_size(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,snd_pcm_uframes_t val,int dir )alsa-lib 使用snd_pcm_uframes_t 类型表示帧的数量参数dir 与snd_pcm_hw_params_set_rate()函数的 dir 参数意义相同。 使用示例将周期大小设置为1024 帧 ret snd_pcm_hw_params_set_period_size(pcm_handle, hwparams, 1024, 0); if (0 ret) fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret)); 注意参数val 的单位是帧、而不是字节。 (6)设置buffer 大小snd_pcm_hw_params_set_buffer_size() 调用snd_pcm_hw_params_set_buffer_size()函数设置buffer 的大小其函数原型如下所示 int snd_pcm_hw_params_set_buffer_size(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_uframes_t val )参数val 指定buffer 的大小以帧为单位通常buffer 的大小是周期大小的整数倍譬如16 个周期但函数snd_pcm_hw_params_set_buffer_size()是以帧为单位来表示buffer 的大小所以需要转换一下譬如将buffer 大小设置为16 个周期则参数val 等于16 * 1024假设一个周期为1024 帧16384 帧。 函数调用成功返回0失败返回一个小于0 的错误码。 使用示例 ret snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, 16*1024); if (0 ret) fprintf(stderr, snd_pcm_hw_params_set_buffer_size error: %s\n, snd_strerror(ret));除了snd_pcm_hw_params_set_buffer_size()函数之外我们还可以调用snd_pcm_hw_params_set_periods() 函数设置buffer 大小其函数原型如下所示 int snd_pcm_hw_params_set_periods(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int val, int dir )参数val 指定了buffer 的大小该大小以周期为单位、并不是以帧为单位注意区分 参数dir 与snd_pcm_hw_params_set_rate()函数的dir 参数意义相同。 函数调用成功返回0失败将返回一个小于0 的错误码。 使用示例 ret snd_pcm_hw_params_set_periods(pcm_handle, hwparams, 16, 0); //buffer 大小为16 个周期 if (0 ret) fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));(7)安装/加载硬件配置参数snd_pcm_hw_params() 参数设置完成之后最后调用snd_pcm_hw_params()加载/安装配置、将配置参数写入硬件使其生效其函数原型如下所示 int snd_pcm_hw_params(snd_pcm_t *pcm, snd_pcm_hw_params_t *params)函数调用成功返回0失败将返回一个小于0 的错误码。函数snd_pcm_hw_params()调用之后其内部会自动调用snd_pcm_prepare()函数PCM 设备的状态被更改为SND_PCM_STATE_PREPARED。 设备有多种不同的状态SND_PCM_STATE_PREPARED 为其中一种关于状态的问题后面在向大家介绍。调用snd_pcm_prepare()函数会使得PCM 设备处于SND_PCM_STATE_PREPARED 状态也就是处于一种准备好的状态。 使用示例 ret snd_pcm_hw_params(pcm_handle, hwparams); if (0 ret) fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret)); 读/写数据 接下来就可以进行读/写数据了如果是PCM 播放则调用snd_pcm_writei()函数向播放缓冲区buffer 中写入音频数据如果是PCM 录音则调用snd_pcm_readi()函数从录音缓冲区buffer 中读取数据它们的函数原型如下所示 snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size ) snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size )参数pcm 为PCM 设备的句柄调用snd_pcm_writei()函数将参数buffer应用程序的缓冲区缓冲区中的数据写入到驱动层的播放环形缓冲区buffer 中参数size 指定写入数据的大小以帧为单位通常情况下每次调用snd_pcm_writei()写入一个周期数据。 调用snd_pcm_readi()函数将从驱动层的录音环形缓冲区buffer 中读取数据到参数buffer 指定的缓冲区中应用程序的缓冲区参数size 指定读取数据的大小以帧为单位通常情况下每次调用snd_pcm_readi() 读取一个周期数据。 Tipssnd_pcm_writei/snd_pcm_readi 函数原型中参数buffer 指的是应用程序的缓冲区不要与驱动层的环形缓冲区搞混了 snd_pcm_readi/snd_pcm_writei 调用成功返回实际读取/写入的帧数调用失败将返回一个负数错误码。即使调用成功实际读取/写入的帧数不一定等于参数size 所指定的帧数仅当发生信号或XRUN 时返回的帧数可能会小于参数size。 阻塞与非阻塞 调用snd_pcm_open()打开设备时若指定为阻塞方式则调用snd_pcm_readi/snd_pcm_writei 以阻塞方式进行读/写。对于PCM 录音来说当buffer 缓冲区中无数据可读时调用snd_pcm_readi()函数将会阻塞直到音频设备向buffer 中写入采集到的音频数据同理对于PCM 播放来说当buffer 缓冲区中的数据满时调用snd_pcm_writei()函数将会阻塞直到音频设备从buffer 中读走数据进行播放。 若调用snd_pcm_open()打开设备时指定为非阻塞方式则调用snd_pcm_readi/snd_pcm_writei 以非阻塞方式进行读/写。对于PCM 录音来说当buffer 缓冲区中无数据可读时调用snd_pcm_readi()不会阻塞、而是立即以错误形式返回同理对于PCM 播放来说当buffer 缓冲区中的数据满时调用snd_pcm_writei() 函数也不会阻塞、而是立即以错误形式返回。 snd_pcm_readn 和snd_pcm_writen snd_pcm_readi/snd_pcm_writei 适用于交错模式interleaved读/写数据如果用户设置的访问类型并不是交错模式而是非交错模式non interleaved此时便不可再使用snd_pcm_readi/snd_pcm_writei 进行读写操作了而需要使用snd_pcm_readn 和snd_pcm_writen 进行读写。 示例代码之PCM 播放 通过上小节的一个介绍相信大家对alsa-lib 音频应用编程已经有了基本的认识和理解本小节我们来编写一个简单地音乐播放器可以播放WAV 音频文件代码笔者已经写好了如下所示 本例程源码对应的路径为开发板光盘-11、Linux C 应用编程例程源码-28_alsa-lib-pcm_playback.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_PLAYBACK_DEV hw:0,0 /************************************ WAV 音频文件解析相关数据结构申明 ************************************/ typedef struct WAV_RIFF {char ChunkID[4]; /* RIFF */u_int32_t ChunkSize; /* 从下一个地址开始到文件末尾的总字节数*/char Format[4]; /* WAVE */ } __attribute__((packed)) RIFF_t; typedef struct WAV_FMT {char Subchunk1ID[4]; /* fmt */u_int32_t Subchunk1Size; /* 16 for PCM */u_int16_t AudioFormat; /* PCM 1*/u_int16_t NumChannels; /* Mono 1, Stereo 2, etc. */u_int32_t SampleRate; /* 8000, 44100, etc. */u_int32_t ByteRate; /* SampleRate * NumChannels * BitsPerSample/8 */u_int16_t BlockAlign; /* NumChannels * BitsPerSample/8 */u_int16_t BitsPerSample; /* 8bits, 16bits, etc. */ } __attribute__((packed)) FMT_t; static FMT_t wav_fmt; typedef struct WAV_DATA {char Subchunk2ID[4]; /* data */u_int32_t Subchunk2Size; /* data size */ } __attribute__((packed)) DATA_t; /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static unsigned int buf_bytes; // 应用程序缓冲区的大小字节为单位 static void *buf NULL; // 指向应用程序缓冲区的指针 static int fd -1; // 指向WAV 音频文件的文件描述符 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数设备驱动层buffer 的大小 static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_PLAYBACK_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数驱动层buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}buf_bytes period_size * wav_fmt.BlockAlign; // 变量赋值一个周期的字节大小return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } static int open_wav_file(const char *file) {RIFF_t wav_riff;DATA_t wav_data;int ret;fd open(file, O_RDONLY);if (0 fd){fprintf(stderr, open error: %s: %s\n, file, strerror(errno));return -1;}/* 读取RIFF chunk */ret read(fd, wav_riff, sizeof(RIFF_t));if (sizeof(RIFF_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(RIFF, wav_riff.ChunkID, 4) || // 校验strncmp(WAVE, wav_riff.Format, 4)){fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 读取sub-chunk-fmt */ret read(fd, wav_fmt, sizeof(FMT_t));if (sizeof(FMT_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(fmt , wav_fmt.Subchunk1ID, 4)){ // 校验fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 打印音频文件的信息*/printf(音频文件格式信息\n\n);printf( file name: %s\n, file);printf( Subchunk1Size: %u\n, wav_fmt.Subchunk1Size);printf( AudioFormat: %u\n, wav_fmt.AudioFormat);printf( NumChannels: %u\n, wav_fmt.NumChannels);printf( SampleRate: %u\n, wav_fmt.SampleRate);printf( ByteRate: %u\n, wav_fmt.ByteRate);printf( BlockAlign: %u\n, wav_fmt.BlockAlign);printf( BitsPerSample: %u\n\n, wav_fmt.BitsPerSample);/* sub-chunk-data */if (0 lseek(fd, sizeof(RIFF_t) 8 wav_fmt.Subchunk1Size,SEEK_SET)){perror(lseek error);close(fd);return -1;}while (sizeof(DATA_t) read(fd, wav_data, sizeof(DATA_t))){/* 找到sub-chunk-data */if (!strncmp(data, wav_data.Subchunk2ID, 4)) // 校验return 0;if (0 lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)){perror(lseek error);close(fd);return -1;}}fprintf(stderr, check error: %s\n, file);return -1; } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {int ret;if (2 ! argc){fprintf(stderr, Usage: %s audio_file\n, argv[0]);exit(EXIT_FAILURE);}/* 打开WAV 音频文件*/if (open_wav_file(argv[1]))exit(EXIT_FAILURE);/* 初始化PCM Playback 设备*/if (snd_pcm_init())goto err1;/* 申请读缓冲区*/buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err2;}/* 播放*/for (;;){memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes); // 从音频文件中读取数据if (0 ret) // 如果读取出错或文件读取完毕goto err3;ret snd_pcm_writei(pcm, buf, period_size);if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto err3;}else if (ret period_size){ // 实际写入的帧数小于指定的帧数// 此时我们需要调整下音频文件的读位置// 将读位置向后移动往回移(period_size-ret)*frame_bytes 个字节// frame_bytes 表示一帧的字节大小if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto err3;}}} err3:free(buf); // 释放内存 err2:snd_pcm_close(pcm); // 关闭pcm 设备 err1:close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); } 本应用程序实现可以播放WAV 音频文件关于WAV 文件格式的解析本文档不作说明WAV 文件格式其实非常简单大家自己百度了解。 在main()函数中首先对参数进行了校验执行测试程序需要用户传入一个参数这个参数用于指定一个需要播放的WAV 音频文件。接着调用自定义函数open_wav_file()对WAV 文件进行解析其实也就是对它的头部数据进行校验、解析获取音频格式信息以及音频数据的位置偏移量。 接着调用自定义函数snd_pcm_init()对PCM 设备进行初始化在snd_pcm_init()函数中首先调用alsa-lib 库函数snd_pcm_open()打开PCM 播放设备接着对PCM 设备硬件参数进行设置包括访问类型、数据格式、采样率、声道数、周期大小以及buffer 的大小这些内容前面已经给大家详细介绍过这里不再重述 回到main()函数调用C 库函数malloc()申请分配一个缓冲区用于存放从音频文件中读取出来的音频数据。 一切准备好之后就可以播放音频了在for 循环中首先调用read()函数从音频文件中读取出音频数据每次读取一个周期将读取到的数据存放在buf 指向的缓冲区中接着调用alsa-lib 库函数snd_pcm_writei() 写入数据进行播放。示例程序中调用snd_pcm_open()时使用的是阻塞方式当驱动层环形缓冲区buffer 还未满时调用snd_pcm_writei()并不会阻塞而是会将数据写入到环形缓冲区中、然后返回调用一次 snd_pcm_writei() 写入一个周期数据、调用一次再写入一个周期当环形缓冲区数据满时调用 snd_pcm_writei()会阻塞直到音频设备播放完一个周期、此时会出现一个空闲周期接着snd_pcm_writei() 将数据填充到这个空闲周期后返回。 以上对示例代码进行了一个简单地介绍代码本身非常简单没什么难点代码中注释信息也已经描述地比较清楚了相信大家都可以看懂。需要注意必须要在源码中包含alsa-lib 的头文件alsa/asoundlib.h 编译示例代码 接下来编译上述示例代码编译的方法非常简单按照以前的惯例编译时无非是要指定两个路径alsa-lib 头文件所在路径、alsa-lib 库文件所在路径以及链接库需要链接的库文件名称譬如 ${CC} -o testApp testApp.c -Ixxx -Lyyy -lzzzxxx 表示头文件的路径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。 该目录下有两个目录lib 和usr这两个目录其实就是Linux 系统根目录下的lib 和usr所以lib 目录下存放了一些链接库文件usr 目录下包含了include 和lib 目录分别存放了头文件和链接库文件。 usr/include/alsa 目录下存放了alsa-lib 的头文件如下所示 我们需要包含的头文件asoundlib.h 头文件就在该目录下。 usr/lib 目录下包含了alsa-lib 库文件如下所示 alsa-lib 链接库libasound.so 就在该目录下。那既然找到了alsa-lib 的头文件路径和库文件路径编译应用程序时直接指定这些路径即可。但我们不需要自己手动指定这些路径交叉编译器已经把这些路径添加到它的搜索路径中了使用echo ${CC}查看环境变量CC 的内容如下所示 其中交叉编译器arm-poky-linux-gnueabi-gcc 有一个选–sysroot它指定了一个路径这个路径就是交叉编译工具安装目录下的sysroots/cortexa7hf-neon-poky-linux-gnueabi 目录–sysroot 选项用于设置目标平台的根目录设置了平台根目录之后当编译应用程序时编译器会将根目录下的usr/include 添加到头文件搜索路径中、将根目录下的lib 和usr/lib 添加到库文件搜索路径中。 所以由此可知编译应用程序时我们只需指定链接库即可如下所示 ${CC} -o testApp testApp.c -lasound测试应用程序 将编译得到的可执行文件拷贝到开发板Linux 系统/home/root 目录下并拷贝一个WAV 音频文件到 /home/root 目录下如下所示 接着进行测试在测试之前我们还需要对声卡混音器进行配置当然你也可以不配置因为开发板出厂系统中声卡是已经配置好的。这里我们直接使用amixer 工具进行配置配置如下 #打开耳机播放ZC amixer sset Headphone Playback ZC on #打开喇叭播放ZCamixer sset Speaker Playback ZC onamixer sset Speaker AC 3 amixer sset Speaker DC 3 #音量设置amixer sset Headphone 105,105 // 耳机音量设置amixer sset Playback 230,230 // 播放音量设置amixer sset Speaker 118,118 // 喇叭音量设置 #打开左右声道amixer sset Right Output Mixer PCM on // 打开右声道amixer sset Left Output Mixer PCM on // 打开左声道 由于篇幅有限打印信息不能给大家全部截取出来。声音的大小大家根据情况进行调节。 声卡设置完成之后接着运行测试程序如下所示 程序运行之后对传入的WAV 文件进行解析并将其音频格式信息打印出来。 此时开发板喇叭便会开始播放音乐如果连接了耳机则会通过耳机播放音乐。 示例代码值PCM 录音 本小节我们来编写一个PCM 音频录制录音的测试程序示例代码笔者已经给出如下所示 本例程源码对应的路径为开发板光盘-11、Linux C 应用编程例程源码-28_alsa-lib-pcm_capture.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_CAPTURE_DEV hw:0,0 /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数buffer 的大小 static unsigned int rate 44100; // 采样率 static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_CAPTURE_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, 2);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {unsigned char *buf NULL;unsigned int buf_bytes;int fd -1;int ret;if (2 ! argc){fprintf(stderr, Usage: %s output_file\n, argv[0]);exit(EXIT_FAILURE);}/* 初始化PCM Capture 设备*/if (snd_pcm_init())exit(EXIT_FAILURE);/* 申请读缓冲区*/buf_bytes period_size * 4; // 字节大小 周期大小*帧的字节大小16 位双声道buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err1;}/* 打开一个新建文件*/fd open(argv[1], O_WRONLY | O_CREAT | O_EXCL);if (0 fd){fprintf(stderr, open error: %s: %s\n, argv[1], strerror(errno));goto err2;}/* 录音*/for (;;){// memset(buf, 0x00, buf_bytes); //buf 清零ret snd_pcm_readi(pcm, buf, period_size); // 读取PCM 数据一个周期if (0 ret){fprintf(stderr, snd_pcm_readi error: %s\n, snd_strerror(ret));goto err3;}// snd_pcm_readi 的返回值ret 等于实际读取的帧数* 4 转为字节数ret write(fd, buf, ret * 4); // 将读取到的数据写入文件中if (0 ret)goto err3;} err3:close(fd); // 关闭文件 err2:free(buf); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备exit(EXIT_FAILURE); } 在main()函数中首先对参数进行了校验执行测试程序需要用户传入一个参数指定输出文件因为示例程序中会将录制的音频数据保存到该文件中。 接着调用自定义函数snd_pcm_init()对PCM 设备进行初始化在snd_pcm_init()函数中首先调用alsa-lib 库函数snd_pcm_open()打开PCM 录音设备接着对PCM 设备硬件参数进行设置访问类型设置交错模式SND_PCM_ACCESS_RW_INTERLEAVED、数据格式设置为SND_PCM_FORMAT_S16_LE、采样率设置为44100、双声道、周期大小设置为1024 帧、buffer 大小设置为16 个周期。 回到main()函数调用C 库函数malloc()申请分配一个缓冲区用于存放从驱动层环形缓冲区buffer 读取出来的音频数据。并打开一个新建文件因为使用了O_CREAT | O_EXCL 标志。 一切准备好之后就可以进行音频录制了在for 循环中首先调用alsa-lib 库函数snd_pcm_readi()从环形缓冲区中读取音频设备采集到的音频数据读取出来之后调用write()函数将数据写入到文件中。示例程序中调用snd_pcm_open()时使用的是阻塞方式当环形缓冲区buffer 中有数据可读时调用snd_pcm_readi() 并不会阻塞而是读取出数据、然后返回调用一次snd_pcm_readi()读取一个周期、调用一次再读取一个周期当环形缓冲区为空时调用snd_pcm_readi()会阻塞直到音频设备采集到一个周期数据、此时被阻塞 snd_pcm_readi()调用被唤醒、读取这一个周期然后返回。 编译示例代码 接下来我们编译示例代码如下所示 ${CC} -o testApp testApp.c -lasound测试应用程序 将编译得到的可执行文件拷贝到开发板Linux 系统/home/root 目录下在执行测试程序之前我们需要对声卡进行配置同样使用amixer 工具进行配置如下 amixer sset Capture 58,58 //录制音量大小 amixer sset ADC PCM 200,200 //PCM ADC # 左声道Mixer Boost 管理 amixer sset Left Input Mixer Boost off amixer sset Left Boost Mixer LINPUT1 off amixer sset Left Input Boost Mixer LINPUT1 0 amixer sset Left Boost Mixer LINPUT2 off amixer sset Left Input Boost Mixer LINPUT2 0 amixer sset Left Boost Mixer LINPUT3 off amixer sset Left Input Boost Mixer LINPUT3 0 # 右声道Mixer Boost 管理 amixer sset Right Input Mixer Boost on amixer sset Right Boost Mixer RINPUT1 on amixer sset Right Input Boost Mixer RINPUT1 5 amixer sset Right Boost Mixer RINPUT2 on amixer sset Right Input Boost Mixer RINPUT2 5 amixer sset Right Boost Mixer RINPUT3 off amixer sset Right Input Boost Mixer RINPUT3 0 左右声道的Mixer Boost混音器增强为什么要这样去配置这个与硬件设计有关系我们就不去解释这个了。具体详情可以参考《I.MX6U 嵌入式Linux 驱动开发指南》文档中音频驱动章节的内容。 接下来执行测试程序进行录音如下所示 执行测试程序之后就开始录音了接着我们可以对着底板上的麦MIC说话板载的MIC 如下所示 程序就会把我们说的话录进去如果想要停止录音、只能终止进程按CtrlC 终止应用程序此时在当前目录下会生成cap.wav 音频文件如下所示 生成的文件是一个纯音频数据的文件并不是WAV 格式的文件因为这个文件没有头部信息程序中如果检测到该文件不是WAV 格式文件、会直接退出所以不能直接使用上小节 cap.wav 文件这里要注意当然你可以对上小节的示例代码进行修改也可直接使用aplay 工具播放这段录制的音频如下 aplay -f cd cap.wav图28.5.18 使用aplay 播放录制的音频 如果录制正常使用aplay 播放出来的声音就是我们录制的声音 LINE_IN 测试 除了麦克风之外开发板底板上还有一个LINE_IN 接口也就是线路输入如下图所示 上图中左边的是耳机接口、右边的是LINE_IN 接口支持音频输入我们通过本测试程序对LINE_IN 接口进行测试采集LINE_IN 接口输入的音频。测试时我们使用一根3.5mm 公对公音频线一头连接到手机或者电脑、另外一头连接到LINE_IN 接口上然后手机或电脑端播放音乐那么音频数据就会通过 LINE_IN 接口输入到开发板被我们的应用程序采集录制。 在测试之前我们需要对声卡进行配置如下所示 amixer sset Capture 58,58 //录制音量大小 amixer sset ADC PCM 200,200 //PCM ADC # 左声道Mixer Boost 管理 amixer sset Left Input Mixer Boost off amixer sset Left Boost Mixer LINPUT1 off amixer sset Left Input Boost Mixer LINPUT1 0 amixer sset Left Boost Mixer LINPUT2 on amixer sset Left Input Boost Mixer LINPUT2 5 amixer sset Left Boost Mixer LINPUT3 off amixer sset Left Input Boost Mixer LINPUT3 0 # 右声道Mixer Boost 管理 amixer sset Right Input Mixer Boost on amixer sset Right Boost Mixer RINPUT1 off amixer sset Right Input Boost Mixer RINPUT1 0 amixer sset Right Boost Mixer RINPUT2 off amixer sset Right Input Boost Mixer RINPUT2 0 amixer sset Right Boost Mixer RINPUT3 on amixer sset Right Input Boost Mixer RINPUT3 5 配置好之后就可以进行测试了执行程序之后手机或电脑端播放音乐开发板采集从LINE_IN 接口输入的音频数据测试方式跟MIC 麦克风一样大家自己去测试 使用异步方式 上小节中的示例代码28.5.1 和示例代码28.5.2 都是采用了同步方式进行读写这样会使得应用程序无法做一些其它的事情本小节我们来学习如何使用异步方式读写。 其实使用异步方式读写非常简单只需要注册异步处理函数即可。 snd_async_add_pcm_handler()函数 alsa-lib 提供了snd_async_add_pcm_handler()函数用于注册异步处理函数其实我们只需要通过这个函数注册一个异步处理函数即可其函数原型如下所示 int snd_async_add_pcm_handler(snd_async_handler_t **handler, snd_pcm_t *pcm, snd_async_callback_t callback, void *private_data )调用该函数需要传入4 个参数 ⚫ handler参数snd_async_handler_t 用于描述一个异步处理所以一个snd_async_handler_t 对象表示一个异步处理对象调用snd_async_add_pcm_handler()函数会实例化一个snd_async_handler_t 对象并将对象的指针指针作为异步处理对象的句柄通过*handler 返回出来。 ⚫ pcmpcm 设备的句柄。 ⚫ callback异步处理函数或者叫回调函数snd_async_callback_t 函数指针如下所示 typedef void(*snd_async_callback_t)(snd_async_handler_t *handler)参数handler 也就是异步处理对象的句柄。 ⚫ private_data传递给异步处理函数的私有数据私有数据的数据类型可以由用户自己定义调用 snd_async_add_pcm_handler()函数时参数private_date 指向你的私有数据对象。在异步处理函数中便可以获取到私有数据调用snd_async_handler_get_callback_private()函数即可如下所示 struct my_private_data *data snd_async_handler_get_callback_private(handler);关于snd_async_add_pcm_handler()函数的参数介绍就给大家说这么多。当调用该函数之后用户传入的PCM 设备将会与异步处理对象关联起来在异步处理函数callback 中可以通过异步处理对象的句柄获取到PCM 设备的句柄通过snd_async_handler_get_pcm()获取如下所示 snd_pcm_t *pcm_handle snd_async_handler_get_pcm(handler);实现异步I/O应用程序通常需要完成这三件事情 ⚫ 使能异步I/O ⚫ 设置异步I/O 的所有者 ⚫ 注册信号处理函数譬如SIGIO 信号或其它实时信号。 这是内容在13.3 小节给大家详细介绍过这里不再啰嗦所以由此可知snd_async_add_pcm_handler 函数中已经帮我们完成这些事情。 使用示例 static void snd_playback_async_callback(snd_async_handler_t *handler) {snd_pcm_t *handle snd_async_handler_get_pcm(handler);//获取PCM 句柄...... }int main(void) {......snd_async_handler_t *async_handler NULL;/* 注册异步处理函数*/ret snd_async_add_pcm_handler(async_handler, pcm, snd_playback_async_callback, NULL);if (0 ret)fprintf(stderr, snd_async_add_pcm_handler error: %s\n, snd_strerror(ret));...... } 调用snd_async_add_pcm_handler()注册了异步回调函数snd_playback_async_callback()当环形缓冲区有空闲的周期可填充数据时以播放为例音频设备驱动程序会向应用程序发送信号SIGIO接着应用程序便会跳转到snd_playback_async_callback()函数执行。 而对于录音来说当环形缓冲区中有数据可读时譬如音频设备已经录制了一个周期、并将数据写入到了环形缓冲区驱动程序便会向应用程序发送信号接着应用程序跳转到回调函数执行。 在播放情况下通常我们会先将环形缓冲区填满当音频设备每播放完一个周期就会产生一个空闲周期此时应用程序会接收到信号进而跳转到异步回调函数中执行。 snd_pcm_avail_update()函数 在异步处理函数中我们通常会使用到这个函数在录音情况下应用程序调用snd_pcm_avail_update() 函数用于获取当前可读取的帧数在播放情况下应用程序调用该函数用于获取当前可写入的帧数。换句话说也就是驱动层环形缓冲区中当前有多少帧数据可读取录音或可写入多少帧数据播放环形缓冲区未满时、应用程序才可写入数据。 该函数原型如下所示 snd_pcm_sframes_t snd_pcm_avail_update(snd_pcm_t *pcm);本小节主要给大家介绍这两个函数因为后面的示例代码中会使用到。 PCM 播放示例-异步方式 通过上面的介绍本小节我们来编写一个使用异步方式的PCM 播放示例程序直接基于示例代码28.5.1 进行修改代码笔者已经写好了如下所示 本例程源码对应的路径为开发板光盘-11 、Linux C 应用编程例程源码-28_alsa-lib-pcm_playback_async.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_PLAYBACK_DEV hw:0,0 /************************************ WAV 音频文件解析相关数据结构申明 ************************************/ typedef struct WAV_RIFF {char ChunkID[4]; /* RIFF */u_int32_t ChunkSize; /* 从下一个地址开始到文件末尾的总字节数*/char Format[4]; /* WAVE */ } __attribute__((packed)) RIFF_t; typedef struct WAV_FMT {char Subchunk1ID[4]; /* fmt */u_int32_t Subchunk1Size; /* 16 for PCM */u_int16_t AudioFormat; /* PCM 1*/u_int16_t NumChannels; /* Mono 1, Stereo 2, etc. */u_int32_t SampleRate; /* 8000, 44100, etc. */u_int32_t ByteRate; /* SampleRate * NumChannels * BitsPerSample/8 */u_int16_t BlockAlign; /* NumChannels * BitsPerSample/8 */u_int16_t BitsPerSample; /* 8bits, 16bits, etc. */ } __attribute__((packed)) FMT_t; static FMT_t wav_fmt; typedef struct WAV_DATA {char Subchunk2ID[4]; /* data */u_int32_t Subchunk2Size; /* data size */ } __attribute__((packed)) DATA_t; /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static unsigned int buf_bytes; // 应用程序缓冲区的大小字节为单位 static void *buf NULL; // 指向应用程序缓冲区的指针 static int fd -1; // 指向WAV 音频文件的文件描述符 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数设备驱动层buffer 的大小 /************************************ static 静态函数 ************************************/ static void snd_playback_async_callback(snd_async_handler_t *handler) {snd_pcm_t *handle snd_async_handler_get_pcm(handler); // 获取PCM 句柄snd_pcm_sframes_t avail;int ret;avail snd_pcm_avail_update(handle); // 获取环形缓冲区中有多少帧数据需要填充while (avail period_size){ // 我们一次写入一个周期memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes);if (0 ret)goto out;ret snd_pcm_writei(handle, buf, period_size);if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto out;}else if (ret period_size){ // 实际写入的帧数小于指定的帧数// 此时我们需要调整下音频文件的读位置重新读取没有播放出去的数据// 将读位置向后移动往回移(period_size-ret)*frame_bytes 个字节// frame_bytes 表示一帧的字节大小if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto out;}}avail snd_pcm_avail_update(handle); // 再次获取、更新avail}return; out:snd_pcm_close(handle); // 关闭pcm 设备free(buf);close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); // 退出程序 } static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;snd_async_handler_t *async_handler NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_PLAYBACK_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数驱动层环形缓冲区buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}buf_bytes period_size * wav_fmt.BlockAlign; // 变量赋值一个周期的字节大小/* 注册异步处理函数*/ret snd_async_add_pcm_handler(async_handler, pcm, snd_playback_async_callback, NULL);if (0 ret){fprintf(stderr, snd_async_add_pcm_handler error: %s\n, snd_strerror(ret));goto err1;}return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } static int open_wav_file(const char *file) {RIFF_t wav_riff;DATA_t wav_data;int ret;fd open(file, O_RDONLY);if (0 fd){fprintf(stderr, open error: %s: %s\n, file, strerror(errno));return -1;}/* 读取RIFF chunk */ret read(fd, wav_riff, sizeof(RIFF_t));if (sizeof(RIFF_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(RIFF, wav_riff.ChunkID, 4) || // 校验strncmp(WAVE, wav_riff.Format, 4)){fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 读取sub-chunk-fmt */ret read(fd, wav_fmt, sizeof(FMT_t));if (sizeof(FMT_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(fmt , wav_fmt.Subchunk1ID, 4)){ // 校验fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 打印音频文件的信息*/printf(音频文件格式信息\n\n);printf( file name: %s\n, file);printf( Subchunk1Size: %u\n, wav_fmt.Subchunk1Size);printf( AudioFormat: %u\n, wav_fmt.AudioFormat);printf( NumChannels: %u\n, wav_fmt.NumChannels);printf( SampleRate: %u\n, wav_fmt.SampleRate);printf( ByteRate: %u\n, wav_fmt.ByteRate);printf( BlockAlign: %u\n, wav_fmt.BlockAlign);printf( BitsPerSample: %u\n\n, wav_fmt.BitsPerSample);/* sub-chunk-data */if (0 lseek(fd, sizeof(RIFF_t) 8 wav_fmt.Subchunk1Size,SEEK_SET)){perror(lseek error);close(fd);return -1;}while (sizeof(DATA_t) read(fd, wav_data, sizeof(DATA_t))){/* 找到sub-chunk-data */if (!strncmp(data, wav_data.Subchunk2ID, 4)) // 校验return 0;if (0 lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)){perror(lseek error);close(fd);return -1;}}fprintf(stderr, check error: %s\n, file);return -1; } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {snd_pcm_sframes_t avail;int ret;if (2 ! argc){fprintf(stderr, Usage: %s audio_file\n, argv[0]);exit(EXIT_FAILURE);}/* 打开WAV 音频文件*/if (open_wav_file(argv[1]))exit(EXIT_FAILURE);/* 初始化PCM Playback 设备*/if (snd_pcm_init())goto err1;/* 申请读缓冲区*/buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err2;}/* 播放先将环形缓冲区填满数据*/avail snd_pcm_avail_update(pcm); // 获取环形缓冲区中有多少帧数据需要填充while (avail period_size){ // 我们一次写入一个周期memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes);if (0 ret)goto err3;ret snd_pcm_writei(pcm, buf, period_size); // 向环形缓冲区中写入数据if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto err3;}else if (ret period_size){ // 实际写入的帧数小于指定的帧数// 此时我们需要调整下音频文件的读位置// 将读位置向后移动往回移(period_size-ret)*frame_bytes 个字节// frame_bytes 表示一帧的字节大小if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto err3;}}avail snd_pcm_avail_update(pcm); // 再次获取、更新avail}for (;;){/* 主程序可以做一些其它的事当环形缓冲区有空闲周期需要写入数据时* 音频设备驱动程序会向应用程序发送SIGIO 信号* 接着应用程序跳转到snd_playback_async_callback()函数执行*/// do_something();sleep(1);} err3:free(buf); // 释放内存 err2:snd_pcm_close(pcm); // 关闭pcm 设备 err1:close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); } 在snd_pcm_init() 函数中我们调用了snd_async_add_pcm_handler() 函数注册了异步回调函数 snd_playback_async_callback()当可写入数据时跳转到snd_playback_async_callback()函数去执行。 在异步回调函数中我们首先调用snd_pcm_avail_update()获取当前可写入多少帧数据然后在while() 循环中调用read()读取音频文件的数据、接着调用snd_pcm_writei()向环形缓冲区写入数据每次循环写入一个周期直到把缓冲区写满然后退出回调函数。 回到main()函数中在进入for()死循环之前我们先将环形缓冲区填满执行的代码与回调函数中的代码相同这里就不再说明了 编译示例代码 在Ubuntu 系统下执行命令编译示例代码 ${CC} -o testApp testApp.c -lasound测试应用程序 将上面编译得到的可执行文件拷贝开发板Linux 系统/home/root 目录下然后在开发板上测试大家自己去测 PCM 录音示例-异步方式 本小节编写使用异步方式的PCM 录音的示例程序代码笔者已经写好了如下所示 本例程源码对应的路径为开发板光盘-11 、Linux C 应用编程例程源码-28_alsa-lib-pcm_capture_async.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_CAPTURE_DEV hw:0,0 /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static unsigned int buf_bytes; // 应用层缓冲区的大小字节为单位 static void *buf NULL; // 指向应用层缓冲区的指针 static int fd -1; // 输出文件的文件描述符 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数驱动层环形缓冲区的大小 static unsigned int rate 44100; // 采样率 /************************************ static 静态函数 ************************************/ static void snd_capture_async_callback(snd_async_handler_t *handler) {snd_pcm_t *handle snd_async_handler_get_pcm(handler);snd_pcm_sframes_t avail;int ret;avail snd_pcm_avail_update(handle); // 检查有多少帧数据可读while (avail period_size){ // 每次读取一个周期// memset(buf, 0x00, buf_bytes); //buf 清零ret snd_pcm_readi(handle, buf, period_size); // 读取PCM 数据一个周期if (0 ret){fprintf(stderr, snd_pcm_readi error: %s\n, snd_strerror(ret));goto out;}// snd_pcm_readi 的返回值ret 等于实际读取的帧数* 4 转为字节数ret write(fd, buf, ret * 4); // 将读取到的数据写入文件中if (0 ret)goto out;avail snd_pcm_avail_update(handle); // 再次读取、更新avail}return; out:snd_pcm_close(handle); // 关闭pcm 设备free(buf);close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); // 退出程序 } static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;snd_async_handler_t *async_handler NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_CAPTURE_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, 2);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}/* 注册异步处理函数*/ret snd_async_add_pcm_handler(async_handler, pcm, snd_capture_async_callback, NULL);if (0 ret){fprintf(stderr, snd_async_add_pcm_handler error: %s\n, snd_strerror(ret));goto err1;}return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {int ret;if (2 ! argc){fprintf(stderr, Usage: %s output_file\n, argv[0]);exit(EXIT_FAILURE);}/* 初始化PCM Capture 设备*/if (snd_pcm_init())exit(EXIT_FAILURE);/* 申请读缓冲区*/buf_bytes period_size * 4; // 字节大小 周期大小*帧的字节大小16 位双声道buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err1;}/* 打开一个新建文件*/fd open(argv[1], O_WRONLY | O_CREAT | O_EXCL);if (0 fd){fprintf(stderr, open error: %s: %s\n, argv[1], strerror(errno));goto err2;}/* 录音*/ret snd_pcm_start(pcm); // 开始录音if (0 ret){fprintf(stderr, snd_pcm_start error: %s\n, snd_strerror(ret));goto err3;}for (;;){/* 主程序可以做一些其它的事当环形缓冲区有数据可读时* 音频设备驱动程序会向应用程序发送SIGIO 信号* 接着应用程序跳转到snd_capture_async_callback()函数执行、读取数据*/// do_something();sleep(1);} err3:close(fd); // 关闭文件 err2:free(buf); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备exit(EXIT_FAILURE); } 这份代码基于示例代码28.5.2 改写使用异步方式读取录制的音频数据。 代码不再解释了值得注意的是在main()函数中我们调用了snd_pcm_start()函数这个函数前面没给大家介绍过该函数的作用其实如它命名那般用于启动PCM 设备譬如在录音情况下调用该函数开始录音在播放情况下调用该函数开始播放。 前面的几个示例代码中为啥没有调用该函数呢这个问题我们先留着、稍后再给大家介绍 编译示例代码 执行命令编译示例代码 ${CC} -o testApp testApp.c -lasound测试应用程序 将编译得到的可执行文件拷贝到开发板Linux 系统/home/root 目录下然后进行测试测试方法与示例代码28.5.2 对应的测试程序相同这里不再重述大家自己去测 使用poll()函数 上小节我们使用了异步I/O 方式读写PCM 设备本小节我们来学习如何使用poll I/O 多路复用来实现读写数据。 使用poll I/O 多路复用实现读写 I/O 多路复用是一种高级I/O在第一篇13.2 小节给大家进行了详细地介绍可通过select()或poll()函数来实现I/O 多路复用本小节我们使用poll()函数来实现I/O 多路复用接下来将向大家介绍 获取计数snd_pcm_poll_descriptors_count 该函数用于获取PCM 句柄的轮询描述符计数其函数原型如下所示 int snd_pcm_poll_descriptors_count(snd_pcm_t *pcm); 调用该函数返回PCM 句柄的轮询描述符计数。 分配struct pollfd 对象 为每一个轮询描述符分配一个struct pollfd 对象譬如 struct pollfd *pfds NULL; int count; /* 获取PCM 句柄的轮询描述符计数*/ count snd_pcm_poll_descriptors_count(pcm); if (0 count) {fprintf(stderr, Invalid poll descriptors count\n);return -1; } /* 分配内存*/ pfds calloc(count, sizeof(struct pollfd)); if (NULL pfds) {perror(calloc error);return -1; } 填充struct pollfdsnd_pcm_poll_descriptors 接下来调用snd_pcm_poll_descriptors()函数对struct pollfd 对象进行填充初始化其函数原型如下所示 int snd_pcm_poll_descriptors(snd_pcm_t *pcm,struct pollfd *pfds,unsigned int space );参数space 表示pfds 数组中的元素个数。 /* 填充pfds */ ret snd_pcm_poll_descriptors(pcm, pfds, count); if (0 ret) return -1; pollsnd_pcm_poll_descriptors_revents 一切准备完成之后就可以调用poll()函数来监视PCM 设备是否有数据可读或可写当有数据可读或可写时poll()函数返回此时我们可以调用snd_pcm_poll_descriptors_revents()函数获取文件描述符中返回的事件类型并与poll 的events 标志进行比较以确定是否可读或可写snd_pcm_poll_descriptors_revents() 函数原型如下所示 int snd_pcm_poll_descriptors_revents(snd_pcm_t *pcm,struct pollfd *pfds,unsigned int nfds,unsigned short *revents )参数nfds 表示pfds 数组中元素的个数调用该函数获取文件描述符中返回的事件通过参数revents 返回出来注意不要直接读取struct pollfd 对象中的revents 成员变量因为snd_pcm_poll_descriptors_revents() 函数会对poll()系统调用返回的revents 掩码进行“分解”以纠正语义POLLIN 读取POLLOUT 写入。 使用示例 for (;;) {ret poll(pfds, count, -1); // 调用pollif (0 ret){perror(poll error);return -1;}ret snd_pcm_poll_descriptors_revents(pcm, pfds, count, revents);if (0 ret)return -1;if (revents POLLERR) // 发生I/O 错误return -1;if (revents POLLIN){ // 表示可读取数据// 从PCM 设备读取数据}if (revents POLLOUT){ // 表示可写入数据// 将数据写入PCM 设备} } PCM 播放示例代码 对示例代码28.5.1 进行修改使用poll I/O 多路复用示例代码如下所示 本例程源码对应的路径为开发板光盘-11 、Linux C 应用编程例程源码-28_alsa-lib-pcm_playback_poll.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include poll.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_PLAYBACK_DEV hw:0,0 /************************************ WAV 音频文件解析相关数据结构申明 ************************************/ typedef struct WAV_RIFF {char ChunkID[4]; /* RIFF */u_int32_t ChunkSize; /* 从下一个地址开始到文件末尾的总字节数*/char Format[4]; /* WAVE */ } __attribute__((packed)) RIFF_t; typedef struct WAV_FMT {char Subchunk1ID[4]; /* fmt */u_int32_t Subchunk1Size; /* 16 for PCM */u_int16_t AudioFormat; /* PCM 1*/u_int16_t NumChannels; /* Mono 1, Stereo 2, etc. */u_int32_t SampleRate; /* 8000, 44100, etc. */u_int32_t ByteRate; /* SampleRate * NumChannels * BitsPerSample/8 */u_int16_t BlockAlign; /* NumChannels * BitsPerSample/8 */u_int16_t BitsPerSample; /* 8bits, 16bits, etc. */ } __attribute__((packed)) FMT_t; static FMT_t wav_fmt; typedef struct WAV_DATA {char Subchunk2ID[4]; /* data */u_int32_t Subchunk2Size; /* data size */ } __attribute__((packed)) DATA_t; /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static unsigned int buf_bytes; // 应用程序缓冲区的大小字节为单位 static void *buf NULL; // 指向应用程序缓冲区的指针 static int fd -1; // 指向WAV 音频文件的文件描述符 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数设备驱动层buffer 的大小 static struct pollfd *pfds NULL; static int count; static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_PLAYBACK_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数驱动层buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}buf_bytes period_size * wav_fmt.BlockAlign; // 变量赋值一个周期的字节大小return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } static int open_wav_file(const char *file) {RIFF_t wav_riff;DATA_t wav_data;int ret;fd open(file, O_RDONLY);if (0 fd){fprintf(stderr, open error: %s: %s\n, file, strerror(errno));return -1;}/* 读取RIFF chunk */ret read(fd, wav_riff, sizeof(RIFF_t));if (sizeof(RIFF_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(RIFF, wav_riff.ChunkID, 4) || // 校验strncmp(WAVE, wav_riff.Format, 4)){fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 读取sub-chunk-fmt */ret read(fd, wav_fmt, sizeof(FMT_t));if (sizeof(FMT_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(fmt , wav_fmt.Subchunk1ID, 4)){ // 校验fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 打印音频文件的信息*/printf(音频文件格式信息\n\n);printf( file name: %s\n, file);printf( Subchunk1Size: %u\n, wav_fmt.Subchunk1Size);printf( AudioFormat: %u\n, wav_fmt.AudioFormat);printf( NumChannels: %u\n, wav_fmt.NumChannels);printf( SampleRate: %u\n, wav_fmt.SampleRate);printf( ByteRate: %u\n, wav_fmt.ByteRate);printf( BlockAlign: %u\n, wav_fmt.BlockAlign);printf( BitsPerSample: %u\n\n, wav_fmt.BitsPerSample);/* sub-chunk-data */if (0 lseek(fd, sizeof(RIFF_t) 8 wav_fmt.Subchunk1Size,SEEK_SET)){perror(lseek error);close(fd);return -1;}while (sizeof(DATA_t) read(fd, wav_data, sizeof(DATA_t))){/* 找到sub-chunk-data */if (!strncmp(data, wav_data.Subchunk2ID, 4)) // 校验return 0;if (0 lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)){perror(lseek error);close(fd);return -1;}}fprintf(stderr, check error: %s\n, file);return -1; } static int snd_pcm_poll_init(void) {int ret;/* 获取PCM 句柄的轮询描述符计数*/count snd_pcm_poll_descriptors_count(pcm);if (0 count){fprintf(stderr, Invalid poll descriptors count\n);return -1;}/* 分配内存*/pfds calloc(count, sizeof(struct pollfd));if (NULL pfds){perror(calloc error);return -1;}/* 填充pfds */ret snd_pcm_poll_descriptors(pcm, pfds, count);if (0 ret)return -1;return 0; } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {unsigned short revents;snd_pcm_sframes_t avail;int ret;if (2 ! argc){fprintf(stderr, Usage: %s audio_file\n, argv[0]);exit(EXIT_FAILURE);}/* 打开WAV 音频文件*/if (open_wav_file(argv[1]))exit(EXIT_FAILURE);/* 初始化PCM Playback 设备*/if (snd_pcm_init())goto err1;/* 申请读缓冲区*/buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err2;}/* I/O 多路复用poll 初始化*/if (snd_pcm_poll_init())goto err3;for (;;){ret poll(pfds, count, -1); // 调用pollif (0 ret){perror(poll error);goto err3;}ret snd_pcm_poll_descriptors_revents(pcm, pfds, count, revents);if (0 ret)goto err3;if (revents POLLERR)goto err3;if (revents POLLOUT){ // 可写数据avail snd_pcm_avail_update(pcm); // 获取环形缓冲区中有多少帧数据需要填充while (avail period_size){ // 我们一次写入一个周期memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes);if (0 ret)goto err3;ret snd_pcm_writei(pcm, buf, period_size);if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto err3;}else if (ret period_size){if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto err3;}}avail snd_pcm_avail_update(pcm); // 再次获取、更新avail}}} err3:free(buf); // 释放内存 err2:snd_pcm_close(pcm); // 关闭pcm 设备 err1:close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); } PCM 录音示例代码 对示例代码28.5.2 进行修改使用poll I/O 多路复用示例代码如下所示 本例程源码对应的路径为开发板光盘-11 、Linux C 应用编程例程源码-28_alsa-lib-pcm_capture_poll.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include poll.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_CAPTURE_DEV hw:0,0 /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数buffer 的大小 static unsigned int rate 44100; // 采样率 static struct pollfd *pfds NULL; static int count; static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_CAPTURE_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, 2);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } static int snd_pcm_poll_init(void) {int ret;/* 获取PCM 句柄的轮询描述符计数*/count snd_pcm_poll_descriptors_count(pcm);if (0 count){fprintf(stderr, Invalid poll descriptors count\n);return -1;}/* 分配内存*/pfds calloc(count, sizeof(struct pollfd));if (NULL pfds){perror(calloc error);return -1;}/* 填充pfds */ret snd_pcm_poll_descriptors(pcm, pfds, count);if (0 ret)return -1;return 0; } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {unsigned char *buf NULL;unsigned int buf_bytes;unsigned short revents;snd_pcm_sframes_t avail;int fd -1;int ret;if (2 ! argc){fprintf(stderr, Usage: %s output_file\n, argv[0]);exit(EXIT_FAILURE);}/* 初始化PCM Capture 设备*/if (snd_pcm_init())exit(EXIT_FAILURE);/* 申请读缓冲区*/buf_bytes period_size * 4; // 字节大小 周期大小*帧的字节大小16 位双声道buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err1;}/* 打开一个新建文件*/fd open(argv[1], O_WRONLY | O_CREAT | O_EXCL);if (0 fd){fprintf(stderr, open error: %s: %s\n, argv[1], strerror(errno));goto err2;}/* I/O 多路复用poll 初始化*/if (snd_pcm_poll_init())goto err3;/* 开始录音*/ret snd_pcm_start(pcm);if (0 ret){fprintf(stderr, snd_pcm_start error: %s\n, snd_strerror(ret));goto err3;}for (;;){ret poll(pfds, count, -1); // 调用pollif (0 ret){perror(poll error);goto err3;}ret snd_pcm_poll_descriptors_revents(pcm, pfds, count, revents);if (0 ret)goto err3;if (revents POLLERR)goto err3;if (revents POLLIN){ // 可读数据avail snd_pcm_avail_update(pcm); // 检查有多少帧数据可读while (avail period_size){ // 每次读取一个周期ret snd_pcm_readi(pcm, buf, period_size); // 读取PCM 数据一个周期if (0 ret){fprintf(stderr, snd_pcm_readi error: %s\n, snd_strerror(ret));goto err3;}ret write(fd, buf, ret * 4); // 将读取到的数据写入文件中if (0 ret)goto err3;avail snd_pcm_avail_update(pcm); // 再次读取、更新avail}}} err3:close(fd); // 关闭文件 err2:free(buf); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备exit(EXIT_FAILURE); } PCM 设备的状态 本小节向大家介绍PCM 设备的状态有哪些alsa-lib 提供了函数snd_pcm_state()用于获取PCM 设备当前的状态其函数原型如下所示 snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm);可以看到它的返回值是一个snd_pcm_state_t 类型的变量snd_pcm_state_t 其实是一个枚举类型描述了PCM 设备包含的所有状态如下所示 enum snd_pcm_state_t {SND_PCM_STATE_OPEN 0,SND_PCM_STATE_SETUP,SND_PCM_STATE_PREPARED,SND_PCM_STATE_RUNNING,SND_PCM_STATE_XRUN,SND_PCM_STATE_DRAINING,SND_PCM_STATE_PAUSED,SND_PCM_STATE_SUSPENDED,SND_PCM_STATE_DISCONNECTED,SND_PCM_STATE_LAST SND_PCM_STATE_DISCONNECTED,SND_PCM_STATE_PRIVATE1 1024 } SND_PCM_STATE_OPEN 该状态表示PCM 设备处于打开状态譬如当调用snd_pcm_open()后PCM 设备就处于该状态。 SND_PCM_STATE_SETUP alsa-lib 文档中的解释为“Setup installed”该状态表示设备已经初始化完成了参数已经配置好了。 SND_PCM_STATE_PREPARED 该状态表示设备已经准备好了可以开始了“Ready to start”譬如可以开始播放了、可以开始录音了。 前面提到了这个状态当应用程序调用snd_pcm_hw_params() 函数之后设备就处于 SND_PCM_STATE_PREPARED 状态了。应用程序中可以调用snd_pcm_prepare()函数使设备处于 SND_PCM_STATE_PREPARED 状态该函数原型如下所示 int snd_pcm_prepare(snd_pcm_t *pcm);该行数调用成功返回0失败将返回一个负数错误码。 函数调用成功PCM 设备将处于SND_PCM_STATE_PREPARED 状态。事实上应用程序调用时 snd_pcm_hw_params()时函数内部会自动调用snd_pcm_prepare()所以为什么调用snd_pcm_hw_params() 之后设备就已经处于SND_PCM_STATE_PREPARED 状态了调用snd_pcm_hw_params()函数其实应该发生了两种状态的转变为首先由SND_PCM_STATE_OPEN 变为SND_PCM_STATE_SETUP 状态、再由 SND_PCM_STATE_SETUP 变为SND_PCM_STATE_PREPARED 状态。 SND_PCM_STATE_RUNNING 该状态表示设备正在运行譬如正在播放、正在录音。 上小节我们提到应用程序可以调用snd_pcm_start()函数以启动PCM 设备启动成功之后设备开始播放或采集此时设备处于SND_PCM_STATE_RUNNING 状态。 此外当设备处于SND_PCM_STATE_PREPARED 状态时应用程序调用snd_pcm_readi/snd_pcm_writei 进行读写数据时这些函数内部会自动调用snd_pcm_start()函数譬如播放模式下调用snd_pcm_writei 写入数据后会自动开启PCM 设备进行播放这里要注意一定是在数据写入到环形缓冲区之后、才开启PCM 设备播放音频因为一旦开启之后环形缓冲区中必须要有至少一个周期的数据可供音频设备播放否则将会发生欠载underrun、函数调用以错误形式返回在录音模式下调用snd_pcm_readi()函数后自动开启PCM 进行音频采集。 所以这就是为什么示例代码28.5.1、示例代码28.5.2、示例代码28.6.1 这几个示例中并没有调用 snd_pcm_start()函数的原因。 当设备处于运行状态时应用程序可调用snd_pcm_drop()或snd_pcm_drain()函数使设备停止运行譬如停止播放、停止音频采集它们的函数原型如下所示 int snd_pcm_drain(snd_pcm_t *pcm); int snd_pcm_drop(snd_pcm_t *pcm);函数调用成功返回0失败返回负值错误码。 这两个函数都可使设备停止运行它们的区别如下 ⚫ snd_pcm_drop()函数将立即停止PCM丢弃挂起的帧 ⚫ snd_pcm_drain()函数并不会立即停止PCM而是处理完挂起的帧之后再停止PCM对于播放会等待所有待播放的帧播放完毕应该就是环形缓冲区中的待播放数据然后停止PCM对于录音停止PCM 之前会检索残留帧。 当调用snd_pcm_drop()或snd_pcm_drain()停止PCM 设备后设备将回到SND_PCM_STATE_SETUP 状态。 SND_PCM_STATE_XRUN 当发生XRUN 时设备会处于SND_PCM_STATE_XRUN 状态XRUN 前面给大家解释过了这里不再重述当处于SND_PCM_STATE_XRUN 状态时应用程序可以调用snd_pcm_prepare()使设备恢复使其回到SND_PCM_STATE_PREPARED 状态。 SND_PCM_STATE_DRAINING 这个状态笔者没弄清楚alsa-lib 文档中的解释为“Draining: running (playback) or stopped (capture)”。 SND_PCM_STATE_PAUSED pause 就是暂停的意思所以该状态表示设备处于暂停状态。譬如当设备正在运行时也就是处于 SND_PCM_STATE_RUNNING 状态应用程序调用snd_pcm_pause()函数可让设备暂停其函数原型如下所示 int snd_pcm_pause(snd_pcm_t *pcm, int enable);函数snd_pcm_pause() 既可以使的设备暂停、同样也可使其恢复从暂停恢复运行即 SND_PCM_STATE_RUNNING—SND_PCM_STATE_RUNNING通过参数enable 控制当enable 等于 1表示使设备暂停enable 等于0 表示使设备恢复运行。 snd_pcm_pause()函数调用成功返回0失败返回一个负值错误码。 这里有个问题需要注意并不是所有的音频设备硬件上支持暂停的功能可以通过 snd_pcm_hw_params_can_pause()函数来判断设备是否支持暂停其函数原型如下所示 int snd_pcm_hw_params_can_pause(const snd_pcm_hw_params_t *params);函数返回1 表示硬件支持暂停返回0 表示硬件不支持暂停。 SND_PCM_STATE_SUSPENDED 该状态表示硬件已经挂起suspended如果硬件发生了挂起应用程序可以调用snd_pcm_resume()函数从挂起中恢复并确保不会丢失样本数据精细恢复。snd_pcm_resume()函数原型如下所示 int snd_pcm_resume(snd_pcm_t *pcm);函数调用成功返回0失败返回一个负值错误码。 当然并非所有硬件都支持此功能可以调用snd_pcm_hw_params_can_resume()函数判断硬件是否支持从挂起中恢复其函数原型如下所示 int snd_pcm_hw_params_can_resume(const snd_pcm_hw_params_t *params);函数调用返回1 表示支持返回0 表示不支持。 SND_PCM_STATE_DISCONNECTED 该状态表示硬件已经断开连接。 状态之间的转换 通过上面的介绍我们已经知道了PCM 设备的几种不同的状态、以及它们的一个转换关系为了能够加深大家的印象笔者对其进行了整理主要整理了SND_PCM_STATE_OPEN、SND_PCM_STATE_SETUP、 SND_PCM_STATE_PREPARED 、SND_PCM_STATE_RUNNING 、SND_PCM_STATE_XRUN 以及 SND_PCM_STATE_PAUSED 这6 种状态之间的转换关系如下图所示 笔者尽力了这图画的还是有点乱不过没关系状态转换还是描述清楚了。其实这个状态之间的转换关系不难理解哪种状态能转哪种状态、哪种状态不能转哪种状态这个还是很容易理解的。这里笔者就不再多说了。 PCM 播放示例代码-加入状态控制 通过上面的介绍我们已经知道了PCM 设备不同状态之间转换譬如播放音乐时如何暂停、如何停止、又如何恢复。本小节我们来编写一个PCM 播放程序在示例代码28.6.1 的基础上加入对播放过程的控制譬如用户按下空格键可以暂停播放、再次按下空格则恢复播放。 示例代码笔者已经写好如下所示。 本例程源码对应的路径为开发板光盘-11 、Linux C 应用编程例程源码-28_alsa-lib-pcm_playback_ctl.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include termios.h #include signal.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_PLAYBACK_DEV hw:0,0 /************************************ WAV 音频文件解析相关数据结构申明 ************************************/ typedef struct WAV_RIFF {char ChunkID[4]; /* RIFF */u_int32_t ChunkSize; /* 从下一个地址开始到文件末尾的总字节数*/char Format[4]; /* WAVE */ } __attribute__((packed)) RIFF_t; typedef struct WAV_FMT {char Subchunk1ID[4]; /* fmt */u_int32_t Subchunk1Size; /* 16 for PCM */u_int16_t AudioFormat; /* PCM 1*/u_int16_t NumChannels; /* Mono 1, Stereo 2, etc. */u_int32_t SampleRate; /* 8000, 44100, etc. */u_int32_t ByteRate; /* SampleRate * NumChannels * BitsPerSample/8 */u_int16_t BlockAlign; /* NumChannels * BitsPerSample/8 */u_int16_t BitsPerSample; /* 8bits, 16bits, etc. */ } __attribute__((packed)) FMT_t; static FMT_t wav_fmt; typedef struct WAV_DATA {char Subchunk2ID[4]; /* data */u_int32_t Subchunk2Size; /* data size */ } __attribute__((packed)) DATA_t; /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static unsigned int buf_bytes; // 应用程序缓冲区的大小字节为单位 static void *buf NULL; // 指向应用程序缓冲区的指针 static int fd -1; // 指向WAV 音频文件的文件描述符 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数设备驱动层buffer 的大小 static struct termios old_cfg; // 用于保存终端当前的配置参数 /************************************ static 静态函数 ************************************/ static void snd_playback_async_callback(snd_async_handler_t *handler) {snd_pcm_t *handle snd_async_handler_get_pcm(handler); // 获取PCM 句柄snd_pcm_sframes_t avail;int ret;avail snd_pcm_avail_update(handle); // 获取环形缓冲区中有多少帧数据需要填充while (avail period_size){ // 我们一次写入一个周期memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes);if (0 ret)goto out;ret snd_pcm_writei(handle, buf, period_size);if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto out;}else if (ret period_size){ // 实际写入的帧数小于指定的帧数// 此时我们需要调整下音频文件的读位置// 将读位置向后移动往回移(period_size-ret)*frame_bytes 个字节// frame_bytes 表示一帧的字节大小if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto out;}}avail snd_pcm_avail_update(handle); // 再次获取、更新avail}return; out:snd_pcm_drain(pcm); // 停止PCMsnd_pcm_close(handle); // 关闭pcm 设备tcsetattr(STDIN_FILENO, TCSANOW, old_cfg); // 退出前恢复终端的状态free(buf);close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); // 退出程序 } static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;snd_async_handler_t *async_handler NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_PLAYBACK_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数驱动层环形缓冲区buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}buf_bytes period_size * wav_fmt.BlockAlign; // 变量赋值一个周期的字节大小/* 注册异步处理函数*/ret snd_async_add_pcm_handler(async_handler, pcm, snd_playback_async_callback, NULL);if (0 ret){fprintf(stderr, snd_async_add_pcm_handler error: %s\n, snd_strerror(ret));goto err1;}return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } static int open_wav_file(const char *file) {RIFF_t wav_riff;DATA_t wav_data;int ret;fd open(file, O_RDONLY);if (0 fd){fprintf(stderr, open error: %s: %s\n, file, strerror(errno));return -1;}/* 读取RIFF chunk */ret read(fd, wav_riff, sizeof(RIFF_t));if (sizeof(RIFF_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(RIFF, wav_riff.ChunkID, 4) || // 校验strncmp(WAVE, wav_riff.Format, 4)){fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 读取sub-chunk-fmt */ret read(fd, wav_fmt, sizeof(FMT_t));if (sizeof(FMT_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(fmt , wav_fmt.Subchunk1ID, 4)){ // 校验fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 打印音频文件的信息*/printf(音频文件格式信息\n\n);printf( file name: %s\n, file);printf( Subchunk1Size: %u\n, wav_fmt.Subchunk1Size);printf( AudioFormat: %u\n, wav_fmt.AudioFormat);printf( NumChannels: %u\n, wav_fmt.NumChannels);printf( SampleRate: %u\n, wav_fmt.SampleRate);printf( ByteRate: %u\n, wav_fmt.ByteRate);printf( BlockAlign: %u\n, wav_fmt.BlockAlign);printf( BitsPerSample: %u\n\n, wav_fmt.BitsPerSample);/* sub-chunk-data */if (0 lseek(fd, sizeof(RIFF_t) 8 wav_fmt.Subchunk1Size,SEEK_SET)){perror(lseek error);close(fd);return -1;}while (sizeof(DATA_t) read(fd, wav_data, sizeof(DATA_t))){/* 找到sub-chunk-data */if (!strncmp(data, wav_data.Subchunk2ID, 4)) // 校验return 0;if (0 lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)){perror(lseek error);close(fd);return -1;}}fprintf(stderr, check error: %s\n, file);return -1; } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {snd_pcm_sframes_t avail;struct termios new_cfg;sigset_t sset;int ret;if (2 ! argc){fprintf(stderr, Usage: %s audio_file\n, argv[0]);exit(EXIT_FAILURE);}/* 屏蔽SIGIO 信号*/sigemptyset(sset);sigaddset(sset, SIGIO);sigprocmask(SIG_BLOCK, sset, NULL);/* 打开WAV 音频文件*/if (open_wav_file(argv[1]))exit(EXIT_FAILURE);/* 初始化PCM Playback 设备*/if (snd_pcm_init())goto err1;/* 申请读缓冲区*/buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err2;}/* 终端配置*/tcgetattr(STDIN_FILENO, old_cfg); // 获取终端标准输入-标准输出构成了一套终端memcpy(new_cfg, old_cfg, sizeof(struct termios)); // 备份new_cfg.c_lflag ~ICANON; // 将终端设置为非规范模式new_cfg.c_lflag ~ECHO; // 禁用回显tcsetattr(STDIN_FILENO, TCSANOW, new_cfg); // 使配置生效/* 播放先将环形缓冲区填满数据*/avail snd_pcm_avail_update(pcm); // 获取环形缓冲区中有多少帧数据需要填充while (avail period_size){ // 我们一次写入一个周期memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes);if (0 ret)goto err3;ret snd_pcm_writei(pcm, buf, period_size); // 向环形缓冲区中写入数据if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto err3;}else if (ret period_size){ // 实际写入的帧数小于指定的帧数// 此时我们需要调整下音频文件的读位置// 将读位置向后移动往回移(period_size-ret)*frame_bytes 个字节// frame_bytes 表示一帧的字节大小if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto err3;}}avail snd_pcm_avail_update(pcm); // 再次获取、更新avail}sigprocmask(SIG_UNBLOCK, sset, NULL); // 取消SIGIO 信号屏蔽char ch;for (;;){ch getchar(); // 获取用户输入的控制字符switch (ch){case q: // Q 键退出程序sigprocmask(SIG_BLOCK, sset, NULL); // 屏蔽SIGIO 信号goto err3;case : // 空格暂停/恢复switch (snd_pcm_state(pcm)){case SND_PCM_STATE_PAUSED: // 如果是暂停状态则恢复运行ret snd_pcm_pause(pcm, 0);if (0 ret)fprintf(stderr, snd_pcm_pause error: %s\n, snd_strerror(ret));break;case SND_PCM_STATE_RUNNING: // 如果是运行状态则暂停ret snd_pcm_pause(pcm, 1);if (0 ret)fprintf(stderr, snd_pcm_pause error: %s\n, snd_strerror(ret));break;}break;}} err3:snd_pcm_drop(pcm); // 停止PCMtcsetattr(STDIN_FILENO, TCSANOW, old_cfg); // 退出前恢复终端的状态free(buf); // 释放内存 err2:snd_pcm_close(pcm); // 关闭pcm 设备 err1:close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); } 上述示例程序是在示例代码28.6.1 基础上进行修改了加入了用户控制单元程序设定 ⚫ q在终端按下q 键退出应用程序 ⚫ 终端按下空格键暂停播放再次按下恢复播放。 下面给大家简单地介绍下上述示例代码的设计在main()函数中我们首先屏蔽了SIGIO 信号如下 /* 屏蔽SIGIO 信号*/ sigemptyset(sset); sigaddset(sset, SIGIO); sigprocmask(SIG_BLOCK, sset, NULL);这主要是为了程序设计上的安全考虑等把环形缓冲区填满数据之后再取消SIGIO 信号屏蔽。当然你也可以不这样做。 接着打开用户传入的音频文件、初始化PCM 播放设备、申请应用程序所需的缓冲区 /* 打开WAV 音频文件*/ if (open_wav_file(argv[1]))exit(EXIT_FAILURE); /* 初始化PCM Playback 设备*/ if (snd_pcm_init())goto err1; /* 申请读缓冲区*/ buf malloc(buf_bytes); if (NULL buf) {perror(malloc error);goto err2; } 接着对终端进行设置将终端配置为非规范模式、取消回显配置为非规范模式之后用户输入的字符会直接被应用程序读取到而无需按下回车键取消回显意味着用户输入的字符在终端不会显示出来这些内容在串口应用编程章节给大家详细介绍过这里就不再啰嗦 /* 终端配置*/ tcgetattr(STDIN_FILENO, old_cfg); //获取终端标准输入-标准输出构成了一套终端 memcpy(new_cfg, old_cfg, sizeof(struct termios));//备份 new_cfg.c_lflag ~ICANON; //将终端设置为非规范模式 new_cfg.c_lflag ~ECHO; //禁用回显 tcsetattr(STDIN_FILENO, TCSANOW, new_cfg);//使配置生效接下来将数据写入环形缓冲区开始播放。 取消SIGIO 信号信号屏蔽。 最后进入for()循环中通过getchar()读取用户输入的字符用户输入q 时退出程序这里需要注意退出程序时需要调用tcsetattr()将终端配置参数恢复到之前的状态否则你的终端将可能会出现下面这种情况 这个时候你就只能重启了。 用户输入空格暂停或恢复调用snd_pcm_pause()实现暂停/恢复。 代码比较简单笔者也就不再多说了 编译示例代码 执行命令编译应用程序 ${CC} -o testApp testApp.c -lasound测试应用程序 将编译得到的可执行文件拷贝到开发板Linux 系统/home/root 目录下并准备一个WAV 音频文件接着我们执行测试程序 运行之后开始播放音乐此时我们可以通过空格键来暂停播放、再按空格键恢复播放按q 键退出程序大家自己去测试。 Tips本测试程序不能放在后台运行一旦放入后台程序将停止不是终止、是暂停运行因为这个程序在设计逻辑上就不符合放置在后台因为程序中会读取用户从终端标准输入输入的字符如果放入后台那用户输入的字符就不可能被该程序所读取到这是其一其二程序中修改了终端的配置。 snd_pcm_readi/snd_pcm_writei 错误处理 当snd_pcm_readi/snd_pcm_writei 调用出错时会返回一个小于0负值的错误码可调用snd_strerror() 函数获取对应的错误描述信息。前面的示例代码中我们并没有对snd_pcm_readi/snd_pcm_writei 的错误返回做过多、细节的处理而是简单地在出错之后退出。 事实上当调用snd_pcm_readi/snd_pcm_writei 出错时可根据不同的情况作进一步的处理在alsa-lib 文档中有介绍到snd_pcm_readi/snd_pcm_writei 函数的不同错误返回值表示不同的含义如下所示 snd_pcm_readi()函数与它相同。 当返回值等于-EBADFD表示PCM 设备的状态不对因为执行snd_pcm_readi/snd_pcm_writei 读取/写入数据需要PCM 设备处于SND_PCM_STATE_PREPARED 或SND_PCM_STATE_RUNNING 状态前面已经详细地给大家介绍了PCM 设备的状态间转换问题。 当返回值等于-EPIPE表示发生了XRUN此时可以怎么做呢这个可以根据自己的实际需要进行处理譬如调用snd_pcm_drop()停止PCM 设备或者调用snd_pcm_prepare()使设备恢复进入准备状态。 当返回值等于-ESTRPIPE表示硬件发生了挂起此时PCM 设备处于SND_PCM_STATE_SUSPENDED 状态譬如你可以调用snd_pcm_resume() 函数从挂起中精确恢复如果硬件不支持还可调用 snd_pcm_prepare()函数使设备进入准备状态或者执行其它的处理根据应用需求的进行相应的处理。 以上给大家介绍了调用snd_pcm_readi/snd_pcm_writei 函数出错时的一些情况以及可以采取的一些措施 混音器设置 前面给大家介绍了alsa-utils 提供的两个声卡配置工具alsamixer 和amixer。这两个工具同样是基于alsa-lib 库函数编写的本小节我们来学习如何在自己的应用程序中通过调用alsa-lib 库函数对声卡混音器进行配置譬如音量调节。 混音器相关的接口在alsa-lib 的Mixer Interface 模块中有介绍点击图28.2.2 中“Mixer Interface”可查看混音器相关接口的介绍如下所示 大家可以简单地浏览下该模块下提供了那些函数点击函数名可以查看该函数的简单介绍信息。 打开混音器snd_mixer_open 在使用混音器之后需要打开混音器调用snd_mixer_open()函数打开一个空的混音器其函数原型如下所示 int snd_mixer_open(snd_mixer_t **mixerp, int mode); alsa-lib 使用snd_mixer_t 数据结构描述混音器调用snd_mixer_open()函数会实例化一个snd_mixer_t 对象并将对象的指针也就是混音器的句柄通过mixerp 返回出来。参数mode 指定了打开模式通常设置为0 使用默认模式即可 函数调用成功返回0失败返回一个小于0 的错误码。 使用示例 snd_mixer_t *mixer NULL; int ret; ret snd_mixer_open(mixer, 0); if (0 ret)fprintf(stderr, snd_mixer_open error: %s\n, snd_strerror(ret)); Attach 关联设备snd_mixer_attach 调用snd_mixer_open()函数打开并实例化了一个空的混音器接下来我们要去关联声卡控制设备调用 snd_mixer_attach()函数进行关联其函数原型如下所示 int snd_mixer_attach(snd_mixer_t *mixer, const char *name); 参数mixer 对应的是混音器的句柄参数name 指定了声卡控制设备的名字同样这里使用的也是逻辑设备名而非设备节点的名字命名方式为hw:ii 表示声卡的卡号通常一个声卡对应一个控制设备譬如hw:0表示声卡0 的控制设备这其实就对应/dev/snd/controlC0 设备。与snd_pcm_open()函数中PCM 设备的命名一样snd_mixer_attach()函数中声卡控制设备的命名也有其它方式这里暂时先不管这个问题。 调用snd_mixer_open()函数会将参数name 所指定的控制设备与混音器mixer 进行关联。 函数调用成功返回0失败返回一个小于0 的错误码。 使用示例 ret snd_mixer_attach(mixer, hw:0); if (0 ret)fprintf(stderr, snd_mixer_attach error: %s\n, snd_strerror(ret));注册snd_mixer_selem_register 调用snd_mixer_selem_register()函数注册混音器其函数原型如下所示 int snd_mixer_selem_register(snd_mixer_t *mixer,struct snd_mixer_selem_regopt *options,snd_mixer_class_t **classp);参数options 和参数classp 直接设置为NULL 即可。 函数调用成功返回0失败返回一个小于0 的错误码。 使用示例 ret snd_mixer_selem_register(mixer, NULL, NULL); if (0 ret) fprintf(stderr, snd_mixer_selem_register error: %s\n, snd_strerror(ret));加载snd_mixer_load 最后需要加载混音器调用snd_mixer_load()函数完成加载函数原型如下所示 int snd_mixer_load(snd_mixer_t * mixer); 函数调用成功返回0失败返回小于0 的错误码。 使用示例 ret snd_mixer_load(mixer); if (0 ret)fprintf(stderr, snd_mixer_load error: %s\n, snd_strerror(ret));查找元素 经过上面一系列步骤之后接下来就可以使用混音器了alsa-lib 中把混音器的配置项称为元素element譬如耳机音量调节Headphone 是一个元素、Headphone Playback ZC’是一个元素、‘Right Output Mixer PCM’ 也是一个元素。 snd_mixer_first_elem 和snd_mixer_last_elem alsa-lib 使用数据结构snd_mixer_elem_t 来描述一个元素所以一个snd_mixer_elem_t 对象就是一个元素。混音器有很多的元素也就是有很多配置项通过snd_mixer_first_elem()函数可以找到混音器的第一个元素其函数原型如下所示 snd_mixer_elem_t *snd_mixer_first_elem(snd_mixer_t *mixer);通过snd_mixer_last_elem()函数可找到混音器的最后一个元素如下 snd_mixer_elem_t *snd_mixer_last_elem(snd_mixer_t *mixer);snd_mixer_elem_next 和snd_mixer_elem_prev 调用snd_mixer_elem_next()和snd_mixer_elem_prev()函数可获取指定元素的下一个元素和上一个元素 snd_mixer_elem_t *snd_mixer_elem_next(snd_mixer_elem_t *elem); snd_mixer_elem_t *snd_mixer_elem_prev(snd_mixer_elem_t *elem);所以通过snd_mixer_first_elem 和snd_mixer_elem_next() 或者snd_mixer_last_elem() 和 snd_mixer_elem_prev()就可以遍历整个混音器中的所有元素如下所示 snd_mixer_elem_t *elem NULL;elem snd_mixer_first_elem(mixer);//找到第一个元素 while (elem) {............snd_mixer_elem_next(elem); //找到下一个元素 }snd_mixer_selem_get_name 调用snd_mixer_selem_get_name()函数可获取指定元素的名字如下所示 const char *snd_mixer_selem_get_name(snd_mixer_elem_t *elem);获取元素的名字之后进行对比以确定是否是我们要找的元素 const char *name snd_mixer_selem_get_name(elem); if (!strcmp(name, Headphone)) { //该配置项是Headphone } else { //该配置项不是Headphone }获取/更改元素的配置值 前面给大家提到了混音器的配置值有两种类型第一种它的配置值是在一个范围内的数值譬如音量大小的调节另一种则是bool 类型用于控制开启或关闭譬如0 表示关闭配置、1 表示使能配置。 snd_mixer_selem_has_playback_volume/snd_mixer_selem_has_capture_volume 我们可以调用snd_mixer_selem_has_playback_volume 播放或snd_mixer_selem_has_capture_volume 录音函数来判断一个指定元素的配置值是否是volume 类型也就是上面说的第一种情况。函数原型如下所示 int snd_mixer_selem_has_playback_volume(snd_mixer_elem_t *elem); int snd_mixer_selem_has_capture_volume(snd_mixer_elem_t *elem);函数返回0 表示不是volume 类型返回1 表示是volume 类型。 snd_mixer_selem_has_playback_switch/snd_mixer_selem_has_capture_switch 调用snd_mixer_selem_has_playback_switch播放snd_mixer_selem_has_capture_switch录音函数判断一个指定元素的配置值是否是switch 类型也就是上面说的第二种情况。函数原型如下所示 int snd_mixer_selem_has_playback_switch(snd_mixer_elem_t *elem); int snd_mixer_selem_has_capture_switch(snd_mixer_elem_t *elem);函数返回0 表示不是switch 类型返回1 表示是switch 类型。 snd_mixer_selem_has_playback_channel/snd_mixer_selem_has_capture_channel 通过snd_mixer_selem_has_playback_channel播放或snd_mixer_selem_has_capture_channel录音函数可判断指定元素是否包含指定通道其函数原型如下所示 int snd_mixer_selem_has_playback_channel(snd_mixer_elem_t *elem,snd_mixer_selem_channel_id_t channel ); int snd_mixer_selem_has_capture_channel(snd_mixer_elem_t *elem,snd_mixer_selem_channel_id_t channel );参数channel 用于指定一个通道snd_mixer_selem_channel_id_t 是一个枚举类型如下所示 enum snd_mixer_selem_channel_id_t {SND_MIXER_SCHN_UNKNOWN -1,SND_MIXER_SCHN_FRONT_LEFT 0, // 左前SND_MIXER_SCHN_FRONT_RIGHT, // 右前SND_MIXER_SCHN_REAR_LEFT, // 左后SND_MIXER_SCHN_REAR_RIGHT, // 右后SND_MIXER_SCHN_FRONT_CENTER, // 前中SND_MIXER_SCHN_WOOFER, // 低音喇叭SND_MIXER_SCHN_SIDE_LEFT, // 左侧SND_MIXER_SCHN_SIDE_RIGHT, // 右侧SND_MIXER_SCHN_REAR_CENTER, // 后中SND_MIXER_SCHN_LAST 31,SND_MIXER_SCHN_MONO SND_MIXER_SCHN_FRONT_LEFT // 单声道 }; 如果元素是双声道元素通常只包含左前SND_MIXER_SCHN_FRONT_LEFT 和右前SND_MIXER_SCHN_FRONT_RIGHT 两个声道。如果是单声道设备通常只包含 SND_MIXER_SCHN_MONO其数值等于SND_MIXER_SCHN_FRONT_LEFT。 可以调用snd_mixer_selem_is_playback_mono播放或snd_mixer_selem_is_capture_mono录音函数判断一个指定的元素是否是单声道元素其函数原型如下所示 int snd_mixer_selem_is_playback_mono(snd_mixer_elem_t *elem); int snd_mixer_selem_is_capture_mono(snd_mixer_elem_t *elem);snd_mixer_selem_get_playback_volume/snd_mixer_selem_get_capture_volume 调用snd_mixer_selem_get_playback_volume播放或snd_mixer_selem_get_capture_volume录音获取指定元素的音量大小其函数原型如下所示 int snd_mixer_selem_get_playback_volume(snd_mixer_elem_t *elem,snd_mixer_selem_channel_id_t channel,long *value ); int snd_mixer_selem_get_capture_volume(snd_mixer_elem_t *elem,snd_mixer_selem_channel_id_t channel,long *value );参数elem 指定对应的元素参数channel 指定该元素的某个声道。调用 snd_mixer_selem_get_playback_volume()函数可获取elem 元素的channel 声道对应的音量大小并将获取到的音量值通过value 返回出来。 函数调用成功返回0失败返回一个小于0 的错误码。 譬如获取左前声道的音量播放 long value; snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, value);snd_mixer_selem_set_playback_volume/snd_mixer_selem_set_capture_volume 设置指定元素的音量值其函数原型如下所示 int snd_mixer_selem_set_playback_volume(snd_mixer_elem_t *elem,snd_mixer_selem_channel_id_t channel,long value ); int snd_mixer_selem_set_capture_volume(snd_mixer_elem_t *elem,snd_mixer_selem_channel_id_t channel,long value );调用snd_mixer_selem_set_playback_volume播放或snd_mixer_selem_set_capture_volume录音设置元素的某个声道的音量参数elem 指定元素、参数channel 指定该元素的某个声道参数value 指定音量值。 调用snd_mixer_selem_set_playback_volume_all/snd_mixer_selem_set_capture_volume_all可一次性设置指定元素所有声道的音量函数原型如下所示 int snd_mixer_selem_set_playback_volume_all(snd_mixer_elem_t *elem,long value); int snd_mixer_selem_set_capture_volume_all(snd_mixer_elem_t *elem,long value); snd_mixer_selem_get_playback_volume_range/snd_mixer_selem_get_capture_volume_range 获取指定元素的音量范围其函数原型如下所示 int snd_mixer_selem_get_playback_volume_range(snd_mixer_elem_t *elem,long *min,long *max); int snd_mixer_selem_get_capture_volume_range(snd_mixer_elem_t *elem,long *min,long *max); 示例程序 本小节我们将对示例代码28.8.1 进行修改添加音量控制示例代码如下所示 本例程源码对应的路径为开发板光盘-11 、Linux C 应用编程例程源码-28_alsa-lib-pcm_playback_mixer.c。 #include stdio.h #include stdlib.h #include errno.h #include string.h #include termios.h #include signal.h #include alsa/asoundlib.h /************************************ 宏定义 ************************************/ #define PCM_PLAYBACK_DEV hw:0,0 #define MIXER_DEV hw:0 /************************************ WAV 音频文件解析相关数据结构申明 ************************************/ typedef struct WAV_RIFF {char ChunkID[4]; /* RIFF */u_int32_t ChunkSize; /* 从下一个地址开始到文件末尾的总字节数*/char Format[4]; /* WAVE */ } __attribute__((packed)) RIFF_t; typedef struct WAV_FMT {char Subchunk1ID[4]; /* fmt */u_int32_t Subchunk1Size; /* 16 for PCM */u_int16_t AudioFormat; /* PCM 1*/u_int16_t NumChannels; /* Mono 1, Stereo 2, etc. */u_int32_t SampleRate; /* 8000, 44100, etc. */u_int32_t ByteRate; /* SampleRate * NumChannels * BitsPerSample/8 */u_int16_t BlockAlign; /* NumChannels * BitsPerSample/8 */u_int16_t BitsPerSample; /* 8bits, 16bits, etc. */ } __attribute__((packed)) FMT_t; static FMT_t wav_fmt; typedef struct WAV_DATA {char Subchunk2ID[4]; /* data */u_int32_t Subchunk2Size; /* data size */ } __attribute__((packed)) DATA_t; /************************************ static 静态全局变量定义 ************************************/ static snd_pcm_t *pcm NULL; // pcm 句柄 static snd_mixer_t *mixer NULL; // 混音器句柄 static snd_mixer_elem_t *playback_vol_elem NULL; // 播放音量控制元素 static unsigned int buf_bytes; // 应用程序缓冲区的大小字节为单位 static void *buf NULL; // 指向应用程序缓冲区的指针 static int fd -1; // 指向WAV 音频文件的文件描述符 static snd_pcm_uframes_t period_size 1024; // 周期大小单位: 帧 static unsigned int periods 16; // 周期数设备驱动层buffer 的大小 static struct termios old_cfg; // 用于保存终端当前的配置参数 /************************************ static 静态函数 ************************************/ static void snd_playback_async_callback(snd_async_handler_t *handler) {snd_pcm_t *handle snd_async_handler_get_pcm(handler); // 获取PCM 句柄snd_pcm_sframes_t avail;int ret;avail snd_pcm_avail_update(handle); // 获取环形缓冲区中有多少帧数据需要填充while (avail period_size){ // 我们一次写入一个周期memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes);if (0 ret)goto out;ret snd_pcm_writei(handle, buf, period_size);if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto out;}else if (ret period_size){ // 实际写入的帧数小于指定的帧数// 此时我们需要调整下音频文件的读位置// 将读位置向后移动往回移(period_size-ret)*frame_bytes 个字节// frame_bytes 表示一帧的字节大小if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto out;}}avail snd_pcm_avail_update(handle); // 再次获取、更新avail}return; out:snd_pcm_drain(pcm); // 停止PCMsnd_mixer_close(mixer); // 关闭混音器snd_pcm_close(handle); // 关闭pcm 设备tcsetattr(STDIN_FILENO, TCSANOW, old_cfg); // 退出前恢复终端的状态free(buf);close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); // 退出程序 } static int snd_pcm_init(void) {snd_pcm_hw_params_t *hwparams NULL;snd_async_handler_t *async_handler NULL;int ret;/* 打开PCM 设备*/ret snd_pcm_open(pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);if (0 ret){fprintf(stderr, snd_pcm_open error: %s: %s\n,PCM_PLAYBACK_DEV, snd_strerror(ret));return -1;}/* 实例化hwparams 对象*/snd_pcm_hw_params_malloc(hwparams);/* 获取PCM 设备当前硬件配置,对hwparams 进行初始化*/ret snd_pcm_hw_params_any(pcm, hwparams);if (0 ret){fprintf(stderr, snd_pcm_hw_params_any error: %s\n, snd_strerror(ret));goto err2;}/**************设置参数***************//* 设置访问类型: 交错模式*/ret snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_access error: %s\n, snd_strerror(ret));goto err2;}/* 设置数据格式: 有符号16 位、小端模式*/ret snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_format error: %s\n, snd_strerror(ret));goto err2;}/* 设置采样率*/ret snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_rate error: %s\n, snd_strerror(ret));goto err2;}/* 设置声道数: 双声道*/ret snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_channels error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期大小: period_size */ret snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_period_size error: %s\n, snd_strerror(ret));goto err2;}/* 设置周期数驱动层环形缓冲区buffer 的大小: periods */ret snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 ret){fprintf(stderr, snd_pcm_hw_params_set_periods error: %s\n, snd_strerror(ret));goto err2;}/* 使配置生效*/ret snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); // 释放hwparams 对象占用的内存if (0 ret){fprintf(stderr, snd_pcm_hw_params error: %s\n, snd_strerror(ret));goto err1;}buf_bytes period_size * wav_fmt.BlockAlign; // 变量赋值一个周期的字节大小/* 注册异步处理函数*/ret snd_async_add_pcm_handler(async_handler, pcm, snd_playback_async_callback, NULL);if (0 ret){fprintf(stderr, snd_async_add_pcm_handler error: %s\n, snd_strerror(ret));goto err1;}return 0; err2:snd_pcm_hw_params_free(hwparams); // 释放内存 err1:snd_pcm_close(pcm); // 关闭pcm 设备return -1; } static int snd_mixer_init(void) {snd_mixer_elem_t *elem NULL;const char *elem_name;long minvol, maxvol;int ret;/* 打开混音器*/ret snd_mixer_open(mixer, 0);if (0 ret){fprintf(stderr, snd_mixer_open error: %s\n, snd_strerror(ret));return -1;}/* 关联一个声卡控制设备*/ret snd_mixer_attach(mixer, MIXER_DEV);if (0 ret){fprintf(stderr, snd_mixer_attach error: %s\n, snd_strerror(ret));goto err;}/* 注册混音器*/ret snd_mixer_selem_register(mixer, NULL, NULL);if (0 ret){fprintf(stderr, snd_mixer_selem_register error: %s\n, snd_strerror(ret));goto err;}/* 加载混音器*/ret snd_mixer_load(mixer);if (0 ret){fprintf(stderr, snd_mixer_load error: %s\n, snd_strerror(ret));goto err;}/* 遍历混音器中的元素*/elem snd_mixer_first_elem(mixer); // 找到第一个元素while (elem){elem_name snd_mixer_selem_get_name(elem); // 获取元素的名称/* 针对开发板出厂系统WM8960 声卡设备*/if (!strcmp(Speaker, elem_name) || // 耳机音量对喇叭外音输出有效!strcmp(Headphone, elem_name) || // 喇叭音量对耳机输出有效!strcmp(Playback, elem_name)){ // 播放音量总的音量控制对喇叭和耳机输出都有效if (snd_mixer_selem_has_playback_volume(elem)){ // 是否是音量控制元素snd_mixer_selem_get_playback_volume_range(elem, minvol, maxvol); // 获取音量可设置范围snd_mixer_selem_set_playback_volume_all(elem, (maxvol - minvol) * 0.9 minvol); // 全部设置为90%if (!strcmp(Playback, elem_name))playback_vol_elem elem;}}elem snd_mixer_elem_next(elem);}return 0; err:snd_mixer_close(mixer);return -1; } static int open_wav_file(const char *file) {RIFF_t wav_riff;DATA_t wav_data;int ret;fd open(file, O_RDONLY);if (0 fd){fprintf(stderr, open error: %s: %s\n, file, strerror(errno));return -1;}/* 读取RIFF chunk */ret read(fd, wav_riff, sizeof(RIFF_t));if (sizeof(RIFF_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(RIFF, wav_riff.ChunkID, 4) || // 校验strncmp(WAVE, wav_riff.Format, 4)){fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 读取sub-chunk-fmt */ret read(fd, wav_fmt, sizeof(FMT_t));if (sizeof(FMT_t) ! ret){if (0 ret)perror(read error);elsefprintf(stderr, check error: %s\n, file);close(fd);return -1;}if (strncmp(fmt , wav_fmt.Subchunk1ID, 4)){ // 校验fprintf(stderr, check error: %s\n, file);close(fd);return -1;}/* 打印音频文件的信息*/printf(音频文件格式信息\n\n);printf( file name: %s\n, file);printf( Subchunk1Size: %u\n, wav_fmt.Subchunk1Size);printf( AudioFormat: %u\n, wav_fmt.AudioFormat);printf( NumChannels: %u\n, wav_fmt.NumChannels);printf( SampleRate: %u\n, wav_fmt.SampleRate);printf( ByteRate: %u\n, wav_fmt.ByteRate);printf( BlockAlign: %u\n, wav_fmt.BlockAlign);printf( BitsPerSample: %u\n\n, wav_fmt.BitsPerSample);/* sub-chunk-data */if (0 lseek(fd, sizeof(RIFF_t) 8 wav_fmt.Subchunk1Size,SEEK_SET)){perror(lseek error);close(fd);return -1;}while (sizeof(DATA_t) read(fd, wav_data, sizeof(DATA_t))){/* 找到sub-chunk-data */if (!strncmp(data, wav_data.Subchunk2ID, 4)) // 校验return 0;if (0 lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)){perror(lseek error);close(fd);return -1;}}fprintf(stderr, check error: %s\n, file);return -1; } static void show_help(void) {printf(基于alsa-lib 音乐播放器\n\n操作菜单:\n q 退出程序\n space空格 暂停播放/恢复播放\n w 音量增加\n s 音量减小--\n\n); } /************************************ main 主函数 ************************************/ int main(int argc, char *argv[]) {snd_pcm_sframes_t avail;struct termios new_cfg;sigset_t sset;int ret;if (2 ! argc){fprintf(stderr, Usage: %s audio_file\n, argv[0]);exit(EXIT_FAILURE);}/* 屏蔽SIGIO 信号*/sigemptyset(sset);sigaddset(sset, SIGIO);sigprocmask(SIG_BLOCK, sset, NULL);/* 打开WAV 音频文件*/if (open_wav_file(argv[1]))exit(EXIT_FAILURE);/* 初始化PCM Playback 设备*/if (snd_pcm_init())goto err1;/* 初始化混音器*/if (snd_mixer_init())goto err2;/* 申请读缓冲区*/buf malloc(buf_bytes);if (NULL buf){perror(malloc error);goto err3;}/* 终端配置*/tcgetattr(STDIN_FILENO, old_cfg); // 获取终端标准输入-标准输出构成了一套终端memcpy(new_cfg, old_cfg, sizeof(struct termios)); // 备份new_cfg.c_lflag ~ICANON; // 将终端设置为非规范模式new_cfg.c_lflag ~ECHO; // 禁用回显tcsetattr(STDIN_FILENO, TCSANOW, new_cfg); // 使配置生效/* 播放先将环形缓冲区填满数据*/avail snd_pcm_avail_update(pcm); // 获取环形缓冲区中有多少帧数据需要填充while (avail period_size){ // 我们一次写入一个周期memset(buf, 0x00, buf_bytes); // buf 清零ret read(fd, buf, buf_bytes);if (0 ret)goto err4;ret snd_pcm_writei(pcm, buf, period_size); // 向环形缓冲区中写入数据if (0 ret){fprintf(stderr, snd_pcm_writei error: %s\n, snd_strerror(ret));goto err4;}else if (ret period_size){ // 实际写入的帧数小于指定的帧数// 此时我们需要调整下音频文件的读位置// 将读位置向后移动往回移(period_size-ret)*frame_bytes 个字节// frame_bytes 表示一帧的字节大小if (0 lseek(fd, (ret - period_size) * wav_fmt.BlockAlign, SEEK_CUR)){perror(lseek error);goto err4;}}avail snd_pcm_avail_update(pcm); // 再次获取、更新avail}sigprocmask(SIG_UNBLOCK, sset, NULL); // 取消SIGIO 信号屏蔽/* 显示帮助信息*/show_help();/* 等待获取用户输入*/char ch;long vol;for (;;){ch getchar(); // 获取用户输入的控制字符switch (ch){case q: // Q 键退出程序sigprocmask(SIG_BLOCK, sset, NULL); // 屏蔽SIGIO 信号goto err4;case : // 空格暂停/恢复switch (snd_pcm_state(pcm)){case SND_PCM_STATE_PAUSED: // 如果是暂停状态则恢复运行ret snd_pcm_pause(pcm, 0);if (0 ret)fprintf(stderr, snd_pcm_pause error: %s\n, snd_strerror(ret));break;case SND_PCM_STATE_RUNNING: // 如果是运行状态则暂停ret snd_pcm_pause(pcm, 1);if (0 ret)fprintf(stderr, snd_pcm_pause error: %s\n, snd_strerror(ret));break;}break;case w: // 音量增加if (playback_vol_elem){// 获取音量snd_mixer_selem_get_playback_volume(playback_vol_elem,SND_MIXER_SCHN_FRONT_LEFT, vol);vol;// 设置音量snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);}break;case s: // 音量降低if (playback_vol_elem){// 获取音量snd_mixer_selem_get_playback_volume(playback_vol_elem,SND_MIXER_SCHN_FRONT_LEFT, vol);vol--;// 设置音量snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);}break;}} err4:snd_pcm_drop(pcm); // 停止PCMtcsetattr(STDIN_FILENO, TCSANOW, old_cfg); // 退出前恢复终端的状态free(buf); // 释放内存 err3:snd_mixer_close(mixer); // 关闭混音器 err2:snd_pcm_close(pcm); // 关闭pcm 设备 err1:close(fd); // 关闭打开的音频文件exit(EXIT_FAILURE); } main()函数中调用了自定义函数snd_mixer_init()对声卡混音器进行了初始化snd_mixer_init()函数中做的事情也就是上面给大家所介绍的流程首先打开一个空的混音器、attach 关联一个声卡控制设备、注册混音器、加载混音器整个这一套操作完成之后就可以去使用混音器了查找混音器中的元素对元素进行配置。 在snd_mixer_init()函数中我们对WM8960 声卡的Speaker元素喇叭输出音量、Headphone元素耳机输出音量以及Playback元素播放音量进行了配置将它们都设置为90%之后将Playback元素的句柄赋值给全局静态变量playback_vol_elem。 回到main()函数在for 循环中获取用户输入的控制字符在这里我们添加了w 和s当用户按下w 键时增加音量、按下s 键时降低音量这里控制的音量是WM8960 的Playback音量播放音量。 编译应用程序 编译上述示例代码 ${CC} -o testApp testApp.c -lasound测试应用程序 将编译得到的可执行文件拷贝到开发板Linux 系统的/home/root 目录下准备一个WAV 格式的音频文件执行测试程序 ./testApp ./EXO-Overdose.wav命令执行之后会显示操作方式大家可以根据提示自己测试 回环测试例程 alsa-utils 提供了一个用于回环测试的工具alsaloop可以实现边录音、边播放该程序用法比较简单执行alsaloop --help可以查看alsaloop 测试程序的使用帮助信息如下所示 譬如直接运行alsaloop -t 1000可以进行测试大家可以自己亲自测试下。 回环测试原理上很简单录制音频、然后再播放出来但是事实上并不如此还需要考虑到很多的因素因为对于录音和播放来说录制一个周期和播放一个周期硬件上处理这一个周期所花费的时间并不相同一个是ADC 过程、而一个是DAC 过程所以往往很容易出现XRUN所以如何有效合理地设计你的应用程序将变得很重要、以最大限度降低XRUN 情况的发生。 笔者测试过alsaloop 工具虽然也会出现XRUN但比较少如果对此有兴趣的读者可以参考alsaloop 程序的源代码直接下载alsa-util 源码包在alsa-util 源码包中就可以找到alsaloop 程序的源码如下所示 除了alsaloop 的源码之外还包括前面所介绍的aplay、alsamixer、amixer、alsactl 等这些工具的源码都在这里有兴趣的读者可以看看。 总结 本章我们学习了Linux 下的音频应用编程应用程序基于alsa-lib 库实现播放、录音等功能本章并没有做过多深入的学习仅仅只是给大家介绍了alsa-lib 库函数中一些基本的API 接口其中还有绝大部分的接口并没有给大家介绍如果大家有兴趣可以自己深入研究、学习 本小节我们来聊一聊ALSA 插件。 ALSA 插件plugin ALSA 提供了一些PCM 插件以扩展PCM 设备的功能和特性插件负责各种样本转换、通道之间的样本复制等。 调用snd_pcm_open()函数时需要填写PCM 设备名alsa-lib 库使用逻辑设备名而不是设备节点名。前面编写的示例程序中我们使用了hw:i,j这种格式的名字这其实指定的是一个名为hw 的插件而冒号后面的两个数字i 和j 表示两个参数也就是使用该插件时传入的两个参数第一个参数表示声卡号第二个参数表示设备号。 开发板Linux 系统的/usr/share/alsa/目录下有一个名为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 插件如下所示 上图中的每一个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 提供的文档。
http://www.pierceye.com/news/582991/

相关文章:

  • 一个vps主机放两个网站 速度怎么做发卡网站
  • 海米云网站建设网站开发 去哪里找页面
  • 天津做网站优化的公司新手学做网站优化
  • 万网怎么上传网站wordpress google字体 360
  • 为什么建设的网站有时候访问慢6紫金优化网站制作
  • 如何在公司系统建网站广州短视频seo哪家好
  • 电气网站开发福安网站定制
  • 推荐一下做图文的网站html简单的个人网页代码
  • 网页新建站点网站建设缺陷
  • 移动端网站推广怎么申请pc网站域名
  • 外国男男做暧暧视频网站二级建造师考试试题
  • 普通网站建设是什么wordpress主题显示不
  • 朔州网站建设全球速卖通是什么平台
  • wordpress外贸网站好用的模板下载网站开发就业趋势
  • 长春模板建站代理网站开发嘉比格网络
  • 网站建设预算企业网站的公司和产品信息的介绍与网络营销关系
  • 网站开发的学习电子商务网站建设公
  • 网站的功能需求分析c语言网页编辑器
  • 网站投资多少钱制作做的网站如何上传网上
  • 沈阳自助建站模板网站建设想法
  • 湖南岳阳网站建设公司黄页顺企网怎样不让网站被收录
  • 有没有专门做翻译的网站安徽建设工程信息网招标公告
  • 保险咨询网站留电话中国十大网络公司排行榜
  • 领手工在家做的网站2019网页设计与实现论文
  • 兰州微信信息平台网站建设绍兴本地网站建设
  • 关于旅游网站策划书千锋前端培训多少钱
  • 温州网站建设结构做代练网站能备案
  • 零基础学习做网站第三方装修评估公司
  • 基础微网站开发动态网站彩票投注员做啥的
  • 西安做网站设计公司爱做网站免费版