音频应用编程

news2024/12/27 8:44:52

目录

  • ALSA 概述
  • alsa-lib 简介
  • sound 设备节点
  • alsa-lib 移植
  • 编写一个简单地alsa-lib 应用程序
    • 一些基本概念
    • 打开PCM 设备
    • 设置硬件参数

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。如下图所示:
图28.1.1 alsa 音频示意图

alsa-lib 简介

如上所述,alsa-lib 是一套Linux 应用层的C 语言函数库,为音频应用程序开发提供了一套统一、标准的接口,应用程序只需调用这一套API 即可完成对底层声卡设备的操控,譬如播放与录音。
用户空间的alsa-lib 对应用程序提供了统一的API 接口,这样可以隐藏驱动层的实现细节,简化了应用程序的实现难度、无需应用程序开发人员直接去读写音频设备节点。所以本章,对于我们来说,学习音频应用编程其实就是学习alsa-lib 库函数的使用、如何基于alsa-lib 库函数开发音频应用程序。
ALSA 提供了关于alsa-lib 的使用说明文档,其链接地址为:https://www.alsa-project.org/alsa-doc/alsa-lib/,进入到该链接地址后,如下所示:
在这里插入图片描述
alsa-lib 库支持功能比较多,提供了丰富的API 接口供应用程序开发人员调用,根据函数的功能、作用将这些API 进行了分类,可以点击上图中Modules 按钮查看其模块划分,如下所示:
在这里插入图片描述
一个分类就是一个模块(module),有些模块下可能该包含了子模块,譬如上图中,模块名称前面有三角箭头的表示该模块包含有子模块。
⚫ Global defines and functions:包括一些全局的定义,譬如函数、宏等;
⚫ Constants for Digital Audio Interfaces:数字音频接口相关的常量;
⚫ Input Interface:输入接口;
⚫ Output Interface:输出接口;
⚫ Error handling:错误处理相关接口;
⚫ Configuration Interface:配置接口;
⚫ Control Interface:控制接口;
⚫ PCM Interface:PCM 设备接口;
⚫ RawMidi Interface:RawMidi 接口;
⚫ Timer Interface:定时器接口;
⚫ Hardware Dependant Interface:硬件相关接口;
⚫ MIDI Sequencer:MIDI 音序器;
⚫ External PCM plugin SDK:外部PCM 插件SDK;
⚫ External Control Plugin SDK:外部控制插件SDK;
⚫ Mixer Interface:混音器接口;
⚫ Use Case Interface:用例接口;
⚫ Topology Interface:拓扑接口。
可以看到,alsa-lib 提供的接口确实非常多、模块很多,以上所列举出来的这些模块,很多模块笔者也不是很清楚它们的具体功能、作用,但是本章我们仅涉及到三个模块下的API 函数,包括:PCM Interface、
Error Interface 以及Mixer Interface。
PCM Interface
PCM Interface,提供了PCM 设备相关的操作接口,譬如打开/关闭PCM 设备、配置PCM 设备硬件或软件参数、控制PCM 设备(启动、暂停、恢复、写入/读取数据),该模块下还包含了一些子模块,如下所示:
图28.2.3 PCM Interface 下的子模块

点击模块名称可以查看到该模块提供的API 接口有哪些以及相应的函数说明,这里就不给大家演示了!
Error Interface
该模块提供了关于错误处理相关的接口,譬如函数调用发生错误时,可调用该模块下提供的函数打印错误描述信息。
Mixer Interface
提供了关于混音器相关的一系列操作接口,譬如音量、声道控制、增益等等。

sound 设备节点

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

