协议:CC BY-NC-SA 4.0
一、声音的基本概念
本章着眼于音频的一些基本概念,包括模拟音频和数字音频。以下是一些资源:
- 《科学家和工程师数字信号处理指南》(
www.dspguide.com/
) - 音乐和计算机:一种理论和历史的方法(
http://music.columbia.edu/cmc/MusicAndComputers/
)作者:菲尔·伯克,拉里·波兰斯基,道格拉斯·雷佩托,玛丽·罗伯茨,丹·罗克莫尔
样本音频
音频是一种模拟现象。声音以各种方式产生,通过声音、乐器和自然事件,如森林中的树木倒下(无论是否有人听到)。在某一点接收到的声音可以被绘制成振幅对时间的曲线图,并且可以呈现几乎任何功能状态,包括不连续的。
对声音的分析通常是通过观察它的频谱来完成的。从数学上来说,这是通过傅里叶变换实现的,但是耳朵仅通过耳朵的结构来执行几乎类似的变换。耳朵听到的“纯”声音对应于简单的正弦波,谐波对应于正弦波,其频率是基本正弦波的倍数。
系统中的模拟信号,例如模拟音频放大器,被设计成与这些频谱信号一起工作。他们试图在整个音频范围内产生相同的放大效果。
计算机和越来越多的电子设备处理由 1 和 0 组成的数字信号。位被组合成具有 256 个可能值的字节、具有 65,536 个可能值的 16 位字,甚至更大的组合,例如 32 位或 64 位字。
抽样率
将模拟信号数字化意味着以固定的时间间隔从该信号中取样,并以离散的尺度表示这些样本。采样的频率就是采样率。例如,CD 上的音频采样频率为 44,100Hz,即每秒 44,100 次。在 DVD 上,每秒可以采样 192,000 次,采样率为 192kHz。相反,标准电话采样速率为 8kHz。
图 1-1 表示取样。
图 1-1。
Analog and sampled signal (Wikipedia: http://en.wikipedia.org/wiki/Pulse-code_modulation
)
采样率影响两个主要因素。首先,采样率越高,数据量越大。在其他条件相同的情况下,采样速率加倍将导致数据需求加倍。另一方面,奈奎斯特-香农定理( http://en.wikipedia.org/wiki/Nyquist_theorem
)对连续数据的采样精度设置了限制:如果信号中的最高频率低于采样速率的一半,则模拟信号只能从数字信号中重构(换句话说,是无失真的)。
这也是关于黑胶唱片和 CD 唱片“质量”的争论结束的地方,就像“黑胶唱片和 CD 唱片的神话不会消亡”( www.eetimes.com/electronics-blogs/audio-designline-blog/4033509/Vinyl-vs-CD-myths-refuse-to-die
)。采样速率为 44.1kHz 时,当转换回模拟信号以用于扬声器或耳机时,原始信号中高于 22.05kHz 的频率可能无法准确再现。由于人类的典型听力范围只有 20,000 赫兹(我的听力范围现在降到了 10,000 赫兹),所以这应该不是一个大问题。但是一些音响发烧友声称他们的耳朵非常灵敏!
样本格式
样本格式是音频数字化的另一个主要特征:用于离散样本的位数。例如,电话信号使用 8kHz 采样速率和 8 位分辨率,因此电话信号只能传送 2⁸(换句话说,256 个)级别(参见 http://electronics.howstuffworks.com/telephone3.htm
“电话如何工作”)。
大多数 CD 和计算机系统使用 16 位格式,提供非常精细的信号渐变,允许 96dB 的范围(参见http://manual.audacityteam.org/man/Digital_Audio
“Audacity:数字采样”)。
框架
一个帧保存一个时间实例的所有样本。对于立体声设备,每帧保存两个样本,而对于五扬声器设备,每帧保存五个样本。
脉冲编码调制
脉码调制(PCM)是代表数字化模拟信号的标准形式。根据维基百科( http://en.wikipedia.org/wiki/Pulse-code_modulation
),“脉冲编码调制是一种用于数字表示采样模拟信号的方法。它是计算机和各种蓝光、DVD 和 CD 格式的数字音频的标准格式,也是数字电话系统等其他应用的标准格式。PCM 流是模拟信号的数字表示,其中模拟信号的幅度以均匀的间隔有规律地采样,每个样本被量化为数字步长范围内最接近的值。
PCM 流有两个基本属性,决定了它们对原始模拟信号的保真度:采样率,即每秒采样的次数;位深度,决定了每个样本可以采用的可能数字值的数量。
然而,即使这是标准,也有变体( http://wiki.multimedia.cx/index.php?title=PCM
)。主要的一个是关于基于单词的系统中的字节表示:小端或大端( http://searchnetworking.techtarget.com/definition/big-endian-and-little-endian
)。下一个变化是有符号对无符号( http://en.wikipedia.org/wiki/Signedness
)。
还有许多其他不太重要的变量,例如数字化是线性的还是对数的。参见 http://wiki.multimedia.cx/index.php?title=PCM
的多媒体维基,了解关于这些的讨论。
超限和欠载
根据“用 ALSA 进行声音编程的介绍”( www.linuxjournal.com/article/6735?page=0,1
),“当声音设备处于活动状态时,数据在硬件和应用缓冲区之间不断地传输。在数据捕获(记录)的情况下,如果应用读取缓冲区中的数据不够快,循环缓冲区将被新数据覆盖。由此导致的数据丢失称为溢出。在回放过程中,如果应用没有足够快地将数据传入缓冲区,它就会变得缺乏数据,从而导致称为欠载运行的错误。”
潜伏
延迟是指从信号进入系统到信号(或其等效物,如放大版本)离开系统所经过的时间。
根据伊恩·沃(Ian Waugh)的《修复音频延迟第一部分》( www.practicalpc.co.uk/computing/sound/latency1.htm
),“延迟就是延迟。在基于计算机的音乐音频系统中,这一问题最为明显,表现为触发信号和听到信号之间的延迟,例如,按下 MIDI 键盘上的一个键,然后通过声卡听到声音播放。”
这就像一个延迟的反应,如果延迟很大,就不可能及时播放任何东西,因为你听到的声音总是比你正在播放的声音落后一点点,这让人分心。
这种延迟在引起问题之前不必太大。许多人可以在大约 40 毫秒的延迟下工作,即使延迟是显而易见的,尽管如果你正在播放烟火音乐,它可能太长了。
理想的延迟是 0,但许多人很难注意到小于 8 毫秒或 10 毫秒的延迟,许多人可以在 20 毫秒的延迟下愉快地工作。
在谷歌上搜索测量音频延迟会出现很多网站。我使用一个简单的测试。我在一台单独的电脑上安装了 Audacity,用它同时录制我发出的声音和测试电脑拾取和播放的声音。我用勺子猛敲瓶子,发出尖锐的敲击声。当放大时,记录的声音显示两个峰值,选择峰值之间的区域向我显示了选择开始/结束的潜伏期。在图 1-2 中,这两个是 17.383 和 17.413 秒,延迟为 30 毫秒。
图 1-2。
Measuring latency with Audacity
振动
将定期对模拟信号进行采样。理想情况下,回放应该使用完全相同的时间间隔。但是,特别是在网络系统中,周期可能没有规律。任何不规则都称为抖动( http://en.wikipedia.org/wiki/Jitter
)。我没有测试抖动的简单方法;我的主要问题仍然是延迟!
混合
混合意味着从一个或多个源获取输入,可能对这些输入信号进行一些处理,然后将它们发送到一个或多个输出。当然,起源是物理混频器,它作用于模拟信号。在数字世界中,同样的功能将在数字信号上执行。
描述模拟混音器的一个简单文档是“混音指南”( www.soundcraft.com/support/gtm_booklet.aspx
)。它包括以下功能:
- 将输入路由至输出
- 为不同的输入和输出信号设置增益和输出电平
- 应用特殊效果,如混响、延迟和音高移动
- 将输入信号混合到公共输出
- 将一个输入信号分成多个输出
结论
这短短的一章介绍了一些基本概念,它们将占据本书其余部分的大部分篇幅。Steven W. Smith 的《科学家和工程师数字信号处理指南( www.dspguide.com/
)有大量的进一步细节,
二、用户级工具
本章着眼于 Linux 系统下典型的用户级工具,包括播放器、各种声音处理工具和编辑器。
演员
下面几节说说玩家。
MPlayer
我认为 MPlayer 很棒,可能比其他玩家用得都多。我通常从命令行运行它,但也有可用的 GUI 版本。它可以播放几乎任何媒体类型——视频和音频。我两样都用。它还将接受 HTTP URLs、DVD URLs 和 VCD URLs 等。
MPlayer 的手册页位于 www.mplayerhq.hu/DOCS/man/en/mplayer.1.html
,参考页位于 www.mplayerhq.hu/DOCS/HTML/en/index.html
。
MPlayer 有一个名为 MPlayerGUI 的 GUI 版本,但在 Ubuntu 的当前版本(如 16.04)下会出现问题,而且显然不会得到修复。有一个 Gnome 版本叫做 GNOME MPlayer,看起来像图 2-1 。
图 2-1。
GNOME MPlayer
可见光通讯
VLC 是我第二喜欢的运动员。它也可以播放几乎任何东西,并接受 HTTP,DVD 和 VCD 的网址。它有一个默认的 GUI,但是也可以在没有 GUI 的情况下用命令cvlc
运行。GUI 看起来如图 2-2 所示。
图 2-2。
VLC
它的主页面是 VideoLAN( www.videolan.org/vlc/index.html
),你可以在欢迎使用 VideoLAN 的文档( http://wiki.videolan.org/Documentation:Documentation
)找到一些文档。
图腾
图腾是常用的,但不是我的最爱之一。
声音工具
有许多声音工具能够执行多种任务,例如转换格式和应用效果。下面几节将介绍其中的一些。
短袜
是声音处理程序的瑞士军刀。最简单的用法是按如下方式更改文件格式:
sox audio.mp3 audio.ogg
这会将 MP3 文件转换成 Ogg-Vorbis 文件(您可能需要安装libsox-fmt-all
包才能处理所有文件格式)。
但是,它还可以执行许多其他功能,例如:
-
转换成单声道,如下图:
sox audio.mp3 audio.ogg channels 1
-
加倍音量,如下图:
sox audio.mp3 audio.ogg vol 2
-
改变采样率,如下图:
sox audio.mp3 audio.ogg rate 16k
它还可以执行更复杂的效果,如合并文件,分裂文件时,它检测到沉默,以及许多其他。
它的主页在 http://sox.sourceforge.net/
。
FFmpeg/avconv
FFmpeg 通常用作从一种格式到另一种格式的转换器。网站上有一系列不错的教程,shredder12 的初学者 FFmpeg 教程( http://linuxers.org/tutorial/ffmpeg-tutorial-beginners
)。
它也可用于从 ALSA 设备如hw:0
或默认设备进行记录。从hw:0
开始的记录可通过以下方式完成:
ffmpeg -f alsa -i hw:0 test.mp3
这可以通过默认的 ALSA 输入完成,如下所示:
ffmpeg -f alsa -i default test.mp3
几年前,FFmpeg 出现了一个分支,产生了 avconv,这是 Ubuntu 系统的默认设置。这两者之间有一些差异,但不足以证明对用户的滋扰因素。FFmpeg 和 avconv 在第十二章中有详细讨论。
GStreamer
GStreamer 允许您构建可以使用gst-launch
播放的“管道”。例如,要使用 ALSA 播放 MP3 文件,您将拥有以下管道:
gst-launch-1.0 filesrc location="concept.mp3" ! decodebin ! alsasink
管道可以执行更复杂的任务,如格式转换、混合等。查看马切伊·卡塔菲亚斯的教程“用 GStreamer 进行多用途多媒体处理”( www.ibm.com/developerworks/aix/library/au-gstreamer.html?ca=dgr-lnxw07GStreamer
)。
GStreamer 还可以使用以下内容播放 MIDI 文件:
gst-launch-1.0 filesrc location="rehab.mid" ! wildmidi ! alsasink
大胆
根据它的网站( http://audacity.sourceforge.net/
),“Audacity 是一款免费、易用、多语言的音频编辑器和录音机,适用于 Windows、Mac OS X、GNU/Linux 和其他操作系统。”这是一个神奇的工具,非常值得使用。后面的章节将会举例说明。
结论
这个简短的章节介绍了 Linux 下的一些用户级工具。这些是我经常使用的。虽然我已经列出了几个主要工具,但随便搜索一下就会发现更多。维基百科“Linux 音频软件列表”页面( https://en.wikipedia.org/wiki/List_of_Linux_audio_software
)有详尽的列表。
三、声音编解码器和文件格式
有许多不同的方式来表示声音数据。其中一些涉及压缩数据,这可能会丢失信息,也可能不会丢失信息。数据可以存储在文件系统中,也可以通过网络传输,这带来了其他问题。本章考虑主要的声音编解码器和容器格式。
概观
音频和视频数据需要以数字格式表示,以便计算机使用。音频和视频数据包含大量的信息,因此这些数据的数字表示会占用大量的空间。因此,计算机科学家开发了许多不同的方式来表示这些信息,有时以保存所有信息的方式(无损),有时以丢失信息的方式(有损)。
每种以数字方式表示信息的方式都被称为编解码器。最简单的方法是将其表示为“原始”脉码调制(PCM)数据,这将在下一节介绍。声卡等硬件设备可以直接处理 PCM 数据,但 PCM 数据会占用大量空间。
大多数编解码器会尝试通过将 PCM 数据编码为另一种形式(称为编码数据)来减少 PCM 数据的内存需求。需要时,可以将其解码回 PCM 格式。根据编解码器算法,重新生成的 PCM 可能与原始 PCM 数据具有相同的信息内容(无损),也可能包含较少的信息(有损)。
编码的音频数据可以包含也可以不包含关于数据属性的信息。该信息可以是关于原始 PCM 数据的,例如声道(单声道、立体声)的数量、采样速率、样本中的位数等等。或者它可以是关于编码过程本身的信息,例如成帧数据的大小。编码数据连同这些附加信息可以存储在文件中,通过网络传输,等等。如果这样做了,编码数据加上附加信息就被合并到一个容器中。
有时,知道您是在处理编码的数据还是在处理保存这些数据的容器是很重要的。例如,磁盘上的文件通常是容器,除了编码数据之外,还保存着其他信息。但是音频数据操作库通常在附加数据被移除之后处理编码数据本身。
脉冲编码调制
这个定义来自维基百科:“脉码调制是一种用于数字表示采样模拟信号的方法。它是计算机和各种蓝光、DVD 和 CD 格式的数字音频的标准格式,也是数字电话系统等其他应用的标准格式。PCM 流是模拟信号的数字表示,其中模拟信号的幅度以均匀的间隔有规律地采样,每个样本被量化为数字步长范围内最接近的值。
PCM 流有两个基本属性,决定了它们对原始模拟信号的保真度:采样率,即每秒采样的次数;位深度,决定了每个样本可以采用的可能数字值的数量。
PCM 数据可以作为“原始”数据存储在文件中。在这种情况下,没有头信息来说明采样率和位深度是什么。许多工具如sox
使用文件扩展名来确定这些属性。
根据 http://sox.sourceforge.net/soxformat.html
,“f32 和 f64 分别表示编码为 32 位和 64 位(IEEE 单精度和双精度)浮点 PCM 的文件;s8、s16、s24 和 s32 分别表示 8、16、24 和 32 位有符号整数 PCMu8、u16、u24 和 u32 分别表示 8、16、24 和 32 位无符号整数 PCM。
但是应该注意,文件扩展名只是帮助理解一些 PCM 编解码器参数以及它们如何存储在文件中。
声音资源文件
WAV 是一种将音频数据包装成容器的文件格式。音频数据通常是 PCM。文件格式基于资源交换文件格式(RIFF)。虽然它是微软/IBM 的格式,但似乎不受专利的限制。
Topherlee ( www.topherlee.com/software/pcm-tut-wavformat.html
)对这种格式给出了很好的描述。WAV 文件头包含有关 PCM 编解码器的信息,以及有关其存储方式的信息(例如,little-endian 或 big-endian)。
因为 WAV 文件通常包含未压缩的音频数据,所以它们通常很大,一首三分钟的歌曲大约需要 50Mb。
MP3 文件
MP3 和相关格式受专利保护(实际上,有很多专利)。要使用编码器或解码器,用户应该向弗劳恩霍夫协会这样的组织支付许可费。大多数临时用户既没有这样做,也没有意识到他们应该这样做,但弗劳恩霍夫( www.itif.org/files/2011-fraunhofer-boosting-comp.pdf
)报道称,2011 年 MP3 专利“产生了大约 3 亿美元的年度税收”弗劳恩霍夫协会目前已经选择不追求免费开源实现编码器和解码器的版税。
MP3 使用的编解码器是 MPEG-1 音频层 III ( http://en.wikipedia.org/wiki/MP3
)音频压缩格式。这包括一个 header 组件,它给出了关于数据和压缩算法的所有附加信息。不需要单独的容器格式。
还有沃比斯
奥格·沃尔比斯是“好人”之一。根据 Vorbis.com 的说法,“Ogg Vorbis 是一种完全开放、无专利、专业的音频编码和流媒体技术,拥有开源的所有优势。”
这些名称细分如下:
- Ogg: Ogg 是 Xiph.org 的音频、视频和元数据容器格式的名称。这将流数据放入更容易在文件和其他东西中管理的帧中。
- Vorbis: Vorbis 是 Ogg 中包含的特定音频压缩方案的名称。注意,其他格式也可以嵌入 Ogg 中,比如 FLAC 和 Speex。
扩展名.oga
是 Ogg 音频文件的首选,尽管以前曾使用过.ogg
。
有时有必要密切注意 Ogg 和 Vorbis 之间的区别。例如,OpenMAX IL 有许多标准音频组件,包括一个用于解码各种编解码器的组件。具有“音频解码器 ogg”角色的 LIM 组件可以解码 Vorbis 流。但是即使组件包括 ogg 这个名字,它也不能解码 Ogg 文件,Ogg 文件是 Vorbis 流的容器。它只能解码 Vorbis 流。解码 Ogg 文件需要使用不同的组件,称为“带帧的音频解码器”
WMA 格式
从开源的角度来看,WMA 文件是邪恶的。WMA 文件基于两种微软专有格式。第一种是高级系统格式(ASF)文件格式,它描述了音乐数据的“容器”。第二个是 Windows Media Audio 9 编解码器。
ASF 是首要问题。微软有一个公开的规范( www.microsoft.com/en-us/download/details.aspx?id=14995
),强烈反对任何开源的东西。许可证规定,如果您基于该规范构建一个实现,那么您:
- 无法分发源代码
- 只能分发目标代码
- 除非作为“解决方案”的一部分,否则不能分发目标代码(换句话说,库似乎是被禁止的)
- 不能免费分发您的目标代码
- 无法将您的许可证设置为允许衍生作品
而且更重要的是,2012 年 1 月 1 日之后不允许你开始任何新的实施,而且(在撰写本文时)已经是 2017 年了!
只是说的更难听一点,微软有专利 6041345,“用于容纳多个媒体流的活动流格式”( www.google.com/patents/US6041345
),1997 年 3 月 7 日在美国申请。该专利似乎覆盖了与当时存在的许多其他格式相同的领域,因此该专利的地位(如果受到质疑)尚不清楚。但是,它已经被用来阻止 GPL 授权的项目 VirtualDub ( www.advogato.org/article/101.html
)支持 ASF。文件格式的专利状态有点可疑,但现在可能会变得稍微清楚一些,因为 Oracle 已经失去了对 Java API 专利的要求。
尽管如此,FFmpeg 项目( http://ffmpeg.org/
)还是完成了 ASF 的净室实现,对文件格式进行逆向工程,并且根本不使用 ASF 规范。它还逆向工程 WMA 编解码器。这使得像 MPlayer 和 VLC 这样的播放器可以播放 ASF/WMA 文件。FFmpeg 本身也可以从 ASF/WMA 转换成更好的格式,比如 Ogg Vorbis。
没有用于 WMA 文件的 Java 处理程序,考虑到许可,除非是基于 FFmpeg 的本地代码,否则不太可能有。
-水手
根据 Matroska 的网站( http://matroska.org/
),Matroska 的目标是成为多媒体容器格式的标准。它源自一个名为 MCF 的项目,但与它有显著的区别,因为它基于可扩展二进制元语言(EBML),XML 的二进制衍生物。它包含了现代容器格式的一些特性,如下所示:
- 在文件中快速查找
- 章节条目
- 完整的元数据(标签)支持
- 可选字幕/音频/视频流
- 模块化可扩展
- 错误恢复能力(即使在流损坏时也能恢复播放)
- 可通过互联网和本地网络(HTTP、CIFS、FTP 等)流式传输
- 菜单(像 DVD 一样)
直到我开始看字幕, 1 我才知道 Matroska,它可以(可选地)添加到视频中,似乎是视频的主要格式之一。
在 Ubuntu 资源库中,mkvmerge 是一个 GUI 工具,用于创建和管理 Matroska 文件格式(MKV)的字幕。超级链接" https://mkvtoolnix.download/
" MKVToolNix 是一个处理 MKV 文件的 GUI 工具。
结论
声音有许多编码解码器,而且更多的正在被设计中。它们可以是编解码器、容器,或者两者兼而有之,它们具有各种各样的特性,有些还带有专利之类的障碍。
Footnotes 1
字幕和隐藏式字幕相似但有区别。根据 https://www.accreditedlanguage.com/2016/08/18/subtitles-and-captions-whats-the-difference/
的说法,“字幕是最常用的一种将媒体翻译成另一种语言的方式,以便说其他语言的人可以欣赏它。另一方面,字幕更常用于帮助失聪和听力受损的观众。”
四、Linux 声音架构概述
像大多数 Linux 一样,Linux 声音系统已经从一个简单的系统发展成一个复杂得多的系统。本章给出了 Linux 声音系统的组件的高级概述,以及哪些位最适合用于哪些用例。
资源
以下是一些资源:
- Lennart poeting(
http://0pointer.de/blog/projects/guide-to-sound-apis.html
)的 Linux 声音 API 丛林指南。 - 《工作原理:Linux 音频解释》作者 TuxRadar (
http://tuxradar.com/content/how-it-works-linux-audio-explained
)。 - 疯狂的编码者发布了一篇支持 Linux 中声音的 OSSv4 状态的文章毕竟不是那么抱歉(
http://insanecoding.blogspot.com.au/2009/06/state-of-sound-in-linux-not-so-sorry.html
),引来了很多评论。
成分
图 4-1 表示 Linux 声音系统的不同层次。
图 4-1。
Layers of audio tools and devices
设备驱动程序
底层是硬件本身,音频设备。这些设备是由不同制造商制造的声卡,它们都有不同的功能、接口和价格。就像任何硬件一样,为了让它对操作系统可见和有用,必须有一个设备驱动程序。当然,有成千上万为 Linux 编写的设备驱动程序。编写 Linux 设备驱动程序本身就是一门专业,并且有专门的资料来源,例如 Jonathan Corbet、Alessandro Rubini 和 Greg Kroah-Hartman 编写的 Linux 设备驱动程序第三版( http://lwn.net/Kernel/LDD3/
)。
设备驱动程序必须在“顶部”有标准化的 API,以便设备用户有一个已知的接口来编码。OSS 设备驱动程序 API 被用于音频设备,直到它成为闭源,在这一点上开发人员切换到 ALSA API。当 OSS v4 再次开放时,内核支持 ALSA 接口,而 OSS 不支持。
理想情况下,设备驱动程序 API 应该公开硬件的所有特性,同时不增加额外的负担。对于音频,为音频驱动程序应该做的事情设定界限并不总是那么容易。例如,一些声卡将支持不同来源的模拟信号的混合,而另一些则不支持,一些声卡将具有 MIDI 合成器,而另一些则没有。如果 API 要为支持这些功能的声卡公开这些功能,那么它可能必须在软件中为不支持这些功能的声卡提供这些功能。
关于编写 ALSA 设备驱动程序的文档数量有限。位于 www.alsa-project.org/main/index.php/ALSA_Driver_Documentation
的“ALSA 驱动程序文档”页面指向一些文档,包括 2005 年由岩井隆(Takashi Iwai)编写的关于 ALSA 设备驱动程序( www.alsa-project.org/∼tiwai/writing-an-alsa-driver/
)的文档。还有本·科林斯 2010 年在 http://ben-collins.blogspot.com.au/2010/05/writing-alsa-driver-basics.html
的博客,“写一个 ALSA 司机。”否则,似乎帮助不大。
声音服务器
Linux 是一个多任务、多线程的操作系统。并发进程可能想要同时向声卡写入声音。例如,一个邮件阅读器可能想要“叮”用户报告新邮件,即使他们正在一个嘈杂的计算机游戏中。这不同于声卡能够混合来自不同端口的声音的能力,例如 HDMI 输入端口和模拟输入端口。它需要能够混合(或管理)来自不同过程的声音。作为一个微妙的例子,每个进程的音量应该是单独可控的,还是目的地端口(耳机或扬声器)应该是单独可控的?
这些功能超出了设备驱动程序的范围。Linux 通过“声音服务器”解决了这个问题,声音服务器运行在设备驱动之上,管理这些更复杂的任务。在这些声音服务器之上是与声音服务器对话的应用,声音服务器又将结果数字信号传递给设备驱动程序。
这就是声音服务器之间的显著差异。对于专业音频系统,声音服务器必须能够以最小的延迟或其他负面影响来处理和发送音频。对于消费音频,对音量和目的地的控制可能比延迟更重要;你可能不会在意一条新信息“叮”多花了半秒钟。在这两者之间可能还有其他情况,例如需要音频和视觉效果同步的游戏以及需要模拟和数字源同步的 Karaoke 播放器。
Linux 下的两大声音服务器是用于专业音频的 Jack 和用于消费系统的 PulseAudio。它们是为不同的用例设计的,因此提供不同的特性。
Lennart Poettering 在“Linux Sound API 丛林指南”( http://0pointer.de/blog/projects/guide-to-sound-apis.html
)中很好地总结了这些不同的用例:
- "我想写一个类似媒体播放器的应用!"使用 GStreamer(除非您只关注 KDE,在这种情况下,声子可能是一种替代方法)。
- "我想在我的应用中添加事件声音!"使用 libcanberra,并根据 XDG 声音主题/命名规范安装声音文件(除非您只关注 KDE,在这种情况下,KNotify 可能是一个替代选择,尽管它有不同的关注点)。
- "我想做专业的音频编程、硬盘录音、音乐合成、MIDI 接口!"使用插孔和/或完整的 ALSA 接口。
- "我想做基本的 PCM 音频回放/捕获!"使用安全的 ALSA 子集。
- “我要给我的游戏加声音!”全屏游戏用 SDL 的音频 API,Gtk+等标准 ui 的简单游戏用 libcanberra。
- “我要写一个混音器应用!”使用您想要直接支持的层:如果您想要支持增强的桌面软件混音器,请使用 PulseAudio 音量控制 API。如果你想支持硬件混音器,使用 ALSA 混音器 API。
- “我要给水暖层写音频软件!”使用完整的 ALSA 堆栈。
- “我想写嵌入式应用的音频软件!”对于技术设备,通常安全的 ALSA 子集是一个很好的选择。然而,这在很大程度上取决于您的用例。
复杂性
图 4-1 隐藏了 Linux 声音的真正复杂性。迈克·梅兰森(Adobe 工程师)在 2007 年制作了如图 4-2 所示的图表。
图 4-2。
Linux audio relationships
这个数字不是最新的。例如,OSS 不再是 Linux 的主要部分。一些特殊情况的复杂性是,例如,PulseAudio 位于 ALSA 之上,它也位于 ALSA 之下,如图 4-3 (基于 http://insanecoding.blogspot.com.au/2009/06/state-of-sound-in-linux-not-so-sorry.html
的那个)。
图 4-3。
ALSA and PulseAudio . This diagram is upside down compared to mine
解释如下:
- PulseAudio 可以做 ALSA 做不到的事情,比如混合应用的声音。
- PulseAudio 将其自身安装为默认的 ALSA 输出设备。
- 一个应用将音频发送到 ALSA 默认设备,后者将音频发送到 PulseAudio。
- PulseAudio 将其与任何其他音频混合,然后将其发送回 ALSA 的特定设备。
- 然后,ALSA 弹奏混合音。
是的,很复杂,但是它完成了原本很难完成的任务。
结论
Linux 声音系统的架构是复杂的,并且新的皱纹会定期添加进来。然而,这对于任何音频系统都是一样的。后续章节将充实这些组件的细节。
五、驱动
ALSA 是声卡的底层接口。如果您正在构建自己的声音服务器系统或编写设备驱动程序,那么您会对 ALSA 感兴趣。它位于当前大多数 Linux 系统的底部,所以要理解它们,你可能需要理解 ALSA 的方方面面。如果没兴趣,可以继续。
资源
以下是一些资源:
- 杰夫·特兰特的《ALSA 声音编程入门》(
www.linuxjournal.com/article/6735?page=0,1
) - 近距离观察 ALSA (
www.volkerschatz.com/noise/alsa.html
) - ALSA API (
www.alsa-project.org/alsa-doc/alsa-lib/
- ALSA 编程指南(
www.suse.de/~mana/alsa090_howto.html
) - Linux sound HOWTO for ALSA 用户(
http://techpatterns.com/forums/about1813.html
)来自技术模式
用户空间工具
ALSA 既是一组与声卡对话的 API,也是一组用户级应用,当然是使用 ALSA API 构建的。它包括查询和控制声卡以及从声卡上录音和播放的命令。本节考虑命令行工具。
alsamixer
在终端窗口中运行,允许你选择声卡和控制这些卡上的接口。看起来像图 5-1 。
图 5-1。
alsamixer display
amixer
是一个具有类似功能的命令行应用。
与第一章中描述的通用混音器功能相比,混音器功能非常有限:
- 设置输出和输入通道的回放和采集音量
- 使卡静音或取消静音
Stephen C. Phillips 的文档“具有 ALSA 的 Raspberry Pi 上的声音配置”( http://blog.scphillips.com/2013/01/sound-configuration-on-raspberry-pi-with-alsa/
)适用于所有其他 ALSA 系统,而不仅仅是 Raspberry Pi。
alsactl
这是一个简单的 ALSA 配置控制程序。
扬声器测试
该命令允许您测试哪些输出会到达哪里。例如,对于五声道声音,运行以下命令:
speaker-test -t wav -c 5
这将在我的默认声卡上产生以下文本和音频:
speaker-test 1.0.25
Playback device is default
Stream parameters are 48000Hz, S16_LE, 5 channels
WAV file(s)
Rate set to 48000Hz (requested 48000Hz)
Buffer size range from 39 to 419430
Period size range from 12 to 139810
Using max buffer size 419428
Periods = 4
was set period_size = 104857
was set buffer_size = 419428
0 - Front Left
1 - Front Right
2 - Rear Left
3 - Rear Right
4 - Center
Time per period = 12.948378
它还会向相关的说话者播放短语“左前方”等。
展平/圆角
这将播放一个文件或记录到一个文件中。要向扬声器播放麦克风,请使用:
arecord -r 44100 --buffer-size=128 | aplay --buffer-size=128
要将其记录到文件中,请使用以下命令:
arecord -f dat -d 20 -D hw:0,0 test.wav
这将在您第一个可用的声卡(hw:0,0
)上以 DAT 质量录制一个 20 秒的 WAV 文件。DAT 质量定义为以 48kHz 采样速率和 16 位分辨率录制的立体声数字音频。
识别 ALSA 卡片
最简单的方法是使用-l
选项运行aplay
和arecord
,如下所示:
arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: STAC92xx Analog [STAC92xx Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: Pro [SB X-Fi Surround 5.1 Pro], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: STAC92xx Analog [STAC92xx Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 3: HDMI 0 [HDMI 0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 7: HDMI 1 [HDMI 1]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 8: HDMI 2 [HDMI 2]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: Pro [SB X-Fi Surround 5.1 Pro], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: Pro [SB X-Fi Surround 5.1 Pro], device 1: USB Audio [USB Audio #1]
Subdevices: 1/1
Subdevice #0: subdevice #0
设备名称
在诸如qjackctl
的程序中,这些卡片通常被赋予诸如hw:0
或hw:2.2
的名称(参见第七章)。术语hw
指的是硬件设备。主号是指卡号,副号是指设备号。设备的名称在括号中。
设备也可能有别名。命令aplay -L
列出了设备别名。例如,hdmi
别名是在我的系统上的配置文件/etc/asound.conf
中定义的。
pcm.hdmi0 {
type hw
card 1
device 3 }
pcm.hdmi1 {
type hw
card 1
device 7 }
pcm.hdmi2 {
type hw
card 1
device 8 }
所以,hdmi:0
其实就是hw:1,3
:卡 1,设备 3。
可以定义其他别名来涵盖一系列设备,通过卡和设备进行参数化。例如,/usr/share/alsa/pcm/surround40.conf
定义如下:
pcm.!surround40 {
@args [ CARD DEV ]
@args.CARD {
type string
default {
@func getenv
vars [
ALSA_SURROUND40_CARD
ALSA_PCM_CARD
ALSA_CARD
]
default {
@func refer
name defaults.pcm.surround40.card
}
}
}
@args.DEV {
type integer
default {
@func igetenv
vars [
ALSA_SURROUND40_DEVICE
]
default {
@func refer
name defaults.pcm.surround40.device
}
}
}
...
}
例如,这将surround40:CARD=PCH,DEV=0
定义为hw:0,0
在我的系统上的别名(PCH
是卡 0)。
我不知道从card 1, device 3
到hdmi:0
的简单编程方式。
您可以使用aplay
和arecord
显示别名集。
我的系统上来自aplay -L
的输出如下:
default
Default
sysdefault:CARD=PCH
HDA Intel PCH, STAC92xx Analog
Default Audio Device
front:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
Front speakers
surround40:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
4.0 Surround output to Front and Rear speakers
surround41:CARD=PCH,DE
V=0
HDA Intel PCH, STAC92xx Analog
4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
hdmi:CARD=NVidia,DEV=0
HDA NVidia, HDMI 0
HDMI Audio Output
hdmi:CARD=NVidia,DEV=1
HDA NVidia, HDMI 1
HDMI Audio Output
hdmi:CARD=NVidia,DEV=2
HDA NVidia, HDMI 2
HDMI Audio Output
sysdefault:CARD=Pro
SB X-Fi Surround 5.1 Pro, USB Audio
Default Audio Device
front:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
Front speakers
surround40:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
4.0 Surround output to Front and Rear speakers
surround41:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
iec958:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
IEC958 (S/PDIF) Digital Audio Output
arecord -L
的输出如下:
default
Default
sysdefault:CARD=PCH
HDA Intel PCH, STAC92xx Analog
Default Audio Device
front:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
Front speakers
surround40:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
4.0 Surround output to Front and Rear speakers
surround41:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
sysdefault:CARD=Pro
SB X-Fi Surround 5.1 Pro, USB Audio
Default Audio Device
front:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
Front speakers
surround40:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
4.0 Surround output to Front and Rear speakers
surround41:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=Pr
o,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
iec958:CARD=Pro,DEV=0
SB X-Fi Surround 5.1 Pro, USB Audio
IEC958 (S/PDIF) Digital Audio Output
ALSA 配置文件
Volker Schatz 的这个教程解释了 ALSA 配置文件中正在发生的事情,真的很好:“近距离观察 ALSA”(www.volkerschatz.com/noise/alsa.html
)。
请注意,默认的 ALSA 设备是hw:0
。这是硬编码到 ALSA。但是它可以在配置文件中被覆盖。例如,这可以通过 PulseAudio 来实现(见下一章)。
阿尔萨信息
这将收集有关您的系统的信息,并将其保存在一个文件中。这是一个提供大量信息的 shell 脚本。这是一个被严重删减的信息子集:
upload=true&script=true&cardinfo=
!!################################
!!ALSA Information Script v 0.4.60
!!################################
!!Script ran on: Tue Jun 12 04:50:22 UTC 2012
!!Linux Distribution
!!------------------
Fedora release 16 (Verne) Fedora release 16 (Verne) Fedora release 16 (Verne) Fedora release 16 (Verne)
...
!!ALSA Version
!!------------
Driver version: 1.0.24
Library version: 1.0.25
Utilities version: 1.0.25
!!Loaded ALSA modules
!!-------------------
snd_hda_intel
snd_hda_intel
!!Sound Servers on this system
!!----------------------------
Pulseaudio:
Installed - Yes (/usr/bin/pulseaudio)
Running - Yes
Jack:
Installed - Yes (/usr/bin/jackd)
Running - No
!!Soundcards recognised by ALSA
!!-----------------------------
0 [PCH ]: HDA-Intel - HDA Intel PCH
HDA Intel PCH at 0xe6e60000 irq 47
1 [NVidia ]: HDA-Intel - HDA NVidia
HDA NVidia at 0xe5080000 irq 17
!!PCI Soundcards installed in the system
!!--------------------------------------
00:1b.0 Audio device
: Intel Corporation 6 Series/C200 Series Chipset Family High Definition Audio Controller (rev 04)
01:00.1 Audio device: nVidia Corporation HDMI Audio stub (rev a1)
...
!!HDA-Intel Codec information
!!---------------------------
...
Default PCM:
rates [0x5e0]: 44100 48000 88200 96000 192000
bits [0xe]: 16 20 24
formats [0x1]: PCM
Node 0x0a [Pin Complex] wcaps 0x400583: Stereo Amp-In
Control: name="Mic Jack Mode", index=0, device=0
ControlAmp: chs=0, dir=In, idx=0, ofs=0
Control: name="Mic Capture Volume", index=0, device=0
ControlAmp: chs=3, dir=In, idx=0, ofs=0
Control: name="Mic Jack", index=0, device=0
Amp-In caps: N/A
Amp-In vals: [0x01 0x01]
Pincap 0x0001173c: IN OUT HP EAPD Detect
Vref caps: HIZ 50 GRD 80
EAPD 0x2: EAPD
Pin Default 0x03a11020: [Jack] Mic at Ext Left
Conn = 1/8, Color = Black
DefAssociation = 0x2, Sequence = 0x0
Pin-ctls: 0x24: IN VREF_80
Unsolicited: tag=03, enabled=1
Power: setting=D0, actual=D0
Connection: 3
0x13* 0x14 0x1c
!!ALSA configuration files
!!------------------------
!!System wide config file (/etc/asound.conf)
#
# Place your global alsa-lib configuration here...
#
@hooks [
{
func load
files [
"/etc/alsa/pulse-default.conf"
]
errors false
}
]
pcm.hdmi0 {
type hw
card 1
device 3 }
pcm.hdmi1 {
type hw
card 1
device 7 }
pcm.hdmi2 {
type hw
card 1
device 8 }
!!Aplay/Arecord output
!!------------
APLAY
**** List of PLAYBACK Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: STAC92xx Analog [STAC92xx Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 3: HDMI 0 [HDMI 0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 7: HDMI 1 [HDMI 1]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 8: HDMI 2 [HDMI 2]
Subdevices: 1/1
Subdevice #0: subdevice #0
ARECORD
**** List of CAPTURE Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: STAC92xx Analog [STAC92xx Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
!!Amixer output
!!-------------
!!-------Mixer controls for card 0 [PCH]
Card hw:0 'PCH'/'HDA Intel PCH at 0xe6e60000 irq 47'
Mixer name : 'IDT 92HD90BXX'
Components : 'HDA:111d76e7,10280494,00100102'
Controls : 19
Simple ctrls : 10
Simple mixer control 'Master',0
Capabilities: pvolume pvolume-joined pswitch pswitch-joined penum
Playback channels: Mono
Limits: Playback 0 - 64
Mono: Playback 62 [97%] [-1.50dB] [on]
Simple mixer control 'Headphone',0
Capabilities: pvolume pswitch penum
Playback channels: Front Left - Front Right
Limits: Playback 0 - 64
Mono:
Front Left: Playback 64 [100%] [0.00dB] [on]
Front Right: Playback 64 [100%] [0.00dB] [on]
Simple mixer control 'PCM',0
Capabilities: pvolume penum
Playback channels: Front Left - Front Right
Limits: Playback 0 - 255
Mono:
Front Left: Playback 254 [100%] [0.20dB]
Front Right: Playback 254 [100%] [0.20dB]
Simple mixer control 'Front',0
Capabilities: pvolume pswitch penum
Playback channels: Front Left - Front Right
Limits: Playback 0 - 64
Mono:
Front Left: Playback 64 [100%] [0.00dB] [on]
Front Right: Playback 64 [100%] [0.00dB] [on]
Simple mixer control 'Mic',0
Capabilities: cvolume penum
Capture channels: Front Left - Front Right
Limits: Capture 0 - 3
Front Left: Capture 1 [33%] [10.00dB]
Front Right: Capture 1 [33%] [10.00dB]
Simple mixer control 'Mic Jack Mode',0
Capabilities: enum
Items: 'Mic In' 'Line In'
Item0: 'Mic In'
Simple mixer control 'Beep',0
Capabilities: pvolume pvolume-joined pswitch pswitch-joined penum
Playback channels: Mono
Limits: Playback 0 - 3
Mono: Playback 1 [33%] [-12.00dB] [on]
Simple mixer control 'Capture',0
Capabilities: cvolume cswitch penum
Capture channels: Front Left - Front Right
Limits: Capture 0 - 46
Front Left: Capture 46 [100%] [30.00dB] [on]
Front Right: Capture 46 [100%] [30.00dB] [on]
Simple mixer control 'Dock Mic',0
Capabilities: cvolume penum
Capture channels: Front Left - Front Right
Limits: Capture 0 - 3
Front Left: Capture 0 [0%] [0.00dB]
Front Right: Capture 0 [0%] [0.00dB]
Simple mixer control 'Internal Mic',0
Capabilities: cvolume penum
Capture channels: Front Left - Front Right
Limits: Capture 0 - 3
Front Left: Capture 0 [0%] [0.00dB]
Front Right: Capture 0 [0%] [0.00dB]
!!-------Mixer controls for card 1 [NVidia]
Card hw:1 'NVidia'/'HDA NVidia at 0xe5080000 irq 17'
Mixer name : 'Nvidia GPU 1c HDMI/DP'
Components : 'HDA:10de001c,10281494,00100100'
Controls : 18
Simple ctrls : 3
Simple mixer control 'IEC958',0
Capabilities: pswitch pswitch-joined penum
Playback channels: Mono
Mono: Playback [on]
Simple mixer control 'IEC958',1
Capabilities: pswitch pswitch-joined penum
Playback channels: Mono
Mono: Playback [off]
Simple mixer control 'IEC958',2
Capabilities: pswitch pswitch-joined penum
Playback channels: Mono
Mono: Playback [off]
!!Alsactl output
!!-------------
--startcollapse--
state.PCH {
control.1 {
iface MIXER
name 'Front Playback Volume'
value.0 64
value.1 64
comment {
access 'read write'
type INTEGER
count 2
range '0 - 64'
dbmin -4800
dbmax 0
dbvalue.0 0
dbvalue.1 0
}
}
...
使用 ALSA 的应用
通过使用适当的命令行参数,许多应用可以直接使用 ALSA。
MPlayer
要使用 MPlayer 向 ALSA 设备播放文件,请使用如下代码:
mplayer -ao alsa:device=hw=1.0 -srate 48000 bryan.mp3
可见光通讯
要使用 VLC 向 ALSA 设备播放文件,请使用如下代码:
vlc --aout alsa ...
TiMidity
要使用 TiMidity 向 ALSA 设备播放文件,请使用如下代码:
timidity -Os ...
编程 ALSA
有几个关于编程 ALSA 的教程,包括保罗·戴维斯(他是 Jack 的领头人)的“使用 ALSA 音频 API 的教程”( http://equalarea.com/paul/alsa-audio.html
)。
你可以在 www.alsa-project.org/alsa-doc/alsa-lib/pcm.html
找到 API 的概述。杰夫·特兰特有一本《ALSA 声音编程入门》ALSA API 庞大而复杂,而且并不总是清楚它是如何组合在一起的,或者在哪里使用哪个部分。来自 ALSA 库 API ( www.alsa-project.org/main/index.php/ALSA_Library_API
)。
目前设计的界面如下:
- 信息界面(
/proc/asound
) - 控制界面(
/dev/snd/controlCX
) - ◆界面(
/dev/snd/mixerCXDX
) - PCM 接口(
/dev/snd/pcmCXDX
) - 原始 MIDI 接口(
/dev/snd/midiCXDX
) - 序列器接口(
/dev/snd/seq
) - 定时器界面(
/dev/snd/timer
)
信息接口是 ALSA 用于设备信息和一些控制目的的接口。
控制接口用于调节声卡提供的音量和其他控制功能。
混音器接口允许应用以透明的方式共享音频设备的使用,是 ALSA 的主要功能之一。
PCM 接口允许通过配置机制定义虚拟和硬件设备。它是数字音频应用的常用接口。
raw MIDI 接口用于与 MIDI 设备进行低级交互,并直接处理 MIDI 事件。
音序器接口用于比原始 MIDI 接口更高级别的 MIDI 应用。
计时器接口旨在使用声音硬件中的内部计时器,并允许声音事件同步。
硬件设备信息
查找硬件卡和设备的信息是一个多步骤的操作。首先必须识别硬件卡。这是使用控制界面( www.alsa-project.org/alsa-doc/alsa-lib/group___control.html
)功能完成的。使用的方法如下:
snd_card_next
snd_ctl_open
snd_ctl_pcm_next_device
snd_ctl_card_info_get_id
snd_ctl_card_info_get_name
卡片由 0 以上的整数标识。使用snd_card_next
找到下一个卡号,使用种子值-1 找到第一张卡。然后用它的 ALSA 名字打开卡,比如hw:0
、hw:1
等等、by snd_ctl_open
,其中填入一个handle
值。反过来,该句柄用于使用snd_ctl_card_info
填充卡片信息,并使用snd_ctl_card_info_get_name
等函数从该句柄中提取字段。在接下来的程序中,这将提供如下信息:
card 0: PCH [HDA Intel PCH]
有关更多信息,您需要切换到该卡的 PCM 功能。链接控制和 PCM 接口的函数是snd_ctl_pcm_info
,它用 PCM 相关信息填充类型为snd_pcm_info_t
的结构。不幸的是,该功能在 ALSA 文档的控制接口和 PCM 接口部分都没有记载,而是在control.c
下的文件部分。结构snd_pcm_info_t
在 PCM 接口( www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html#g2226bdcc6e780543beaadc319332e37b
)部分中几乎没有记载,并且只有几个感兴趣的字段。(结构见本站: www.qnx.com/developers/docs/6.4.0/neutrino/audio/libs/snd_pcm_info_t.html
)。)使用 PCM 功能snd_pcm_info_get_id
和snd_pcm_info_get_name
访问这些字段。
snd_pcm_info_t
结构的主要价值在于它是 PCM 流( www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m___info.html
)函数的主要参数。特别是,这允许您获得设备和子设备以及关于它们的信息。
查找和显示卡和硬件设备信息的程序是aplay-l.c
,如下图所示:
/**
* aplay-l.c
*
* Code from aplay.c
*
* does the same as aplay -l
* http://alsa-utils.sourcearchive.com/documentation/1.0.15/aplay_8c-source.html
*/
/*
* Original notice:
*
* Copyright (c) by Jaroslav Kysela <perex@perex.cz>
* Based on vplay program by Michael Beck
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <locale.h>
// used by gettext for i18n, not needed here
#define _(STR) STR
static void device_list(snd_pcm_stream_t stream)
{
snd_ctl_t *handle;
int card, err, dev, idx;
snd_ctl_card_info_t *info;
snd_pcm_info_t *pcminfo;
snd_ctl_card_info_alloca(&info);
snd_pcm_info_alloca(&pcminfo);
card = -1;
if (snd_card_next(&card) < 0 || card < 0) {
error(_("no soundcards found..."));
return;
}
printf(_("**** List of %s Hardware Devices ****\n"),
snd_pcm_stream_name(stream));
while (card >= 0) {
char name[32];
sprintf(name, "hw:%d", card);
if ((err = snd_ctl_open(&handle, name, 0)) < 0) {
error("control open (%i): %s", card, snd_strerror(err));
goto next_card;
}
if ((err = snd_ctl_card_info(handle, info)) < 0) {
error("control hardware info (%i): %s", card, snd_strerror(err));
snd_ctl_close(handle);
goto next_card;
}
dev = -1;
while (1) {
unsigned int count;
if (snd_ctl_pcm_next_device(handle, &dev)<0)
error("snd_ctl_pcm_next_device");
if (dev < 0)
break;
snd_pcm_info_set_device(pcminfo, dev);
snd_pcm_info_set_subdevice(pcminfo, 0);
snd_pcm_info_set_stream(pcminfo, stream);
if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
if (err != -ENOENT)
error("control digital audio info (%i): %s", card, snd_strerror(err));
continue;
}
printf(_("card %i: [%s,%i] %s [%s], device %i: %s [%s]\n"),
card, name, dev, snd_ctl_card_info_get_id(info), snd_ctl_card_info_get_name(info),
dev,
snd_pcm_info_get_id(pcminfo),
snd_pcm_info_get_name(pcminfo));
count = snd_pcm_info_get_subdevices_count(pcminfo);
printf( _(" Subdevices: %i/%i\n"),
snd_pcm_info_get_subdevices_avail(pcminfo), count);
for (idx = 0; idx < (int)count; idx++) {
snd_pcm_info_set_subdevice(pcminfo, idx);
if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
error("control digital audio playback info (%i): %s", card, snd_strerror(err));
} else {
printf(_(" Subdevice #%i: %s\n"),
idx, snd_pcm_info_get_subdevice_name(pcminfo));
}
}
}
snd_ctl_close(handle);
next_card:
if (snd_card_next(&card) < 0) {
error("snd_card_next");
break;
}
}
}
main (int argc, char *argv[])
{
device_list(SND_PCM_STREAM_CAPTURE);
device_list(SND_PCM_STREAM_PLAYBACK);
}
以下是在我的系统上运行aplay-l
的输出:
**** List of CAPTURE Hardware Devices ****
card 0: [hw:0,0] PCH [HDA Intel PCH], device 0: STAC92xx Analog [STAC92xx Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
**** List of PLAYBACK Hardware Devices ****
card 0: [hw:0,0] PCH [HDA Intel PCH], device 0: STAC92xx Analog [STAC92xx Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: [hw:1,3] NVidia [HDA NVidia], device 3: HDMI 0 [HDMI 0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: [hw:1,7] NVidia [HDA NVidia], device 7: HDMI 1 [HDMI 1]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: [hw:1,8] NVidia [HDA NVidia], device 8: HDMI 2 [HDMI 2]
Subdevices: 1/1
Subdevice #0: subdevice #0
PCM 设备信息
您可以使用aplay -L
从设备获取 PCM 别名信息。这使用了来自设备 API 的“提示”机制。请注意,该程序负责释放由 ALSA 库分配的内存。这意味着,如果返回一个字符串或表,那么不仅要遍历字符串/表,还要保留一个指向字符串/表开头的指针,以便可以释放它。
这个的来源是aplay-L.c
,如下图所示:
/**
* aplay-L.c
*
* Code from aplay.c
* does aplay -L
* http://alsa-utils.sourcearchive.com/documentation/1.0.15/aplay_8c-source.html
*/
/*
* Original notice:
*
* Copyright (c) by Jaroslav Kysela <perex@perex.cz>
* Based on vplay program by Michael Beck
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <locale.h>
#define _(STR) STR
static void pcm_list(snd_pcm_stream_t stream )
{
void **hints, **n;
char *name, *descr, *descr1, *io;
const char *filter;
if (snd_device_name_hint(-1, "pcm", &hints) < 0)
return;
n = hints;
filter = stream == SND_PCM_STREAM_CAPTURE ? "Input" : "Output";
while (*n != NULL) {
name = snd_device_name_get_hint(*n, "NAME");
descr = snd_device_name_get_hint(*n, "DESC");
io = snd_device_name_get_hint(*n, "IOID");
if (io != NULL && strcmp(io, filter) == 0)
goto __end;
printf("%s\n", name);
if ((descr1 = descr) != NULL) {
printf(" ");
while (*descr1) {
if (*descr1 == '\n')
printf("\n ");
else
putchar(*descr1);
descr1++;
}
putchar('\n');
}
__end:
if (name != NULL)
free(name);
if (descr != NULL)
free(descr);
if (io != NULL)
free(io);
n++;
}
snd_device_name_free_hint(hints);
}
main (int argc, char *argv[])
{
printf("*********** CAPTURE ***********\n");
pcm_list(SND_PCM_STREAM_CAPTURE);
printf("\n\n*********** PLAYBACK ***********\n");
pcm_list(SND_PCM_STREAM_PLAYBACK);
}
以下是在我的系统上运行aplay-L
的输出:
*********** CAPTURE ***********
default
Default
sysdefault:CARD=PCH
HDA Intel PCH, STAC92xx Analog
Default Audio Device
front:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
Front speakers
surround40:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
4.0 Surround output to Front and Rear speakers
surround41:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
hdmi:CARD=NVidia,DEV=0
HDA NVidia, HDMI 0
HDMI Audio Output
hdmi:CARD=NVidia,DEV=1
HDA NVidia, HDMI 1
HDMI Audio Output
hdmi:CARD=NVidia,DEV=2
HDA NVidia, HDMI 2
HDMI Audio Output
*********** PLAYBACK ***********
null
Discard all samples (playback) or generate zero samples (capture)
pulse
PulseAudio Sound Server
default
Default
sysdefault:CARD=PCH
HDA Intel PCH, STAC92xx Analog
Default Audio Device
front:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
Front speakers
surround40:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
4.0 Surround output to Front and Rear speakers
surround41:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=PCH,DEV=0
HDA Intel PCH, STAC92xx Analog
7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
请注意,这不包括“插头”设备,如plughw:0
。似乎无法访问插头设备列表。
配置空间信息
除了一般特性之外,每个 PCM 器件都能够支持一系列参数,如通道数量、采样速率等。完整的参数集和范围构成了每个设备的“配置空间”。例如,一个设备可以支持两个到六个通道以及多种不同的采样速率。这两个参数形成一个二维空间。全套形成一个 n 维空间。
ALSA 具有查询该空间并在该空间内设置值的功能。空间由snd_pcm_hw_params_any
初始化。求参数的可能值,有叫snd_pcm_hw_params_get
之类的函数。
不同的参数如下:
通道
- 这是支持的声道数(零表示单声道,以此类推)。
速度
- 这是以赫兹为单位的采样率,即每秒采样数。典型地,CD 音频具有每通道 44,100Hz 的采样率,因此每个通道每秒具有 44,100 个样本。
框架
- 每个帧包含每个通道的一个样本。立体声音频在每帧中将包含两个样本。帧速率与采样速率相同。也就是说,假设立体声音频的采样率是 44,100Hz。那么每个通道每秒将有 44,100 个样本。但是也将是每秒 44,100 帧,因此两个通道的总密度将是每秒 88,200 个样本。
周期时间
- 这是刷新缓冲区的硬件中断之间的时间,以微秒计。
期间大小
-
这是每次硬件中断之间的帧数。这些以如下方式相关联:
Period time = period size x time per frame = period size x time per sample = period size / sampling rate
例如,如果采样速率为 48000Hz 立体声,周期大小为 8,192 帧,则硬件中断之间的时间为 8192 / 48000 秒= 170.5 毫秒。
周期
- 这是每个缓冲区的周期数。
缓冲时间
- 这是一个缓冲的时间。
缓冲区大小
-
这是以帧为单位的缓冲区大小。还是那句话,有关系。
Time of one buffer = buffer size in frames x time for one frame = buffer size x number of channels x time for one sample = buffer size x number of channels / sample rate
缓冲区大小应该是周期大小的倍数,通常是周期大小的两倍。
有关更多示例,请参见 FramesPeriods ( www.alsa-project.org/main/index.php/FramesPeriods
)。
下面是从初始状态求各种参数的取值范围的程序;它叫做device-info.c
:
/**
* Jan Newmarch
*/
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
void info(char *dev_name, snd_pcm_stream_t stream) {
snd_pcm_hw_params_t *hw_params;
int err;
snd_pcm_t *handle;
unsigned int max;
unsigned int min;
unsigned int val;
unsigned int dir;
snd_pcm_uframes_t frames;
if ((err = snd_pcm_open (&handle, dev_name, stream, 0)) < 0) {
fprintf (stderr, "cannot open audio device %s (%s)\n",
dev_name,
snd_strerror (err));
return;
}
if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
fprintf (stderr, "cannot allocate hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_any (handle, hw_params)) < 0) {
fprintf (stderr, "cannot initialize hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_get_channels_max(hw_params, &max)) < 0) {
fprintf (stderr, "cannot (%s)\n",
snd_strerror (err));
exit (1);
}
printf("max channels %d\n", max);
if ((err = snd_pcm_hw_params_get_channels_min(hw_params, &min)) < 0) {
fprintf (stderr, "cannot get channel info (%s)\n",
snd_strerror (err));
exit (1);
}
printf("min channels %d\n", min);
/*
if ((err = snd_pcm_hw_params_get_sbits(hw_params)) < 0) {
fprintf (stderr, "cannot get bits info (%s)\n",
snd_strerror (err));
exit (1);
}
printf("bits %d\n", err);
*/
if ((err = snd_pcm_hw_params_get_rate_min(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get min rate (%s)\n",
snd_strerror (err));
exit (1);
}
printf("min rate %d hz\n", val);
if ((err = snd_pcm_hw_params_get_rate_max(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get max rate (%s)\n",
snd_strerror (err));
exit (1);
}
printf("max rate %d hz\n", val);
if ((err = snd_pcm_hw_params_get_period_time_min(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get min period time (%s)\n",
snd_strerror (err));
exit (1);
}
printf("min period time %d usecs\n", val);
if ((err = snd_pcm_hw_params_get_period_time_max(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get max period time (%s)\n",
snd_strerror (err));
exit (1);
}
printf("max period time %d usecs\n", val);
if ((err = snd_pcm_hw_params_get_period_size_min(hw_params, &frames, &dir)) < 0) {
fprintf (stderr, "cannot get min period size (%s)\n",
snd_strerror (err));
exit (1);
}
printf("min period size in frames %d\n", frames);
if ((err = snd_pcm_hw_params_get_period_size_max(hw_params, &frames, &dir)) < 0) {
fprintf (stderr, "cannot get max period size (%s)\n",
snd_strerror (err));
exit (1);
}
printf("max period size in frames %d\n", frames);
if ((err = snd_pcm_hw_params_get_periods_min(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get min periods (%s)\n",
snd_strerror (err));
exit (1);
}
printf("min periods per buffer %d\n", val);
if ((err = snd_pcm_hw_params_get_periods_max(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get min periods (%s)\n",
snd_strerror (err));
exit (1);
}
printf("max periods per buffer %d\n", val);
if ((err = snd_pcm_hw_params_get_buffer_time_min(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get min buffer time (%s)\n",
snd_strerror (err));
exit (1);
}
printf("min buffer time %d usecs\n", val);
if ((err = snd_pcm_hw_params_get_buffer_time_max(hw_params, &val, &dir)) < 0) {
fprintf (stderr, "cannot get max buffer time (%s)\n",
snd_strerror (err));
exit (1);
}
printf("max buffer time %d usecs\n", val);
if ((err = snd_pcm_hw_params_get_buffer_size_min(hw_params, &frames)) < 0) {
fprintf (stderr, "cannot get min buffer size (%s)\n",
snd_strerror (err));
exit (1);
}
printf("min buffer size in frames %d\n", frames);
if ((err = snd_pcm_hw_params_get_buffer_size_max(hw_params, &frames)) < 0) {
fprintf (stderr, "cannot get max buffer size (%s)\n",
snd_strerror (err));
exit (1);
}
printf("max buffer size in frames %d\n", frames);
}
main (int argc, char *argv[])
{
int i;
int err;
int buf[128];
FILE *fin;
size_t nread;
unsigned int rate = 44100;
if (argc != 2) {
fprintf(stderr, "Usage: %s card\n", argv[0]);
exit(1);
}
printf("*********** CAPTURE ***********\n");
info(argv[1], SND_PCM_STREAM_CAPTURE);
printf("*********** PLAYBACK ***********\n");
info(argv[1], SND_PCM_STREAM_PLAYBACK);
exit (0);
}
以下是我的系统上device-info hw:0
的输出:
*********** CAPTURE ***********
max channels 2
min channels 2
min rate 44100 hz
max rate 192000 hz
min period time 83 usecs
max period time 11888617 usecs
min period size in frames 16
max period size in frames 524288
min periods per buffer 2
max periods per buffer 32
min buffer time 166 usecs
max buffer time 23777234 usecs
min buffer size in frames 32
max buffer size in frames 1048576
*********** PLAYBACK ***********
max channels 2
min channels 2
min rate 44100 hz
max rate 192000 hz
min period time 83 usecs
max period time 11888617 usecs
min period size in frames 16
max period size in frames 524288
min periods per buffer 2
max periods per buffer 32
min buffer time 166 usecs
max buffer time 23777234 usecs
min buffer size in frames 32
max buffer size in frames 1048576
这个程序适用于任何 ALSA 设备,包括“插头”设备。以下来自device-info plughw:0
的输出显示了软件包装器如何给出更大范围的可能值:
*********** CAPTURE ***********
max channels 10000
min channels 1
min rate 4000 hz
max rate -1 hz
min period time 83 usecs
max period time 11888617 usecs
min period size in frames 0
max period size in frames -1
min periods per buffer 0
max periods per buffer -1
min buffer time 1 usecs
max buffer time -1 usecs
min buffer size in frames 1
max buffer size in frames -2
*********** PLAYBACK ***********
max channels 10000
min channels 1
min rate 4000 hz
max rate -1 hz
min period time 83 usecs
max period time 11888617 usecs
min period size in frames 0
max period size in frames -1
min periods per buffer 0
max periods per buffer -1
min buffer time 1 usecs
max buffer time -1 usecs
min buffer size in frames 1
max buffer size in frames -2
也可以用别名设备运行,比如device-info surround40
。
ALSA 初始化
逐行分解在( http://soundprogramming.net/programming_apis/alsa_tutorial_1_initialization
)。它解释了后面程序中的许多公共代码。
将音频捕获到文件中
以下程序摘自保罗·戴维斯的《ALSA 音频 API 使用教程》( http://equalarea.com/paul/alsa-audio.html
):
/**
* alsa_capture.c
*/
/* Copyright © 2002
* Paul Davis
* under the GPL license
*/
/**
* Paul Davis
* http://equalarea.com/paul/alsa-audio.html#howto
*/
/**
* Jan Newmarch
*/
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <signal.h>
#define BUFSIZE 128
#define RATE 44100
FILE *fout = NULL;
/*
* quit on ctrl-c
*/
void sigint(int sig) {
if (fout != NULL) {
fclose(fout);
}
exit(1);
}
main (int argc, char *argv[])
{
int i;
int err;
short buf[BUFSIZE];
snd_pcm_t *capture_handle;
snd_pcm_hw_params_t *hw_params;
snd_pcm_format_t rate = RATE;
int nread;
if (argc != 3) {
fprintf(stderr, "Usage: %s cardname file\n", argv[0]);
exit(1);
}
if ((fout = fopen(argv[2], "w")) == NULL) {
fprintf(stderr, "Can't open %s for writing\n", argv[2]);
exit(1);
}
signal(SIGINT, sigint);
if ((err = snd_pcm_open (&capture_ha
ndle, argv[1], SND_PCM_STREAM_CAPTURE, 0)) < 0) {
fprintf (stderr, "cannot open audio device %s (%s)\n",
argv[1],
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
fprintf (stderr, "cannot allocate hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_any (capture_handle, hw_params)) < 0) {
fprintf (stderr, "cannot initialize hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_access (capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf (stderr, "cannot set access type (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_format (capture_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf (stderr, "cannot set sample format (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_rate_near (capture_handle, hw_params, &rate, 0)) < 0) {
fprintf (stderr, "cannot set sample rate (%s)\n",
snd_strerror (err));
exit (1);
}
fprintf(stderr, "rate set to %d\n", rate);
if ((err = snd_pcm_hw_params_set_channels (capture_handle, hw_params, 2)) < 0) {
fprintf (stderr, "cannot set channel count (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params (capture_handle, hw_params)) < 0) {
fprintf (stderr, "cannot set parameters (%s)\n",
snd_strerror (err));
exit (1);
}
snd_pcm_hw_params_free (h
w_params);
/*
if ((err = snd_pcm_prepare (capture_handle)) < 0) {
fprintf (stderr, "cannot prepare audio interface for use (%s)\n",
snd_strerror (err));
exit (1);
}
*/
while (1) {
if ((nread = snd_pcm_readi (capture_handle, buf, BUFSIZE)) < 0) {
fprintf (stderr, "read from audio interface failed (%s)\n",
snd_strerror (err));
/* recover */
snd_pcm_prepare(capture_handle);
} else {
fwrite(buf, sizeof(short), nread, fout);
}
}
snd_pcm_close (capture_handle);
exit(0);
}
播放文件中的音频
要捕获或播放音频,必须像前面的示例一样先打开设备。然后创建一个配置空间,通过设置各种参数的值来缩小空间。访问类型决定了样本是否交错。格式决定了样本的大小以及它们是小端还是大端。如果无法设置请求的值,所有这些都将返回错误。
一些参数在设置时需要小心。例如,采样速率有一系列可能的值,但并非所有这些值都受支持。可以使用snd_pcm_hw_params_set_rate
请求特定的价格。但是如果请求的速率是不可能的,那么将返回一个错误。有几种方法可以避免这种情况。
- 尝试多种速率,直到找到一种受支持的速率。
- 用
snd_pcm_hw_params_test_rate
测试是否支持某个速率。 - 用
snd_pcm_hw_params_set_rate_near
请求 ALSA 给出最接近的支持率。实际选择的速率在速率参数中设置。 - 不要使用硬件设备,如
hw:0
,使用插头设备,如plughw:0
,它将通过重采样支持更多的值。
最后,一旦为配置空间设置了参数,受限空间就由snd_pcm_hw_params
安装到设备上。
PCM 设备上的调用将导致设备中发生状态变化。打开后,设备处于SND_PCM_STATE_OPEN
状态。设置硬件配置后,设备处于SND_PCM_STATE_PREPARE
状态。应用可以使用snd_pcm_start
调用来读写数据。如果发生超限运行或欠载运行,状态可能下降到SND_PCM_STATE_XRUN
,然后需要调用snd_pcm_prepare
将其恢复到SND_PCM_STATE_PREPARE
。
调用readi
读取交错数据。
以下程序摘自保罗·戴维斯的《ALSA 音频 API 使用教程》( http://equalarea.com/paul/alsa-audio.html
):
/**
* alsa_playback.c
*/
/*
* Copyright © 2002
* Paul Davis
* under the GPL license
*/
/**
* Paul Davis
* http://equalarea.com/paul/alsa-audio.html#howto
*/
/**
* Jan Newmarch
*/
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
main (int argc, char *argv[])
{
int i;
int err;
int buf[128];
snd_pcm_t *playback_handle;
snd_pcm_hw_params_t *hw_params;
FILE *fin;
size_t nread;
unsigned int rate = 44100;
if (argc != 3) {
fprintf(stderr, "Usage: %s card file\n", argv[0]);
exit(1);
}
if ((err = snd_pcm_open (&playback_handle, argv[1], SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
fprintf (stderr, "cannot open audio device %s (%s)\n",
argv[1],
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
fprintf (stderr, "cannot allocate hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_any (playback_handle, hw_params)) < 0) {
fprintf (stderr, "cannot initialize hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_access (playback_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf (stderr, "cannot set access type (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_format (playback_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf (stderr, "cannot set sample format (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_rate_near (playback_handle, hw_params, &rate, 0)) < 0) {
fprintf (stderr, "cannot set sample rate (%s)\n",
snd_strerror (err));
exit (1);
}
printf("Rate set to %d\n", rate);
if ((err = snd_pcm_hw_params_set_channels (playback_handle, hw_params, 2)) < 0) {
fprintf (stderr, "cannot set channel count (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params (playback_handle, hw_params)) < 0) {
fprintf (stderr, "cannot set parameters (%s)\n",
snd_strerror (err));
exit (1);
}
snd_pcm_hw_params_free (hw_params);
/*
if ((err = snd_pcm_prepare (playback_handle)) < 0) {
fprintf (stderr, "cannot prepare audio interface for use (%s)\n",
snd_strerror (err));
exit (1);
}
*/
if ((fin = fopen(argv[2], "r")) == NULL) {
fprintf(stderr, "Can't open %s for reading\n", argv[2]);
exit(1);
}
while ((nread = fread(buf, sizeof(int), 128, fin)) > 0) {
//printf("writing\n");
if ((err = snd_pcm_writei w(playback_handle, buf, nread)) != nread) {
fprintf (stderr, "write to audio interface failed (%s)\n",
snd_strerror (err));
snd_pcm_prepare(playback_handle);
}
}alsa_capture.c
snd_pcm_drain(playback_handle);
snd_pcm_close (playback_handle);
exit (0);
}
使用alsamixer
检查麦克风是否启用。通过执行以下操作进行记录:
alsa_capture hw:0 tmp.s16
通过执行以下操作进行回放:
sox -c 2 -r 44100 tmp.s16 tmp.wav
mplayer tmp.wav
或者使用下一个程序:
alsa_playback hw:0 tmp.s16
使用中断
以前的程序依靠 ALSA 来管理设备。调用snd_pcm_writei
将被阻塞,直到所有帧都被播放或放入回放环形缓冲区。这对于许多用途来说是足够的。如果您想获得更好的控制,那么可以设置一个设备可以处理多少帧的阈值,然后等待达到该阈值。当达到阈值时,ALSA 将导致生成内核中断,此时等待将终止,程序可以继续运行。
说明这一点的程序在“关于使用 ALSA 音频 API 的教程”( http://equalarea.com/paul/alsa-audio.html
)中给出。
管理延迟
在 ALSA 源码中发布的是一个程序/test/latency.c
。这可以使用各种参数来测试系统的延迟。警告:把你的音量调低,否则反馈会烧坏你的扬声器!例如,在低设置下,以下给出的延迟仅为 0.93 毫秒:
latency -m 128 -M 128
以下“差”延迟测试给出的延迟为 92.9 毫秒
latency -m 8192 -M 8192 -t 1 -p
获得低延迟是几件事情的组合。为了获得最佳结果,一个针对延迟进行调整的实时 Linux 内核是一个先决条件。关于这一点,参见“低延迟 how to”(www.alsa-project.org/main/index.php/Low_latency_howto
)。在 ALSA 中,您需要通过编程使用snd_pcm_hw_params_set_buffer_size_near
和snd_pcm_hw_params_set_period_size_near
来设置内部缓冲区和周期大小,正如在latency.c
程序中所做的那样,通过将缓冲区设置为 128 字节来获得低延迟,通过将其设置为 8192 字节来获得更高的延迟。
回放捕获的声音
回放捕获的声音涉及两个句柄,可能用于不同的卡。不幸的是,在一个循环中直接组合这两种方法并不奏效。
while (1) {
int nread;
if ((nread = snd_pcm_readi (capture_handle, buf, BUF_SIZE)) != BUF_SIZE) {
fprintf (stderr, "read from audio interface failed (%s)\n",
snd_strerror (nread));
snd_pcm_prepare(capture_handle);
continue;
}
printf("copying %d\n", nread);
if ((err = snd_pcm_writei (playback_handle, buf, nread)) != nread) {
if (err < 0) {
fprintf (stderr, "write to audio interface failed (%s)\n",
snd_strerror (err));
} else {
fprintf (stderr, "write to audio interface failed after %d frames\n", err);
}
snd_pcm_prepare(playback_handle);
}
}
在我的电脑上,它抛出了各种错误,包括管道破裂、设备未准备好和设备不存在。
要直接回放捕获的声音,必须解决许多问题。第一个问题是每个声卡都有自己的时钟。这些时钟必须同步。这对于消费级卡来说很难维持,因为它们的时钟显然质量很低,会漂移或不稳定。然而,ALSA 将尝试使用函数snd_pcm_link
来同步时钟,该函数将两个卡句柄作为参数。
下一个问题是,必须对缓冲区进行更精细的控制,以及 ALSA 将多久填补这些缓冲区一次。这由两个参数控制:缓冲区大小和周期大小(或缓冲时间和周期时间)。周期大小/时间控制发生中断以填充缓冲器的频率。通常,周期大小(时间)被设置为缓冲区大小(时间)的一半。相关功能有snd_pcm_hw_params_set_buffer_size_near
和snd_pcm_hw_params_set_period_size_near
。相应的get
函数可以用来发现实际设置了什么值。
除了硬件参数,ALSA 还可以设置软件参数。这两者之间的区别对我来说不是很清楚,但是无论如何,一个“开始阈值”和一个“可用最小值”必须被设置为软件参数。我已经设法通过使用snd_pcm_sw_params_set_start_threshold
和snd_pcm_sw_params_set_avail_min
将这两者设置为周期大小来获得工作结果。设置软件参数类似于设置硬件参数:首先用snd_pcm_sw_params_current
初始化一个数据结构,然后用 setter 调用限制软件空间,最后用snd_pcm_sw_params
将数据设置到卡中。
ALSA 需要尽可能保持最高产量。否则,它将生成“写错误”我不知道为什么,但它似乎只有在试图从捕获设备读取和复制之前,将两个缓冲区写入回放设备时才有效。有时一个缓冲器就可以了,但不要超过两个。为了避免在回放开始时出现多余的噪声,两个静音缓冲器效果很好。
生成的程序是playback-capture.c
,如下所示:
/**
* Jan Newmarch
*/
#define PERIOD_SIZE 1024
#define BUF_SIZE (PERIOD_SIZE * 2)
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
void print_pcm_state(snd_pcm_t *handle, char *name) {
switch (snd_pcm_state(handle)) {
case SND_PCM_STATE_OPEN:
printf("state open %s\n", name);
break;
case SND_PCM_STATE_SETUP:
printf("state setup %s\n", name);
break;
case SND_PCM_STATE_PREPARED:
printf("state prepare %s\n", name);
break;
case SND_PCM_STATE_RUNNING:
printf("state running %s\n", name);
break;
case SND_PCM_STATE_XRUN:
printf("state xrun %s\n", name);
break;
default:
printf("state other %s\n", name);
break;
}
}
int setparams(snd_pcm_t *handle, char *name) {
snd_pcm_hw_params_t *hw_params;
int err;
if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
fprintf (stderr, "cannot allocate hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_an
y (handle, hw_params)) < 0) {
fprintf (stderr, "cannot initialize hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_access (handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf (stderr, "cannot set access type (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_format (handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf (stderr, "cannot set sample format (%s)\n",
snd_strerror (err));
exit (1);
}
unsigned int rate = 48000;
if ((err = snd_pcm_hw_params_set_rate_near (handle, hw_params, &rate, 0)) < 0) {
fprintf (stderr, "cannot set sample rate (%s)\n",
snd_strerror (err));
exit (1);
}
printf("Rate for %s is %d\n", name, rate);
if ((err = snd_pcm_hw_params_set_channels (handle, hw_params, 2)) < 0) {
fprintf (stderr, "cannot set ch
annel count (%s)\n",
snd_strerror (err));
exit (1);
}
snd_pcm_uframes_t buffersize = BUF_SIZE;
if ((err = snd_pcm_hw_params_set_buffer_size_near(handle, hw_params, &buffersize)) < 0) {
printf("Unable to set buffer size %li: %s\n", BUF_SIZE, snd_strerror(err));
exit (1);;
}
snd_pcm_uframes_t periodsize = PERIOD_SIZE;
fprintf(stderr, "period size now %d\n", periodsize);
if ((err = snd_pcm_hw_params_set_period_size_near(handle, hw_params, &periodsize, 0)) < 0) {
printf("Unable to set period size %li: %s\n", periodsize, snd_strerror(err));
exit (1);
}
if ((err = snd_pcm_hw_params (handle, hw_params)) < 0) {
fprintf (stderr, "cannot set parameters (%s)\n",
snd_strerror (err));
exit (1);
}
snd_pcm_uframes_t p_psize;
snd_pcm_hw_params_get_period_size(hw_params, &p_psize, NULL);
fprintf(stderr, "period size %d\n", p_psize);
snd_pcm_hw_params_get_buffer_size(hw_params, &p_psize);
fprintf(stderr, "buffer size %d\n", p_psize);
snd_pcm_hw_params_free (hw_params);
if ((err = snd_pcm_prepare (handle)) < 0) {
fprintf (stderr, "cannot prepare audio interface for use (%s)\n",
snd_strerror (err));
exit (1);
}
return 0;
}
int set_sw_params(snd_pcm_t *handle, char *name) {
snd_pcm_sw_params_t *swparams;
int err;
snd_pcm_sw_params_alloca(&swparams);
err = snd_pcm_sw_params_current(handle, swparams);
if (err < 0) {
fprintf(stderr, "Broken configuration for this PCM: no configurations available\n");
exit(1);
}
err = snd_pcm_sw_params_set_start_threshold(handle, swparams, PERIOD_SIZE);
if (err < 0) {
printf("Unable to set start threshold: %s\n", snd_strerror(err));
return err;
}
err = snd_pcm_sw_params_set_avail_min(handle, swparams, PERIOD_SIZE);
if (err < 0) {
printf("Unable to set avail min: %s\n", snd_strerror(err));
return err;
}
if (snd_pcm_sw_params(handle, swparams) < 0) {
fprintf(stderr, "unable to install sw params:\n");
exit(1);
}
return 0;
}
/************** some code from latency.c *****************/
main (int argc, char *argv[])
{
int i;
int err;
int buf[BUF_SIZE];
snd_pcm_t *playback_handle;
snd_pcm_t *capture_handle;
snd_pcm_hw_params_t *hw_params;
FILE *fin;
size_t nread;
snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;
if (argc != 3) {
fprintf(stderr, "Usage: %s in-card out-card\n", argv[0]);
exit(1);
}
/**** Out card *******/
if ((err = snd_pcm_open (&playback_handle, argv[2], SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
fprintf (stderr, "cannot open audio device %s (%s)\n",
argv[2],
snd_strerror (err));
exit (1);
}
setparams(playback_handle, "playback");
set_sw_params(playback_handle, "playback");
/*********** In card **********/
if ((err = snd_pcm_open (&capture_handle, argv[1], SND_PCM_STREAM_CAPTURE, 0)) < 0) {
fprintf (stderr, "cannot open audio device %s (%s)\n",
argv[1],
snd_strerror (err));
exit (1);
}
setparams(capture_handle, "capture");
set_sw_params(capture_handle, "capture");
if ((err = snd_pcm_link(capture_handle, playback_handle)) < 0) {
printf("Streams link error: %s\n", snd_strerror(err));
exit(0);
}
if ((err = snd_pcm_prepare (playback_handle)) < 0) {
fprintf (stderr, "cannot prepare playback audio interface for use (%s)\n",
snd_strerror (err));
exit (1);
}
/**************** stuff something into the playback buffer ****************/
if (snd_pcm_format_set_silence(format, buf, 2*BUF_SIZE) < 0) {
fprintf(stderr, "silence error\n");
exit(1);
}
int n = 0;
while (n++ < 2) {
if (snd_pcm_writei (playback_handle, buf, BUF_SIZE) < 0) {
fprintf(stderr, "write error\n");
exit(1);
}
}
/************* COPY ************/
while (1) {
int nread;
if ((nread = snd_pcm_readi (capture_handle, buf, BUF_SIZE)) != BUF_SIZE) {
if (nread < 0) {
fprintf (stderr, "read from audio interface failed (%s)\n",
snd_strerror (nread));
} else {
fprintf (stderr, "read from audio interface failed after %d frames\n", nread);
}
snd_pcm_prepare(capture_handle);
continue;
}
if ((err = snd_pcm_writei (playback_handle, buf, nread)) != nread) {
if (err < 0) {
fprintf (stderr, "write to audio interface failed (%s)\n",
snd_strerror (err));
} else {
fprintf (stderr, "write to audio interface failed after %d frames\n", err);
}
snd_pcm_prepare(playback_handle);
}
}
snd_pcm_drain(playback_handle);
snd_pcm_close (playback_handle);
exit (0);
}
混合音频
如果一个以上的应用想写声卡,只有一个被允许这样做,或者信号必须混合在一起。有些声卡允许硬件混合,但有些不允许。在这种情况下,混合必须在软件中完成,而 ALSA 有这样的机制。
使用 dmix 混合
ALSA 包含一个名为dmix
的插件,默认情况下是启用的。这在软件中将多个音频输入信号混合成一个输出信号。“Dmix how to”(http://alsa.opensrc.org/Dmix
)中给出了对此的描述。基本上,每个想要向 ALSA 写入音频的应用都应该使用插件plug:dmix
,而不是像hw:0
这样的硬件设备。例如,前面讨论的alsa_playback
程序可以被多次调用,并且将 ALSA 输入混合在一起,如下所示:
alsa_playback plug:dmix tmp1.s16 &
alsa_playback plug:dmix tmp2.s16 &
alsa_playback plug:dmix tmp3.s16
使用脉冲音频混合
PulseAudio 直到下一章才会涉及,因为它通常被认为是一个声音服务器,在 ALSA 之上的层中工作。但是,还有一个 ALSA 插件模块,PulseAudio 可以作为插件设备出现在 ALSA 下面!因此,ALSA 可以将输出写入 PulseAudio 插件,该插件可以使用 PulseAudio 的全部功能对其进行处理,然后将其反馈回 ALSA,以便在硬件设备上呈现。
其中一个功能是 PulseAudio 包含一个混音器。因此,两个(或更多)应用可以向 PulseAudio 插件发送音频,然后该插件将混合信号并将其发送回 ALSA。
PulseAudio 插件可以显示为 PCM 设备pulse
或default
。因此,以下三个输出将由 PulseAudio 混合,并由 ALSA 渲染:
alsa_playback default tmp1.s16 &
alsa_playback pulse tmp2.s16 &
alsa_playback default tmp3.s16
简单混音器 API:音量控制
ALSA 有一个单独的混音器模块的 API。其实有两个:异步混音器接口( www.alsa-project.org/alsa-doc/alsa-lib/group___mixer.html
)和简单混音器接口( www.alsa-project.org/alsa-doc/alsa-lib/group___simple_mixer.html
)。我将只讨论简单的接口。
除了混音之外,ALSA 混音器没有太多的功能。基本上,它可以获取和设置频道或全局的音量。基于 http://stackoverflow.com/questions/6787318/set-alsa-master-volume-from-c-code
的功能,通过以下程序说明音量设置:
#include <alsa/asoundlib.h>
#include <alsa/mixer.h>
#include <stdlib.h>
int main(int argc, char **argv) {
snd_mixer_t *mixer;
snd_mixer_selem_id_t *ident;
snd_mixer_elem_t *elem;
long min, max;
long old_volume, volume;
snd_mixer_open(&mixer, 0);
snd_mixer_attach(mixer, "default");
snd_mixer_selem_register(mixer, NULL, NULL);
snd_mixer_load(mixer);
snd_mixer_selem_id_alloca(&ident);
snd_mixer_selem_id_set_index(ident, 0);
snd_mixer_selem_id_set_name(ident, "Master");
elem = snd_mixer_find_selem(mixer, ident);
snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
snd_mixer_selem_get_playback_volume(elem, 0, &old_volume);
printf("Min %ld max %ld current volume %ld\n", min, max, old_volume);
if (argc < 2) {
fprintf(stderr, "Usage: %s volume (%ld - %ld)\n", argv[0], min, max);
exit(1);
}
volume = atol(argv[1]);
snd_mixer_selem_set_playback_volume_all(elem, volume);
printf("Volume reset to %ld\n", volume);
exit(0);
}
编写 ALSA 设备驱动程序
如果你需要为一个新的声卡编写一个设备驱动,请参阅岩井隆的“编写一个 ALSA 驱动”( www.alsa-project.org/~tiwai/writing-an-alsa-driver.pdf
)。
结论
ALSA 是目前 Linux 内核中包含的最低级别的音频栈。它为设备驱动程序提供了一个标准的 API 来访问不同的声音设备和声卡。有多种用户级工具可以访问和操作这些设备,这些工具都是使用这个 API 构建的。
本章介绍了用户级工具以及使用 API 构建自己的工具。有一个指针指向构建设备驱动程序。