Tips:注意,Mini I.MX6U 开发板出厂系统/dev/snd 目录下是没有这些文件的,因为Mini 板不支持音频、没有板载音频编解码芯片,所以本章实验例程无法在Mini 板上进行测试,请悉知!
从上图可以看到有如下设备文件:
⚫ controlC0:用于声卡控制的设备节点,譬如通道选择、混音器、麦克风的控制等,C0 表示声卡0
(card0);
⚫ pcmC0D0c:用于录音的PCM 设备节点。其中C0 表示card0,也就是声卡0;而D0 表示device
0,也就是设备0;最后一个字母c 是capture 的缩写,表示录音;所以pcmC0D0c 便是系统的声卡
0 中的录音设备0;
⚫ pcmC0D0p:用于播放(或叫放音、回放)的PCM 设备节点。其中C0 表示card0,也就是声卡0;而D0 表示device 0,也就是设备0;最后一个字母p 是playback 的缩写,表示播放;所以pcmC0D0p
便是系统的声卡0 中的播放设备0;
⚫ pcmC0D1c:用于录音的PCM 设备节点。对应系统的声卡0 中的录音设备1;
⚫ pcmC0D1p:用于播放的PCM 设备节点。对应系统的声卡0 中的播放设备1。
⚫ timer:定时器。
本章我们编写的应用程序,虽然是调用alsa-lib 库函数去控制底层音频硬件,但最终也是落实到对sound
设备节点的I/O 操作,只不过alsa-lib 已经帮我们封装好了。在Linux 系统的/proc/asound 目录下,有很多的文件,这些文件记录了系统中声卡相关的信息,如下所示:
在这里插入图片描述
cards:
通过"cat /proc/asound/cards"命令、查看cards 文件的内容,可列出系统中可用的、注册的声卡,如下所示:

cat /proc/asound/cards

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

我们的阿尔法板子上只有一个声卡(WM8960 音频编解码器),所以它的编号为0,也就是card0。系统中注册的所有声卡都会在/proc/asound/目录下存在一个相应的目录,该目录的命名方式为cardX(X 表示声卡的编号),譬如图28.3.2 中的card0;card0 目录下记录了声卡0 相关的信息,譬如声卡的名字以及声卡注册的PCM 设备,如下所示:
在这里插入图片描述
devices:
列出系统中所有声卡注册的设备,包括control、pcm、timer、seq 等等。如下所示:

cat /proc/asound/devices

图28.3.5 列出所有设备

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

cat /proc/asound/pcm

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

alsa-lib 移植

因为alsa-lib 是ALSA 提供的一套Linux 下的C 语言函数库,需要将alsa-lib 移植到开发板上,这样基于alsa-lib 编写的应用程序才能成功运行,除了移植alsa-lib 库之外,通常还需要移植alsa-utils,alsa-utils 包含了一些用于测试、配置声卡的工具。
事实上,ALPHA I.MX6U 开发板出厂系统中已经移植了alsa-lib 和alsa-utils,本章我们直接使用出厂系统移植好的alsa-lib 和alsa-utils 进行测试,笔者也就不再介绍移植过程了。其实它们的移植方法也非常简单,如果你想自己尝试移植,网上有很多参考,大家可以自己去看看。
alsa-utils 提供了一些用于测试、配置声卡的工具,譬如aplay、arecord、alsactl、alsaloop、alsamixer、
amixer 等,在开发板出厂系统上可以直接使用这些工具,这些应用程序也都是基于alsa-lib 编写的。
aplay
aplay 是一个用于测试音频播放功能程序,可以使用aplay 播放wav 格式的音频文件,如下所示:
在这里插入图片描述
程序运行之后就会开始播放音乐,因为ALPHA 开发板支持喇叭和耳机自动切换,如果不插耳机默认从喇叭播放音乐,插上耳机以后喇叭就会停止播放,切换为耳机播放音乐,这个大家可以自己进行测试。
需要注意的是,aplay 工具只能解析wav 格式音频文件,不支持mp3 格式解码,所以无法使用aplay 工具播放mp3 音频文件。稍后笔者会向大家介绍如何基于alsa-lib 编写一个简单地音乐播放器,实现与aplay
相同的效果。
alsamixer
alsamixer 是一个很重要的工具,用于配置声卡的混音器,它是一个字符图形化的配置工具,直接在开发板串口终端运行alsamixer 命令,打开图形化配置界面,如下所示:
图28.4.2 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.state,store 表示保存配置。保存成功以后就会生成/var/lib/alsa/asound.state 这个文件,
asound.state 文件中保存了声卡的各种设置信息,大家可以打开此文件查看里面的内容,如下所示:
在这里插入图片描述
除了保存配置之外,还可以加载配置,譬如使用/var/lib/alsa/asound.state 文件中的配置信息来配置声卡,可执行如下命令:

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

restore 表示加载配置,读取/var/lib/alsa/asound.state 文件中的配置信息并对声卡进行设置。关于alsactl
的详细使用方法,可以执行"alsactl -h"进行查看。
开发板出厂系统每次开机启动时便会自动从/var/lib/alsa/asound.state 文件中读取配置信息并配置声卡,而每次关机时(譬如执行reset 或poweroff 命令)又会将声卡当前的配置写入到该文件中进行保存,以便下一次启动时加载。其实也就是在系统启动(或关机)时通过alsactl 工具加载(或保存)配置。
amixer
amixer 工具也是一个声卡配置工具,与alsamixer 功能相同,区别在于,alsamixer 是一个基于字符图形化的配置工具、而amixer 不是图形化配置工具,直接使用命令行配置即可,详细地用法大家可以执行"amixer --help"命令查看,下面笔者简单地提一下该工具怎么用:
执行命令"amixer scontrols"可以查看到有哪些配置项,如下所示:
在这里插入图片描述
从打印信息可知,这里打印出来的配置项与alsamixer 配置界面中所看到的配置项是相同的,那如何进去配置呢?不同的配置项对应的配置方法(配置值或值类型)是不一样的,可以先使用命令"amixer scontents"
查看配置项的说明,如下所示:

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

图28.4.7 使用arecord 工具录音

-f 选项指定音频格式,cd 则表示cd 级别音频,也就是“16 bit little endian, 44100, stereo”;-d 选项指定音频录制时间长度,单位是秒;test.wav 指定音频数据保存的文件。当录制完成之后,会生成test.wav 文件,接着我们可以使用aplay 工具播放这一段音频。
以上给大家介绍了alsa-utils 提供的几个测试音频、配置声卡的工具,当然,本文也只是进行了简单地介绍,更加详细的使用方法还需要大家自己查看帮助信息。

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

本小节开始,我们来学习如何基于alsa-lib 编写音频应用程序,alsa-lib 提供的库函数也别多,笔者肯定不会全部给大家介绍,只介绍基础的使用方法,关于更加深入、更加详细的使用方法需要大家自己去研究、学习。
对于alsa-lib 库的使用,ALSA 提供了一些参考资料来帮助应用程序开发人员快速上手alsa-lib、基于
alsa-lib 进行应用编程,以下笔者给出了链接:
https://users.suse.com/~mana/alsa090_howto.html
https://www.alsa-project.org/alsa-doc/alsa-lib/examples.html
第一份文档向用户介绍了如何使用alsa-lib 编写简单的音频应用程序,包括PCM 播放音频、PCM 录音等,笔者也是参考了这份文档来编写本章教程,对应初学者,建议大家看一看。
第二个链接地址是ALSA 提供的一些示例代码,如下所示:
图28.5.1 ALSA 提供的参考代码

点击对应源文件即可查看源代码。
以上便是ALSA 提供的帮助文档以及参考代码,链接地址已经给出了,大家有兴趣可以看一下。
本小节笔者将向大家介绍如何基于alsa-lib 编写一个简单地音频应用程序,譬如播放音乐、录音等;但在此之前,首先我们需要先来了解一些基本的概念,为后面的学习打下一个坚实的基础!

一些基本概念

主要是与音频相关的基本概念,因为在alsa-lib 应用编程中会涉及到这些概念,所以先给大家进行一个简单地介绍。
样本长度(Sample)
样本是记录音频数据最基本的单元,样本长度就是采样位数,也称为位深度(Bit Depth、Sample Size、
Sample Width)。是指计算机在采集和播放声音文件时,所使用数字声音信号的二进制位数,或者说每个采样样本所包含的位数(计算机对每个通道采样量化时数字比特位数),通常有8bit、16bit、24bit 等。
声道数(channel)
分为单声道(Mono)和双声道/立体声(Stereo)。1 表示单声道、2 表示立体声。
帧(frame)
帧记录了一个声音单元,其长度为样本长度与声道数的乘积,一段音频数据就是由苦干帧组成的。
把所有声道中的数据加在一起叫做一帧,对于单声道:一帧= 样本长度* 1;双声道:一帧= 样本长度* 2。譬如对于样本长度为16bit 的双声道来说,一帧的大小等于:16 * 2 / 8 = 4 个字节。

采样率(Sample rate)
也叫采样频率,是指每秒钟采样次数,该次数是针对桢而言。譬如常见的采样率有:
8KHz - 电话所用采样率
22.05KHz - FM 调频广播所用采样率
44.1KHz - 音频CD,也常用于MPEG-1 音频(VCD、SVCD、MP3)所用采样率
48KHz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率。
交错模式(interleaved)
交错模式是一种音频数据的记录方式,分为交错模式和非交错模式。在交错模式下,数据以连续桢的形式存放,即首先记录完桢1 的左声道样本和右声道样本(假设为立体声格式),再记录桢2 的左声道样本和右声道样本。而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。不过多数情况下,我们一般都是使用交错模式。
周期(period)
周期是音频设备处理(读、写)数据的单位,换句话说,也就是音频设备读写数据的单位是周期,每一次读或写一个周期的数据,一个周期包含若干个帧;譬如周期的大小为1024 帧,则表示音频设备进行一次读或写操作的数据量大小为1024 帧,假设一帧为4 个字节,那么也就是1024*4=4096 个字节数据。
一个周期其实就是两次硬件中断之间的帧数,音频设备每处理(读或写)完一个周期的数据就会产生一个中断,所以两个中断之间相差一个周期,关于中断的问题,稍后再向大家介绍!
缓冲区(buffer)
数据缓冲区,一个缓冲区包含若干个周期,所以buffer 是由若干个周期所组成的一块空间。下面一张图直观地表示了buffer、period、frame、sample(样本长度)之间的关系,假设一个buffer 包含4 个周期、而一个周包含1024 帧、一帧包含两个样本(左、右两个声道):
图28.5.2 buffer/period/frame/sample 之间的关系示例图

音频设备底层驱动程序使用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 的起始位置,也就是第一个周期的起始位置,如下所示:
图28.5.3 pointer 指向buffer 起始位置

应用程序向buffer 写入多少帧数据,则write pointer 指针向前移动多少帧,当应用程序向buffer 中写入一个周期的数据时,write pointer 指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以此类推!当write pointer 移动到buffer 末尾时,又会回到buffer 的起始位置,以此循环!所以由此可知,这是一个环形缓冲区。
以上是应用程序写buffer 的一个过程,接着再来看看音频设备读buffer(播放)的过程。在播放开始之前,read pointer 指向了buffer 的起始位置,也就是第一个周期的起始位置。音频设备每次只播放一个周期的数据(读取一个周期),每一次都是从read pointer 所指位置开始读取;每读取一个周期,read pointer 指针向前移动一个周期,同样,当read pointer 指针移动到buffer 末尾时,又会回到buffer 的起始位置,以此构成一个循环!
应用程序需要向buffer 中写入音频数据,音频设备才能读取数据进行播放,如果read pointer 所指向的周期并没有填充音频数据,则无法播放!当buffer 数据满时,应用程序将不能再写入数据,否则就会覆盖之前的数据,必须要等待音频设备播放完一个周期,音频设备每播放完一个周期,这个周期就变成空闲状态了,此时应用程序就可以写入一个周期的数据以填充这个空闲周期。
⚫ 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 中的数据总是连续地在音频设备和应用程序缓存区间传输,如下图所示:
图28.5.4 buffer 中数据的传输

上图展示了声卡在工作状态下,buffer 中数据的传输情况,总是连续地在音频设备和应用程序缓存区间传输,但事情并不总是那么完美、也会出现有例外;譬如在录音例子中,如果应用程序读取数据不够快,环形缓冲区buffer 中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖;这种数据的丢失被称为overrun。在播放例子中,如果应用程序写入数据到环形缓冲区buffer 中的速度不够快,

缓存区将会“饿死”(缓冲区中无数据可播放);这样的错误被称为underrun(欠载)。在ALSA 文档中,将这两种情形统称为"XRUN",适当地设计应用程序可以最小化XRUN 并且可以从中恢复过来。

打开PCM 设备

从本小节开始,将正式介绍如何编写一个音频应用程序,首先我们需要在应用程序中包含alsa-lib 库的头文件<alsa/asoundlib.h>,这样才能在应用程序中调用alsa-lib 库函数以及使用相关宏。
第一步需要打开PCM 设备,调用函数snd_pcm_open(),该函数原型如下所示:

int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)

该函数一共有4 个参数,如下所示:
⚫ pcmp:snd_pcm_t 用于描述一个PCM 设备,所以一个snd_pcm_t 对象表示一个PCM 设备;
snd_pcm_open 函数会打开参数name 所指定的设备,实例化snd_pcm_t 对象,并将对象的指针(也就是PCM 设备的句柄)通过pcmp 返回出来。
⚫ name:参数name 指定PCM 设备的名字。alsa-lib 库函数中使用逻辑设备名而不是设备文件名,命名方式为"hw:i,j",i 表示声卡的卡号,j 则表示这块声卡上的设备号;譬如"hw:0,0"表示声卡0 上的
PCM 设备0 ,在播放情况下,这其实就对应/dev/snd/pcmC0D0p (如果是录音,则对应
/dev/snd/pcmC0D0c)。除了使用"hw:i,j"这种方式命名之外,还有其它两种常用的命名方式,譬如
“plughw:i,j”、"default"等,关于这些名字的不同,本章最后再向大家进行简单地介绍,这里暂时先不去理会这个问题。
⚫ stream:参数stream 指定流类型,有两种不同类型:SND_PCM_STREAM_PLAYBACK 和
SND_PCM_STREAM_CAPTURE ;SND_PCM_STREAM_PLAYBACK 表示播放,
SND_PCM_STREAM_CAPTURE 则表示采集。
⚫ mode:最后一个参数mode 指定了open 模式,通常情况下,我们会将其设置为0,表示默认打开模式,默认情况下使用阻塞方式打开设备;当然,也可将其设置为SND_PCM_NONBLOCK,表示以非阻塞方式打开设备。
设备打开成功,snd_pcm_open 函数返回0;打开失败,返回一个小于0 的错误编号,可以使用alsa-lib
提供的库函数snd_strerror()来得到对应的错误描述信息,该函数与C 库函数strerror()用法相同。
与snd_pcm_open 相对应的是snd_pcm_close(),函数snd_pcm_close()用于关闭PCM 设备,函数原型如下所示:

int snd_pcm_close(snd_pcm_t *pcm);

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

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

设置硬件参数

打开PCM 设备之后,接着我们需要对设备进行设置,包括硬件配置和软件配置。软件配置就不再介绍了,使用默认配置即可!我们主要是对硬件参数进行配置,譬如采样率、声道数、格式、访问类型、period
周期大小、buffer 大小等。
实例化snd_pcm_hw_params_t 对象
alsa-lib 使用snd_pcm_hw_params_t 数据类型来描述PCM 设备的硬件配置参数,在配置参数之前,我们需要实例化一个snd_pcm_hw_params_t 对象,使用snd_pcm_hw_params_malloc 或

snd_pcm_hw_params_alloca()来实例化一个snd_pcm_hw_params_t 对象,如下所示:
snd_pcm_hw_params_t *hwparams = NULL;
snd_pcm_hw_params_malloc(&hwparams);snd_pcm_hw_params_alloca(&hwparams);

它们之间的区别也就是C 库函数malloc 和alloca 之间的区别。当然,你也可以直接使用malloc()或
alloca() 来分配一个snd_pcm_hw_params_t 对象,亦或者直接定义全局变量或栈自动变量。与
snd_pcm_hw_params_malloc/snd_pcm_hw_params_alloca 相对应的是snd_pcm_hw_params_free ,
snd_pcm_hw_params_free()函数用于释放snd_pcm_hw_params_t 对象占用的内存空间。函数原型如下所示:

void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj)

初始化snd_pcm_hw_params_t 对象
snd_pcm_hw_params_t 对象实例化完成之后,接着我们需要对其进行初始化操作,调用
snd_pcm_hw_params_any()对snd_pcm_hw_params_t 对象进行初始化操作,调用该函数会使用PCM 设备当前的配置参数去初始化snd_pcm_hw_params_t 对象,如下所示:

snd_pcm_hw_params_any(pcm_handle, hwparams);

第一个参数为PCM 设备的句柄,第二个参数传入snd_pcm_hw_params_t 对象的指针。
对硬件参数进行设置
alsa-lib 提供了一系列的snd_pcm_hw_params_set_xxx 函数用于设置PCM 设备的硬件参数,同样也提供了一系列的snd_pcm_hw_params_get_xxx 函数用于获取硬件参数。
(1)设置access 访问类型:snd_pcm_hw_params_set_access()
调用snd_pcm_hw_params_set_access 设置访问类型,其函数原型如下所示:

int snd_pcm_hw_params_set_access(snd_pcm_t *pcm,
	snd_pcm_hw_params_t * params,
	snd_pcm_access_t access
)

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

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

通常,将访问类型设置为SND_PCM_ACCESS_RW_INTERLEAVED ,交错访问模式,通过
snd_pcm_readi/snd_pcm_writei 对PCM 设备进行读/写操作。
函数调用成功返回0;失败将返回一个小于0 的错误码,可通过snd_strerror()函数获取错误描述信息。
使用示例:

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

(2)设置数据格式:snd_pcm_hw_params_set_format()
调用snd_pcm_hw_params_set_format()函数设置PCM 设备的数据格式,函数原型如下所示:

int snd_pcm_hw_params_set_format(snd_pcm_t *pcm,
	snd_pcm_hw_params_t *params,
	snd_pcm_format_t format
)

参数format 指定数据格式,该参数是一个snd_pcm_format_t 类型常量,这是一个枚举类型,如下所示:

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

用的最多的格式是SND_PCM_FORMAT_S16_LE,有符号16 位、小端模式。当然,音频设备不一定支持用户所指定的格式,在此之前,用户可以调用snd_pcm_hw_params_test_format()函数测试PCM 设备是否支持某种格式,如下所示:

if (snd_pcm_hw_params_test_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE)) {
// 返回一个非零值表示不支持该格式
}
else {
// 返回0 表示支持
}

(3)设置声道数:snd_pcm_hw_params_set_channels()
调用snd_pcm_hw_params_set_channels()函数设置PCM 设备的声道数,函数原型如下所示:

int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm,
snd_pcm_hw_params_t *params,
unsigned int val
)

参数val 指定声道数量,val=2 表示双声道,也就是立体声。函数调用成功返回0,失败返回小于0 的错误码。
使用示例:

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

(4)设置采样率大小:snd_pcm_hw_params_set_rate()
调用snd_pcm_hw_params_set_rate 设置采样率大小,其函数原型如下所示:

int snd_pcm_hw_params_set_rate(snd_pcm_t *pcm,
snd_pcm_hw_params_t *params,
unsigned int val,
int dir
)

参数val 指定采样率大小,譬如44100;参数dir 用于控制方向,若dir=-1,则实际采样率小于参数val
指定的值;dir=0 表示实际采样率等于参数val;dir=1 表示实际采样率大于参数val。
函数调用成功返回0;失败将返回小于0 的错误码。
使用示例:

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

(5)设置周期大小:snd_pcm_hw_params_set_period_size()
这里说的周期,也就是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));

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

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

相关文章

探索散列表和哈希表:高效存储与快速检索的魔法

文章目录 散列函数的原理散列表和哈希表的概念与操作解决冲突的方法案例分析&#xff1a;电话簿的实现拓展&#xff1a;性能与碰撞结论 &#x1f389;欢迎来到数据结构学习专栏~探索散列表和哈希表&#xff1a;高效存储与快速检索的魔法 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#…

【模拟集成电路】反馈系统加载效应——基础到进阶(三)

【模拟集成电路】反馈系统加载效应——基础到进阶&#xff08;三&#xff09; -----------------------文末附往期文章链接-------------------- 1.概述2.二端口网络方法2.1二端口网络模型2.2电压-电压反馈的加载2.2电流-电压反馈的加载2.3电压-电流反馈的加载2.4电流-电流反馈…

一文速学-让神经网络不再神秘,一天速学神经网络基础-输出层(四)

前言 思索了很久到底要不要出深度学习内容&#xff0c;毕竟在数学建模专栏里边的机器学习内容还有一大半算法没有更新&#xff0c;很多坑都没有填满&#xff0c;而且现在深度学习的文章和学习课程都十分的多&#xff0c;我考虑了很久决定还是得出神经网络系列文章&#xff0c;不…

Flutter 混合开发调试

针对Flutter开发的同学来说&#xff0c;大部分的应用还是Native Flutter的混合开发&#xff0c;所以每次改完Flutter代码&#xff0c;运行整个项目无疑是很费时间的。所以Flutter官方也给我们提供了混合调试的方案【在混合开发模式下进行调试】&#xff0c;这里以Android Stud…

Python基础学习第四天:Python注释

创建注释 注释以 &#xff03; 开头&#xff0c;Python 将忽略它们&#xff1a; 实例 #This is a comment print("Hello, World!")运行实例 注释可以放在一行的末尾&#xff0c;Python 将忽略该行的其余部分&#xff1a; 实例 print("Hello, World!")…

1-8 隐语小课|私有信息检索(PIR)及其应用场景

“隐语”是开源的可信隐私计算框架&#xff0c;内置 MPC、TEE、同态等多种密态计算虚拟设备供灵活选择&#xff0c;提供丰富的联邦学习算法和差分隐私机制 开源项目 github.com/secretflow gitee.com/secretflow 前言 欢迎来到小剧场全新系列节目「隐语小课」&#xff01;本…

Run the Docker daemon as a non-root user (Rootless mode)

rootless 简介 rootless模式是指以非root用户身份运行Docker守护程序和容器。那么为什么要有rootless mode呢&#xff1f;因为在root用户下安装启动的容器存在安全问题。存在的安全问题具体来说是容器内的root用户就是宿主机的root用户&#xff0c;容器内uid1000的用户就是宿主…

csp认证真题——重复局面——Java题解

目录 题目背景 问题描述 输入格式 输出格式 样例输入 样例输出 样例说明 子任务 提示 【思路解析】 【代码实现】 题目背景 国际象棋在对局时&#xff0c;同一局面连续或间断出现3次或3次以上&#xff0c;可由任意一方提出和棋。 问题描述 国际象棋每一个局面可以…

vscode 清除全部的console.log

在放页面的大文件夹view上面右键点击在文件夹中查找 console.log.*$ 注意&#xff1a;要选择使用正则匹配 替换为 " " (空字符串)

多线程应用——单例模式

单例模式 文章目录 单例模式一.什么是单例模式二.如何实现1.口头实现2.利用语法特性 三.实现方式&#xff08;饿汉式懒汉式&#xff09;1.饿汉式2.懒汉式3.线程安全的单例模式4.双重检查锁5.禁止指令重排序 一.什么是单例模式 单例模式&#xff08;Singleton Pattern&#xff…

数据集学习笔记(六):目标检测和图像分割标注软件介绍和使用,并转换成YOLO系列可使用的数据集格式

文章目录 一、目标检测1.1 labelImg1.2 介绍1.3 安装1.4 使用1.5 转换1.6 验证 二、图像分割2.1 labelme2.2 介绍2.3 安装2.4 使用2.5 转换2.6 验证 一、目标检测 1.1 labelImg 1.2 介绍 labelImg是一个开源的图像标注工具&#xff0c;用于创建图像标注数据集。它提供了一个…

2023-08-30 LeetCode每日一题(到家的最少跳跃次数)

2023-08-30每日一题 一、题目编号 1654. 到家的最少跳跃次数二、题目链接 点击跳转到题目位置 三、题目描述 有一只跳蚤的家在数轴上的位置 x 处。请你帮助它从位置 0 出发&#xff0c;到达它的家。 跳蚤跳跃的规则如下&#xff1a; 它可以 往前 跳恰好 a 个位置&#x…

OpenCVSharp入门学习①-获取本地摄像头数据

1. nuget包安装opencvsharp4和opencvsharp4.extensiongs和opencvsharp4.runtime.win 如果不安装opencvsharp4.runtime.win的话会报 System.TypeInitializationException:““OpenCvSharp.Internal.NativeMethods”的类型初始值设定项引发异常。”DllNotFoundException: 无法加…

vue v-for 例子

vue v-for 例子 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head&…

AssemblyManager 程序集管理器

AssemblyManager 程序集管理器 程序执行中使用反射对框架的搭建有着强大的影响&#xff0c;如何管理程序集方便使用反射获取类型操作对象是本文章的重点 1.AssemblyInfo 对于一个程序集这里使用一个AssemblyInfo对象进行管理 Assembly &#xff1a;对应的程序集AssemblyTyp…

Java多线程与并发编程

课程地址&#xff1a; https://www.itlaoqi.com/chapter.html?sid98&cid1425 源码文档&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1WMvM3j6qhyjIeAT87kIcxg 提取码&#xff1a;5g56 Java多线程与并发编程 1-并发背后的故事什么是并发 2-你必须知道线程的概念程…

Cadence网表导出常见错误

前言 好不容易绘制出来原理图&#xff0c;结果导出报了很多条错误&#xff0c;由于哥们还是小白&#xff0c;所以很多事情还不懂&#xff0c;有错误的地方希望大佬们能够指出&#xff0c;主要还是以我遇到的为主。 生成网表时候的常见错误 36002-封装名缺失 36003-多part器…

pdf怎么调整大小kb?一分钟学会pdf压缩

PDF是一种常见的文件格式&#xff0c;有时候我们需要将PDF文件的大小进行压缩&#xff0c;以便于传输或存储&#xff0c;那么怎么调整PDF文件的大小呢&#xff1f;接下来就给大家分享几个简单又实用的方法&#xff0c;帮助我们轻松解决PDF文件过大的问题。 方法一&#xff1a;嗨…