Linux-声音编程教程-全-

Linux 声音编程教程(全)

原文:Linux Sound Programming

协议:CC BY-NC-SA 4.0

一、声音的基本概念

本章着眼于音频的一些基本概念,包括模拟音频和数字音频。以下是一些资源:

样本音频

音频是一种模拟现象。声音以各种方式产生,通过声音、乐器和自然事件,如森林中的树木倒下(无论是否有人听到)。在某一点接收到的声音可以被绘制成振幅对时间的曲线图,并且可以呈现几乎任何功能状态,包括不连续的。

对声音的分析通常是通过观察它的频谱来完成的。从数学上来说,这是通过傅里叶变换实现的,但是耳朵仅通过耳朵的结构来执行几乎类似的变换。耳朵听到的“纯”声音对应于简单的正弦波,谐波对应于正弦波,其频率是基本正弦波的倍数。

系统中的模拟信号,例如模拟音频放大器,被设计成与这些频谱信号一起工作。他们试图在整个音频范围内产生相同的放大效果。

计算机和越来越多的电子设备处理由 1 和 0 组成的数字信号。位被组合成具有 256 个可能值的字节、具有 65,536 个可能值的 16 位字,甚至更大的组合,例如 32 位或 64 位字。

抽样率

将模拟信号数字化意味着以固定的时间间隔从该信号中取样,并以离散的尺度表示这些样本。采样的频率就是采样率。例如,CD 上的音频采样频率为 44,100Hz,即每秒 44,100 次。在 DVD 上,每秒可以采样 192,000 次,采样率为 192kHz。相反,标准电话采样速率为 8kHz。

图 1-1 表示取样。

A435426_1_En_1_Fig1_HTML.gif

图 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 毫秒。

A435426_1_En_1_Fig2_HTML.jpg

图 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 。

A435426_1_En_2_Fig1_HTML.jpg

图 2-1。

GNOME MPlayer

可见光通讯

VLC 是我第二喜欢的运动员。它也可以播放几乎任何东西,并接受 HTTP,DVD 和 VCD 的网址。它有一个默认的 GUI,但是也可以在没有 GUI 的情况下用命令cvlc运行。GUI 看起来如图 2-2 所示。

A435426_1_En_2_Fig2_HTML.jpg

图 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 声音系统的组件的高级概述,以及哪些位最适合用于哪些用例。

资源

以下是一些资源:

成分

图 4-1 表示 Linux 声音系统的不同层次。

A435426_1_En_4_Fig1_HTML.gif

图 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 所示的图表。

A435426_1_En_4_Fig2_HTML.gif

图 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 的那个)。

A435426_1_En_4_Fig3_HTML.gif

图 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 既是一组与声卡对话的 API,也是一组用户级应用,当然是使用 ALSA API 构建的。它包括查询和控制声卡以及从声卡上录音和播放的命令。本节考虑命令行工具。

alsamixer

在终端窗口中运行,允许你选择声卡和控制这些卡上的接口。看起来像图 5-1 。

A435426_1_En_5_Fig1_HTML.jpg

图 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选项运行aplayarecord,如下所示:

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:0hw: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 3hdmi:0的简单编程方式。

您可以使用aplayarecord显示别名集。

我的系统上来自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:0hw: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_idsnd_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_nearsnd_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_nearsnd_pcm_hw_params_set_period_size_near。相应的get函数可以用来发现实际设置了什么值。

除了硬件参数,ALSA 还可以设置软件参数。这两者之间的区别对我来说不是很清楚,但是无论如何,一个“开始阈值”和一个“可用最小值”必须被设置为软件参数。我已经设法通过使用snd_pcm_sw_params_set_start_thresholdsnd_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 设备pulsedefault。因此,以下三个输出将由 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 构建自己的工具。有一个指针指向构建设备驱动程序。

六、PulseAudio

PulseAudio 是一个声音服务器,位于 ALSA 或 OSS 等设备驱动之上。它提供了比设备驱动程序更多的功能。PulseAudio 专为消费类音频而设计,可轻松在台式机、笔记本电脑和移动设备上使用声音。多个声音源都可以播放给 PulseAudio 服务器,服务器会将它们混合在一起播放。低延迟不是设计目标,因此不适合专业音频。

资源

以下是一些资源:

开始、停止和暂停脉冲音频

如果你有一个当前的 Linux 系统,PulseAudio 可能正在运行。通过从命令行运行以下命令对此进行测试:

ps agx | grep pulse

如果您看到类似/usr/bin/pulseaudio --start --log-target=syslog的行,那么它已经在运行了。

如果它没有运行,而您已经安装了它,那么使用以下命令启动它:

pulseaudio --start

停止 PulseAudio 并不容易。卡拉·施罗德展示了如何在 www.linuxplanet.com/linuxplanet/tutorials/7130/2 。最基本的问题是,PulseAudio 被设置为在被杀死后重生。你必须通过编辑/etc/pulse/client.conf,将autospawn = yes改为autospawn = no,并将daemon-binary设置为/bin/true来关闭它。然后,您可以终止该进程,将其从启动文件中删除,等等。

如果您想暂时运行另一个声音系统(如 Jack ),您可能只想暂停 PulseAudio。你可以通过使用pasuspender来实现。这需要一个命令(在--之后),并将暂停 PulseAudio 服务器对音频设备的访问,直到子命令完成。例如,下面的代码将运行 Jack 服务器,PulseAudio 将退出,直到它完成:

pasuspender -- jackd

用户空间工具

除了编程 API 之外,PulseAudio 还有一系列用户级工具。这些工具提供有关 PulseAudio 系统的信息,并允许各种控制。Gnome 项目也有一个 PulseAudio 感知的控制中心。本节考虑这些工具。

帕曼

这将显示有关 PulseAudio 服务器、其设备和客户端的信息。图 6-1 到 6-3 显示了它给出的信息类型。

A435426_1_En_6_Fig3_HTML.jpg

图 6-3。

Pulse Audio Manager client information

A435426_1_En_6_Fig2_HTML.jpg

图 6-2。

Pulse Audio Manager device information

A435426_1_En_6_Fig1_HTML.jpg

图 6-1。

Pulse Audio Manager server information

铺路机

pavumeter是一个简单的仪表,用于显示默认设备的输入或输出电平。当pavumeter运行时,显示播放设备,如图 6-4 所示。

A435426_1_En_6_Fig4_HTML.jpg

图 6-4。

Pulse Audio playback volume meter

如果由pavumeter --record运行,则显示记录设备,如图 6-5 所示。

A435426_1_En_6_Fig5_HTML.jpg

图 6-5。

Pulse Audio record volume meter

pavucontrol

pavucontrol允许您控制不同连接的音频设备的输入和输出音量,如图 6-6 所示。

A435426_1_En_6_Fig6_HTML.jpg

图 6-6。

Pulse Audio pavucontrol output devices

通过这些选项卡,pavucontrol是一个设备级混音器,能够控制各个设备的整体音量。

PulseAudio 的一个特殊优势是可以进行应用级混音。如果两个音频源写入同一个 PulseAudio 设备,音频将被混合到输出设备。pavucontrol可以使用 Playback 选项卡显示多个应用,显示当前正在混合的所有应用或所有流。每个流都可以单独控制其通道音量。

例如,廉价的 Karaoke 可以通过以下方式将麦克风的直通模块设置为扬声器:

pactl load-module module-loopback latency_msec=1

Karaoke 文件由诸如kmidtimidity的 Karaoke 播放器播放。这里有一个例子:

kmid nightsin.kar

当这两个运行时,可以使用pavucontrol控制相对音量,如图 6-7 。

A435426_1_En_6_Fig7_HTML.jpg

图 6-7。

Pulse Audio pavucontrol multiple devices

Gnome 控制中心(声音)

命令gnome-control-center sound允许完全查看和控制连接的声音设备,包括选择默认输入和输出设备。看起来像图 6-8 。

A435426_1_En_6_Fig8_HTML.jpg

图 6-8。

Gnome control center

parec/paplay/pacat

parecpaplaypacatare是录制和回放声音文件的命令行工具。它们都是指向相同代码的符号链接,只是链接的名称不同。默认格式是 PCM s16。有许多选择,但它们并不总是如你所愿。例如,要以最小的延迟从默认录制设备播放到默认播放设备,请使用以下命令:

pacat -r --latency-msec=1 | pacat -p --latency-msec=1

这实际上有大约 50 毫秒的延迟。

pactl/pacmd

这两个命令基本上做同样的事情。pacmd是选项更多的互动版。例如,带有命令list-sinkspacmd包括以下内容:

        name: <alsa_output.pci-0000_00_1b.0.analog-stereo>
        driver: <module-alsa-card.c>
        flags: HARDWARE HW_MUTE_CTRL HW_VOLUME_CTRL DECIBEL_VOLUME LATENCY FLAT_VOLUME DYNAMIC_LATENCY
        state: SUSPENDED
        suspend cause: IDLE
        priority: 9959
        volume: 0:  93% 1:  93%
                0: -1.88 dB 1: -1.88 dB
                balance 0.00
        base volume: 100%
                     0.00 dB
        volume steps: 65537
        muted: no
        current latency: 0.00 ms
        max request: 0 KiB
        max rewind: 0 KiB
        monitor source: 1
        sample spec: s16le 2ch 44100Hz
        channel map: front-left,front-right
                     Stereo
        used by: 0
        linked by: 0
        configured latency: 0.00 ms; range is 16.00 .. 2000.00 ms
        card: 1 <alsa_card.pci-0000_00_1b.0>
        module: 5
        properties:

                alsa.resolution_bits = "16"
                device.api = "alsa"
                device.class = "sound"
                alsa.class = "generic"
                alsa.subclass = "generic-mix"
                alsa.name = "STAC92xx Analog"
                alsa.id = "STAC92xx Analog"
                alsa.subdevice = "0"
                alsa.subdevice_name = "subdevice #0"
                alsa.device = "0"
                alsa.card = "0"
                alsa.card_name = "HDA Intel PCH"
                alsa.long_card_name = "HDA Intel PCH at 0xe6e60000 irq 47"
                alsa.driver_name = "snd_hda_intel"
                device.bus_path = "pci-0000:00:1b.0"
                sysfs.path = "/devices/pci0000:00/0000:00:1b.0/sound/card0"
                device.bus = "pci"
                device.vendor.id = "8086"
                device.vendor.name = "Intel Corporation"
                device.product.id = "1c20"
                device.product.name = "6 Series/C200 Series Chipset Family High Definition Audio Controller"
                device.form_factor = "internal"
                device.string = "front:0"
                device.buffering.buffer_size = "352800"
                device.buffering.fragment_size = "176400"
                device.access_mode = "mmap+timer"
                device.profile.name = "analog-stereo"
                device.profile.description = "Analog Stereo"
                device.description = "Internal Audio Analog Stereo"
                alsa.mixer_name = "IDT 92HD90BXX"
                alsa.components = "HDA:111d76e7,10280494,00100102"
                module-udev-detect.discovered = "1"
                device.icon_name = "audio-card-pci"
        ports:
                analog-output: Analog Output (priority 9900)
                analog-output-headphones: Analog Headphones (priority 9000)
        active port: <analog-output>

设备名称

PulseAudio 使用自己的命名约定。使用 PulseAudio FAQ ( www.freedesktop.org/wiki/Software/PulseAudio/FAQ#How_do_I_record_stuff.3F )中的代码可以找到源设备(如麦克风)的名称。

pactl list | grep -A2 'Source #' | grep 'Name: .*\.monitor$' | cut -d" " -f2

在我的系统上,这会产生以下结果:

alsa_output.pci-0000_01_00.1.hdmi-stereo.monitor
alsa_output.pci-0000_00_1b.0.analog-stereo.monitor
alsa_input.pci-0000_00_1b.0.analog-stereo

类似地,输出设备如下:

pactl list | grep -A2 'Sink #' | grep 'Name: .*\.monitor$' | cut -d" " -f2

这给出了以下内容:

alsa_output.pci-0000_01_00.1.hdmi-stereo
alsa_output.pci-0000_00_1b.0.analog-stereo

环回模块

使用pactl,您可以加载模块module-loopback,如下所示:

pactl load-module module-loopback latency_msec=1

当加载模块时,声音在内部从输入设备传送到输出设备。延迟实际上为零。

如果你把这个模块装入,比如说,你的笔记本电脑,拔下扬声器,麦克风等等的时候要小心。内部扬声器和麦克风足够近,可以建立一个反馈回路。用以下命令卸载 N 号模块:

pactl unload-module N

(如果您忘记了模块号,只需运行pactl就会列出所有模块,这样您就可以识别环回模块。)

脉动音频和 ALSA

pacmd的输出显示 PulseAudio 使用 ALSA。关系更深:默认的 ALSA 设备是hw:0,但是 PulseAudio 覆盖了它。在/etc/asound.conf中有一个用来装载/etc/alsa/pulse-default.conf的钩子,它包含以下内容:

pcm.!default {
    type pulse
    hint {
        description "Default"
    }
}

这将使用 PulseAudio 模块替换默认设备。

打开默认的 ALSA 设备实际上会调用 PulseAudio,然后 pulse audio 会用它选择的设备回调 ALSA。

使用 PulseAudio 编程

PulseAudio 及其文档的来源是 PulseAudio 2.0 ( http://freedesktop.org/software/pulseaudio/doxygen/index.html )。使用 PulseAudio 编程有两种方式:简单 API 和异步 API。这两者都将在下面的章节中介绍。

简单 API

PulseAudio 有一个“简单”的 API 和一个复杂得多的异步 API。简单的 API 可能足以满足您的需求。

简单 API 有一小组函数,如下所示:

pa_simple * pa_simple_new (const char *server, const char *name, pa_stream_direction_t dir, const char *dev, const char *stream_name, const pa_sample_spec *ss, const pa_channel_map *map, const pa_buffer_attr *attr, int *error)
        Create a new connection to the server.
void    pa_simple_free (pa_simple *s)
        Close and free the connection to the server.
int     pa_simple_write (pa_simple *s, const void *data, size_t bytes, int *error)
        Write some data to the server.
int     pa_simple_drain (pa_simple *s, int *error)
        Wait until all data already written is played by the daemon.
int     pa_simple_read (pa_simple *s, void *data, size_t bytes, int *error)
        Read some data from the server.
pa_usec_t pa_simple_get_latency (pa_simple *s, int *error)
        Return the playback latency.
int     pa_simple_flush (pa_simple *s, int *error)
        Flush the playback buffer

.

播放文件

“从文件播放到默认输出设备”旁边显示的程序来自 PulseAudio 网站。基本结构如下:

  1. 创建新的播放流(pa_simple_new)。
  2. 从文件中读取块(read )…
  3. …把它们写到流中(pa_simple_write)。
  4. 通过冲洗水流结束(pa_simple_drain)。

节目是pacat-simple.c。更奇怪的是,它做了一个dup2来将打开的文件描述符映射到stdin上,然后从stdin中读取。没必要这样。为什么不直接从原始文件描述符中读取呢?

/***
 *   This file is part of PulseAudio.
 *
 *   PulseAudio is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Lesser General Public License as published
 *   by the Free Software Foundation; either version 2.1 of the License,
 *   or (at your option) any later version.
 *
 *   PulseAudio 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 Lesser General Public License
 *   along with PulseAudio; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 *   USA.
 ****/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>

#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

#include <pulse/simple.h>
#include <pulse/error.h>

#define BUFSIZE 1024

int main(int argc, char*argv[]) {

    // set to NULL for default output device
    char *device = "alsa_output.pci-0000_00_1b.0.analog-stereo";

    /* The Sample format to use */
    static const pa_sample_spec ss = {
        .format = PA_SAMPLE_S16LE,
        .rate = 44100,
        .channels = 2
    };

    pa_simple *s = NULL;
    int ret = 1;
    int error;

    /* replace STDIN with the specified file if needed */
    if (argc > 1) {
        int fd;

        if ((fd = open(argv[1], O_RDONLY)) < 0) {
            fprintf(stderr, __FILE__": open() failed: %s\n", strerror(errno));
            goto finish;
        }

        if (dup2(fd, STDIN_FILENO) < 0) {
            fprintf(stderr, __FILE__": dup2() failed: %s\n", strerror(errno));
            goto finish;
        }

        close(fd);
    }

    /* Create a new playback stream */
    if (!(s = pa_simple_new(NULL, argv[0], PA_STREAM_PLAYBACK, device, "playback", &ss, NULL, NULL, &error))) {
        fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
        goto finish;

    }

    for (;;) {
        uint8_t buf[BUFSIZE];
        ssize_t r;

#if 1
        pa_usec_t latency;

        if ((latency = pa_simple_get_latency(s, &error)) == (pa_usec_t) -1) {
            fprintf(stderr, __FILE__": pa_simple_get_latency() failed: %s\n", pa_strerror(error));
            goto finish;
        }

        fprintf(stderr, "%0.0f usec    \r", (float)latency);
#endif

        /* Read some data ... */
        if ((r = read(STDIN_FILENO, buf, sizeof(buf))) <= 0) {
            if (r == 0) /* EOF */
                break;

            fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno));
            goto finish;
        }

        /* ... and play it */
        if (pa_simple_write(s, buf, (size_t) r, &error) < 0) {
            fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
            goto finish;
        }
    }

    /* Make sure that every single sample was played */
    if (pa_simple_drain(s, &error) < 0) {
        fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));
        goto finish;

    }

    ret = 0;

finish:

    if (s)
        pa_simple_free(s);

    return ret;
}

记录到文件中

“从默认输入设备记录到文件”旁边显示的程序来自 PulseAudio 网站。叫parec-simple.c。基本结构如下:

  1. 创建新的录制流(pa_simple_new)。
  2. 从流中读取块(pa_simple_read )…
  3. …将它们写入输出(write)。
  4. 通过释放流来结束(pa_simple_free)。

注意,您需要使用一个pa_sample_spec来告诉 PulseAudio 写入数据的格式。这里我选择了双通道、44100Hz 和 PCM 16 位小端格式。

/***
  This file is part of PulseAudio.

  PulseAudio is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation; either version 2.1 of the License,
  or (at your option) any later version.

  PulseAudio 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 Lesser General Public License
  along with PulseAudio; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  USA.
***/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#include <pulse/simple.h>
#include <pulse/error.h>

#define BUFSIZE 1024

/* A simple routine calling UNIX write() in a loop */
static ssize_t loop_write(int fd, const void*data, size_t size) {
    ssize_t ret = 0;

    while (size > 0) {
        ssize_t r;

        if ((r = write(fd, data, size)) < 0)
            return r;

        if (r == 0)
            break;

        ret += r;
        data = (const uint8_t*) data + r;
        size -= (size_t) r;
    }

    return ret;

}

int main(int argc, char*argv[]) {
    /* The sample type to use */
    static const pa_sample_spec ss = {
        .format = PA_SAMPLE_S16LE,
        .rate = 44100,
        .channels = 2
    };
    pa_simple *s = NULL;
    int ret = 1;
    int error;

    /* Create the recording stream */
    if (!(s = pa_simple_new(NULL, argv[0], PA_STREAM_RECORD, NULL, "record", &ss, NULL, NULL, &error))) {
        fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
        goto finish;
    }

    for (;;) {
        uint8_t buf[BUFSIZE];

        /* Record some data ... */
        if (pa_simple_read(s, buf, sizeof(buf), &error) < 0) {
            fprintf(stderr, __FILE__": pa_simple_read() failed: %s\n", pa_strerror(error));
            goto finish;
        }

        /* And write it to STDOUT */
        if (loop_write(STDOUT_FILENO, buf, sizeof(buf)) != sizeof(buf)) {
            fprintf(stderr, __FILE__": write() failed: %s\n", strerror(errno));
            goto finish;

        }
    }

    ret = 0;

finish:

    if (s)
        pa_simple_free(s);

    return ret;
}

此操作的输出是一个 PCM s16 文件。您可以使用sox(例如sox -c 2 -r 44100 tmp.s16 tmp.wav)将其转换为另一种格式,也可以将其作为原始数据导入 Audacity 并直接播放。

这些对于实时音频有多好?第一个程序可以显示等待时间(将#if 0改为#if 1)。这段代码也可以复制到第二段。结果并不好。

  • 在我的笔记本电脑上录制有 11 毫秒的延迟。
  • 回放有 130 毫秒的延迟。

从源到汇播放

您可以将这两个程序结合起来,使用录音和播放流从麦克风复制到扬声器。程序是pa-mic-2-speaker-simple.c,如下图所示:

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

#include <pulse/simple.h>
#include <pulse/error.h>

#define BUFSIZE 32

int main(int argc, char*argv[]) {

    /* The Sample format to use */
    static const pa_sample_spec ss = {
        .format = PA_SAMPLE_S16LE,
        .rate = 44100,
        .channels = 2
    };

    pa_simple *s_in, *s_out = NULL;
    int ret = 1;
    int error;

    /* Create a new playback stream */
    if (!(s_out = pa_simple_new(NULL, argv[0], PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) {
        fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
        goto finish;
    }

      if (!(s_in = pa_simple_new(NULL, argv[0], PA_STREAM_RECORD, NULL, "record", &ss, NULL, NULL, &error))) {
        fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
        goto finish;
    }

    for (;;) {
        uint8_t buf[BUFSIZE];

        ssize_t r;

#if 1
        pa_usec_t latency;

        if ((latency = pa_simple_get_latency(s_in, &error)) == (pa_usec_t) -1) {
            fprintf(stderr, __FILE__": pa_simple_get_latency() failed: %s\n", pa_strerror(error));
            goto finish;
        }

        fprintf(stderr, "In:  %0.0f usec    \r\n", (float)latency);

        if ((latency = pa_simple_get_latency(s_out, &error)) == (pa_usec_t) -1) {
            fprintf(stderr, __FILE__": pa_simple_get_latency() failed: %s\n", pa_strerror(error));
            goto finish;
        }

        fprintf(stderr, "Out: %0.0f usec    \r\n", (float)latency);
#endif

        if (pa_simple_read(s_in, buf, sizeof(buf), &error) < 0) {

            fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno));
            goto finish;
        }

        /* ... and play it */
        if (pa_simple_write(s_out, buf, sizeof(buf), &error) < 0) {
            fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
            goto finish;
        }
    }

    /* Make sure that every single sample was played */
    if (pa_simple_drain(s_out, &error) < 0) {
        fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));
        goto finish;
    }

    ret = 0;

finish:

    if (s_in)
        pa_simple_free(s_in);
    if (s_out)
        pa_simple_free(s_out);

    return ret;
}

试着运行它,你会发现延迟是明显的,并且不令人满意。

异步 API

简单的 API 很简单。相比之下,异步 API 又大又复杂。使用这个 API 的例子也很少。

几乎所有与该 API 的交互都是异步的。对 PulseAudio 服务器发出一个调用,当响应就绪时,一个库调用一个回调函数,您将在发出库调用时将这个函数传递给它。这避免了用户代码阻塞或进行轮询调用的需要。

基本结构如下:

  1. 创建一个 PulseAudio 主循环(同步:pa_mainloop_new)。
  2. 获取mainloop API 对象,这是一个mainloop函数的表(同步:pa_mainloop_get_api)。
  3. 获取一个上下文对象与 PulseAudio 服务器对话(同步:pa_context_new)。
  4. 建立与 PulseAudio 服务器的连接。这就是异步:pa_context_connect
  5. 从服务器注册上下文状态更改的回调:pa_context_set_state_callback
  6. 开始事件处理循环(pa_mainloop_run)。
  7. 在上下文状态回调中,确定什么状态已经更改。例如,连接已经建立。
  8. 在这个回调中,设置、记录或回放流。
  9. 为这些流建立进一步的回调。
  10. 在流回调中,进行更多的处理,比如将记录流保存到文件中。

步骤 1-7 对于大多数应用来说都是常见的。将调用上下文状态回调来响应服务器中的更改。这些是状态变化,例如PA_CONTEXT_CONNECTINPA_CONTEXT_SETTING_NAME等等。与大多数应用相关的变化将是PA_CONTEXT_READY。这意味着应用可以在稳定状态下向服务器发出请求。

在第 8 步中,应用将设置自己的行为。这是通过为各种操作设置进一步的回调函数来实现的,比如列出设备或播放音频。

设备列表

函数pa_context_get_sink_info_list将设置一个回调函数,列出源设备,如下所示:

pa_context_get_sink_info_list(c, sinklist_cb, NULL)

其中c是上下文,sinklist_cb是应用的回调,NULL是传递给回调的用户数据。

回调的调用方式如下:

void sinklist_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata)

参数eol可以取三个值:负表示某种失败,零表示pa_sink_info的有效条目,正表示列表中不再有有效条目。

结构pa_sink_info定义如下:

struct {
  const char *  name;
  uint32_t      index;
  const char *  description;
  pa_sample_spec        sample_spec;
  pa_channel_map        channel_map;
  uint32_t      owner_module;
  pa_cvolume    volume;
  int   mute;
  uint32_t      monitor_source;
  const char *  monitor_source_name;
  pa_usec_t     latency;
  const char *  driver;
  pa_sink_flags_t       flags;
  pa_proplist *         proplist;
  pa_usec_t     configured_latency;
  pa_volume_t   base_volume;
  pa_sink_state_t       state;
  uint32_t      n_volume_steps;
  uint32_t      card;
  uint32_t      n_ports;
  pa_sink_port_info **  ports;
  pa_sink_port_info *   active_port;
  uint8_t       n_formats;
  pa_format_info **     formats;
} pa_sink_info

关于此结构的更多信息保存在 Doxygen " pa_sink_info结构参考"( http://freedesktop.org/software/pulseaudio/doxygen/structpa__sink__info.html )中。

供参考,主要字段是namedescriptionindex是一些数据结构的不透明索引,在许多 PulseAudio 函数中使用。proplist是可能包含有趣信息的一般信息图。这可以通过遍历地图来检索。

输入设备也有类似的回调和数据结构。

A program to list input and output devices current when the application connects to the server is palist_devices.c:
/**
 * palist_devices.c
 * Jan Newmarch
 */

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define _(x) x

// quit when this reaches 2
int no_more_sources_or_sinks = 0;

int ret;

pa_context *context;

void show_error(char *s) {
    fprintf(stderr, "%s\n", s);
}

void print_properties(pa_proplist *props) {
    void *state = NULL;

    printf("  Properties are: \n");
    while (1) {
        char *key;
        if ((key = pa_proplist_iterate(props, &state)) == NULL) {
            return;
        }
        char *value = pa_proplist_gets(props, key);
        printf("   key: %s, value: %s\n", key, value);
    }
}

/**
 * print information about a sink
 */
void sinklist_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata) {

    // If eol is set to a positive number, you're at the end of the list
    if (eol > 0) {
        printf("**No more sinks\n");
        no_more_sources_or_sinks++;
        if (no_more_sources_or_sinks == 2)
            exit(0);
        return;
    }

    printf("Sink: name %s, description %s\n", i->name, i->description);
    print_properties(i->proplist);
}

/**
 * print information about a source

 */
void sourcelist_cb(pa_context *c, const pa_source_info *i, int eol, void *userdata) {
    if (eol > 0) {
        printf("**No more sources\n");
        no_more_sources_or_sinks++;
        if (no_more_sources_or_sinks == 2)
            exit(0);
        return;
    }

    printf("Source: name %s, description %s\n", i->name, i->description);
    print_properties(i->proplist);
}

void context_state_cb(pa_context *c, void *userdata) {

    switch (pa_context_get_state(c)) {
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
        break;

    case PA_CONTEXT_READY: {
        pa_operation *o;

        // set up a callback to tell us about source devices
        if (!(o = pa_context_get_source_info_list(c,
                                            sourcelist_cb,
                                            NULL
                                                  ))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        // set up a callback to tell us about sink devices
        if (!(o = pa_context_get_sink_info_list(c,
                                            sinklist_cb,
                                            NULL
                                                  ))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        break;
    }

    case PA_CONTEXT_FAILED:
    case PA_CONTEXT_TERMINATED:
    default:
        return;
    }
}

int main(int argc, char *argv[]) {

    // Define our pulse audio loop and connection variables
    pa_mainloop *pa_ml;
    pa_mainloop_api *pa_mlapi;

    // Create a mainloop API and connection to the default server
    pa_ml = pa_mainloop_new();
    pa_mlapi = pa_mainloop_get_api(pa_ml);
    context = pa_context_new(pa_mlapi, "Device list");

    // This function connects to the pulse server
    pa_context_connect(context, NULL, 0, NULL);

    // This function defines a callback so the server will tell us its state.
    pa_context_set_state_callback(context, context_state_cb, NULL);

    if (pa_mainloop_run(pa_ml, &ret) < 0) {
        printf("pa_mainloop_run() failed.");
        exit(1);
    }
}

在我的笔记本电脑上,它给出了以下内容(省略):

Source: name alsa_output.pci-0000_01_00.1.hdmi-stereo.monitor, description Monitor of HDMI Audio stub Digital Stereo (HDMI)
  Properties are:
   key: device.description, value: Monitor of HDMI Audio stub Digital Stereo (HDMI)
   key: device.class, value: monitor
   key: alsa.card, value: 1
   key: alsa.card_name, value: HDA NVidia
   key: alsa.long_card_name, value: HDA NVidia at 0xe5080000 irq 17
   key: alsa.driver_name, value: snd_hda_intel
   key: device.bus_path, value: pci-0000:01:00.1
   key: sysfs.path, value: /devices/pci0000:00/0000:00:01.0/0000:01:00.1/sound/card1
   key: device.bus, value: pci
   key: device.vendor.id, value: 10de
   key: device.vendor.name, value: nVidia Corporation
   key: device.product.id, value: 0e08
   key: device.product.name, value: HDMI Audio stub
   key: device.string, value: 1
   key: module-udev-detect.discovered, value: 1
   key: device.icon_name, value: audio-card-pci
Source: name alsa_output.pci-0000_00_1b.0.analog-stereo.monitor, description Monitor of Internal Audio Analog Stereo
  Properties are:
   ...
Source: name alsa_input.pci-0000_00_1b.0.analog-stereo, description Internal Audio Analog Stereo
  Properties are:
  ...
Source: name alsa_output.usb-Creative_Technology_Ltd_SB_X-Fi_Surround_5.1_Pro_000003d0-00-Pro.analog-stereo.monitor, description Monitor of SB X-Fi Surround 5.1 Pro Analog Stereo
  Properties are:
  ...
Source: name alsa_input.usb-Creative_Technology_Ltd_SB_X-Fi_Surround_5.1_Pro_000003d0-00-Pro.analog-stereo, description SB X-Fi Surround 5.1 Pro Analog Stereo
  Properties are: 

  ...
**No more sources
Sink: name alsa_output.pci-0000_01_00.1.hdmi-stereo, description HDMI Audio stub Digital Stereo (HDMI)
  Properties are:
   key: alsa.resolution_bits, value: 16
   key: device.api, value: alsa
   key: device.class, value: sound
   key: alsa.class, value: generic
   key: alsa.subclass, value: generic-mix
   key: alsa.name, value: HDMI 0
   key: alsa.id, value: HDMI 0
   key: alsa.subdevice, value: 0
   key: alsa.subdevice_name, value: subdevice #0
   key: alsa.device, value: 3
   key: alsa.card, value: 1
   key: alsa.card_name, value: HDA NVidia
   key: alsa.long_card_name, value: HDA NVidia at 0xe5080000 irq 17
   key: alsa.driver_name, value: snd_hda_intel
   key: device.bus_path, value: pci-0000:01:00.1
   key: sysfs.path, value: /devices/pci0000:00/0000:00:01.0/0000:01:00.1/sound/card1
   key: device.bus, value: pci
   key: device.vendor.id, value: 10de
   key: device.vendor.name, value: nVidia Corporation
   key: device.product.id, value: 0e08
   key: device.product.name, value: HDMI Audio stub

   key: device.string, value: hdmi:1
   key: device.buffering.buffer_size, value: 352768
   key: device.buffering.fragment_size, value: 176384
   key: device.access_mode, value: mmap+timer
   key: device.profile.name, value: hdmi-stereo
   key: device.profile.description, value: Digital Stereo (HDMI)
   key: device.description, value: HDMI Audio stub Digital Stereo (HDMI)
   key: alsa.mixer_name, value: Nvidia GPU 1c HDMI/DP
   key: alsa.components, value: HDA:10de001c,10281494,00100100
   key: module-udev-detect.discovered, value: 1
   key: device.icon_name, value: audio-card-pci
Sink: name alsa_output.pci-0000_00_1b.0.analog-stereo, description Internal Audio Analog Stereo
  Properties are:
  ...
Sink: name alsa_output.usb-Creative_Technology_Ltd_SB_X-Fi_Surround_5.1_Pro_000003d0-00-Pro.analog-stereo, description SB X-Fi Surround 5.1 Pro Analog Stereo
  Properties are:
  ...
**No more sinks

一个具有相同效果的替代程序是由 Igor Brezac 和 Eric Connell 编写的 PulseAudio:一个获取设备列表的异步示例( www.ypass.net/blog/2009/10/pulseaudio-an-async-example-to-get-device-lists )。它不像前一个那样遵循复杂的路线,因为它只向服务器查询它的设备。然而,它使用自己的状态机来跟踪它在回调过程中的位置!

监测正在发生的变化:新的源和汇

前面的程序列出了在建立与服务器的连接时向 PulseAudio 注册的源设备和接收设备。但是,当连接新设备或断开现有设备时,PulseAudio 会在上下文中注册一个更改,这也可以通过回调来监控。

这样做的关键是用pa_context_subscribe订阅上下文变化。这需要一个上下文、订阅事件的掩码和用户数据。掩码的可能值在 http://freedesktop.org/software/pulseaudio/doxygen/def_8h.html#ad4e7f11f879e8c77ae5289145ecf6947 处描述,包括汇点变化的PA_SUBSCRIPTION_MASK_SINK和汇点输入事件的PA_SUBSCRIPTION_MASK_SINK_INPUT

设置回调函数来监控这些变化有点奇怪。函数pa_context_subscribe接受一个类型为pa_context_success_cb的回调函数,但是它不包含关于回调原因的信息。与其这样,还不如先给pa_context_set_subscribe_callback打电话。这需要一个类型为pa_context_subscribe_cb_t的回调函数,它确实传递了这样的信息。然后在pa_context_subscribeNULL回拨!

pa_context_subscribe_cb_t订阅回调中,可以检查回调的原因并调用适当的代码。如果发现一个新的接收器订阅,那么可以用pa_context_get_sink_info_by_index找到关于接收器的信息,这需要另一个回调!在追踪所有这些回调之后,您最终可以获得新设备的信息。

注意pa_context_get_sink_info_list使用的回调函数和pa_context_get_sink_info_by_index使用的回调函数是一样的。无论接收器设备是单个设备还是设备列表中的一个设备,每个接收器设备都会调用一次回调。

A program to list devices on connection and also to list changes as devices are connected or disconnected is palist_devices_ongoing.c:
/**
 * palist_clients.c
 * Jan Newmarch
 */

/***
    This file is based on pacat.c and pavuctl.c,  part of PulseAudio.

    pacat.c:
    Copyright 2004-2006 Lennart Poettering
    Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB

    PulseAudio is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser

General Public License as published
    by the Free Software Foundation; either version 2.1 of the License,
    or (at your option) any later version.

    PulseAudio 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 Lesser General Public License
    along with PulseAudio; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
    USA.
***/

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define _(x) x

int ret;

pa_context *context;

void show_error(char *s) {
    fprintf(stderr, "%s\n", s);
}

void print_properties(pa_proplist *props) {
    void *state = NULL;

    printf("  Properties are: \n");
    while (1) {
        char *key;
        if ((key = pa_proplist_iterate(props, &state)) == NULL) {
            return;
        }
        char *value = pa_proplist_gets(props, key);
        printf("   key: %s, value: %s\n", key, value);
    }
}

/**
 * print information about a sink

 */
void sink_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata) {

    // If eol is set to a positive number, you're at the end of the list
    if (eol > 0) {
        return;
    }

    printf("Sink: name %s, description %s\n", i->name, i->description);
    // print_properties(i->proplist);
}

/**
 * print information about a source
 */
void source_cb(pa_context *c, const pa_source_info *i, int eol, void *userdata) {
    if (eol > 0) {
        return;
    }

    printf("Source: name %s, description %s\n", i->name, i->description);
    // print_properties(i->proplist);
}

void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) {

    switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {

    case PA_SUBSCRIPTION_EVENT_SINK:
        if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
            printf("Removing sink index %d\n", index);
        else {
            pa_operation *o;
            if (!(o = pa_context_get_sink_info_by_index(c, index, sink_cb, NULL))) {
                show_error(_("pa_context_get_sink_info_by_index() failed"));
                return;

            }
            pa_operation_unref(o);
        }
        break;

    case PA_SUBSCRIPTION_EVENT_SOURCE:
        if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
            printf("Removing source index %d\n", index);
        else {
            pa_operation *o;
            if (!(o = pa_context_get_source_info_by_index(c, index, source_cb, NULL))) {
                show_error(_("pa_context_get_source_info_by_index() failed"));
                return;
            }
            pa_operation_unref(o);
        }
        break;
    }
}

void context_state_cb(pa_context *c, void *userdata) {

    switch (pa_context_get_state(c)) {
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
        break;

    case PA_CONTEXT_READY: {
        pa_operation *o;

        if (!(o = pa_context_get_source_info_list(c,
                                                  source_cb,
                                                  NULL
                                                  ))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        if (!(o = pa_context_get_sink_info_list(c,
                                                sink_cb,
                                                NULL
                                                ))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        pa_context_set_subscribe_callback(c, subscribe_cb, NULL);

        if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)
                                       (PA_SUBSCRIPTION_MASK_SINK|
                                        PA_SUBSCRIPTION_MASK_SOURCE), NULL, NULL))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        break;
    }

    case PA_CONTEXT_FAILED:
    case PA_CONTEXT_TERMINATED:
    default:
        return;
    }
}

int main(int argc, char *argv[]) {

    // Define our pulse audio loop and connection variables
    pa_mainloop *pa_ml;
    pa_mainloop_api *pa_mlapi;
    pa_operation *pa_op;
    pa_time_event *time_event;

    // Create a mainloop API and connection to the default server

    pa_ml = pa_mainloop_new();
    pa_mlapi = pa_mainloop_get_api(pa_ml);
    context = pa_context_new(pa_mlapi, "Device list");

    // This function connects to the pulse server
    pa_context_connect(context, NULL, 0, NULL);

    // This function defines a callback so the server will tell us its state.
    pa_context_set_state_callback(context, context_state_cb, NULL);

    if (pa_mainloop_run(pa_ml, &ret) < 0) {
        printf("pa_mainloop_run() failed.");
        exit(1);
    }
}

记录一条小溪

如果你从 FreeDesktop.org(www.freedesktop.org/wiki/Software/PulseAudio/Download)下载 PulseAudio 的源码,你会在utils目录中找到一个名为pacat.c的程序。这个程序使用了一些私有 API,不会使用公共库进行编译。它还拥有你期望从制作程序中得到的所有功能。我已经把它去掉了复杂性,这样你就可以找到进入这个 API 的方法。文件是parec.c

/**

 * parec.c
 * Jan Newmarch
 */

/***
  This file is based on pacat.c,  part of PulseAudio.

  pacat.c:
  Copyright 2004-2006 Lennart Poettering
  Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB

  PulseAudio is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation; either version 2.1 of the License,
  or (at your option) any later version.

  PulseAudio 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 Lesser General Public License
  along with PulseAudio; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  USA.
***/

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define CLEAR_LINE "\n"
#define _(x) x

// From pulsecore/macro.h
#define pa_memzero(x,l) (memset((x), 0, (l)))
#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))

int fdout;
char *fname = "tmp.s16";

int verbose = 1;
int ret;

pa_context *context;

static pa_sample_spec sample_spec = {
  .format = PA_SAMPLE_S16LE,
  .rate = 44100,
  .channels = 2
};

static pa_stream *stream = NULL;

/* This is my builtin card. Use paman to find yours
   or set it to NULL to get the default device
*/
static char *device = "alsa_input.pci-0000_00_1b.0.analog-stereo";

static pa_stream_flags_t flags = 0;

void stream_state_callback(pa_stream *s, void *userdata) {
  assert(s);

  switch (pa_stream_get_state(s)) {
  case PA_STREAM_CREATING:
    // The stream has been created, so
    // let's open a file to record to
    printf("Creating stream\n");
    fdout = creat(fname,  0711);
    break;

  case PA_STREAM_TERMINATED:

    close(fdout);
    break;

  case PA_STREAM_READY:

    // Just for info: no functionality in this branch
    if (verbose) {
      const pa_buffer_attr *a;
      char cmt[PA_CHANNEL_MAP_SNPRINT_MAX], sst[PA_SAMPLE_SPEC_SNPRINT_MAX];

      printf("Stream successfully created.");

      if (!(a = pa_stream_get_buffer_attr(s)))
        printf("pa_stream_get_buffer_attr() failed: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
      else {
        printf("Buffer metrics: maxlength=%u, fragsize=%u", a->maxlength, a->fragsize);

      }

      printf("Connected to device %s (%u, %ssuspended).",
             pa_stream_get_device_name(s),
             pa_stream_get_device_index(s),
             pa_stream_is_suspended(s) ? "" : "not ");
    }

    break;

  case PA_STREAM_FAILED:
  default:
    printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
    exit(1);
  }
}

/*********** Stream callbacks **************/

/* This is called whenever new data is available */
static void stream_read_callback(pa_stream *s, size_t length, void *userdata) {

  assert(s);
  assert(length > 0);

  // Copy the data from the server out to a file
  fprintf(stderr, "Can read %d\n", length);

  while (pa_stream_readable_size(s) > 0) {
    const void *data;
    size_t length;

    // peek actually creates and fills the data vbl
    if (pa_stream_peek(s, &data, &length) < 0) {
      fprintf(stderr, "Read failed\n");
      exit(1);
      return;
    }
    fprintf(stderr, "Writing %d\n", length);
    write(fdout, data, length);

    // swallow the data peeked at before
    pa_stream_drop(s);
  }
}

// This callback gets called when our context changes state.  We really only
// care about when it's ready or if it has failed
void state_cb(pa_context *c, void *userdata) {
  pa_context_state_t state;
  int *pa_ready = userdata;

  printf("State changed\n");
  state = pa_context_get_state(c);
  switch  (state) {
    // There are just here for reference

  case PA_CONTEXT_UNCONNECTED:
  case PA_CONTEXT_CONNECTING:
  case PA_CONTEXT_AUTHORIZING:
  case PA_CONTEXT_SETTING_NAME:
  default:
    break;
  case PA_CONTEXT_FAILED:
  case PA_CONTEXT_TERMINATED:
    *pa_ready = 2;
    break;
  case PA_CONTEXT_READY: {
    pa_buffer_attr buffer_attr;

    if (verbose)
      printf("Connection established.%s\n", CLEAR_LINE);

    if (!(stream = pa_stream_new(c, "JanCapture", &sample_spec, NULL))) {
      printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1);
    }

    // Watch for changes in the stream state to create the output file
    pa_stream_set_state_callback(stream, stream_state_callback, NULL);

    // Watch for changes in the stream's read state to write to the output file
    pa_stream_set_read_callback(stream, stream_read_callback, NULL);

    // Set properties of the record buffer

    pa_zero(buffer_attr);
    buffer_attr.maxlength = (uint32_t) -1;
    buffer_attr.prebuf = (uint32_t) -1;
    buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) -1;
    buffer_attr.minreq = (uint32_t) -1;

    // and start recording
    if (pa_stream_connect_record(stream, device, &buffer_attr, flags) < 0) {
      printf("pa_stream_connect_record() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1);
    }
  }

    break;
  }
}

int main(int argc, char *argv[]) {

  // Define our pulse audio loop and connection variables
  pa_mainloop *pa_ml;
  pa_mainloop_api *pa_mlapi;
  pa_operation *pa_op;
  pa_time_event *time_event;

  // Create a mainloop API and connection to the default server
  pa_ml = pa_mainloop_new();
  pa_mlapi = pa_mainloop_get_api(pa_ml);
  context = pa_context_new(pa_mlapi, "test");

  // This function connects to the pulse server
  pa_context_connect(context, NULL, 0, NULL);

  // This function defines a callback so the server will tell us its state.
  pa_context_set_state_callback(context, state_cb, NULL);

  if (pa_mainloop_run(pa_ml, &ret) < 0) {
    printf("pa_mainloop_run() failed.");
    exit(1);
  }
}

播放文件

记录输入流是通过调用pa_stream_peek在流读取回调中完成的。类似地,播放输出流是通过调用pa_stream_write的流写回调来完成的。

在下面的程序中,回调是在上下文状态更改回调的PA_CONTEXT_READY分支中设置的。向流写回调传递消费流准备接收的字节数,因此从文件中读取该字节数并将它们写入流。

在文件结尾必须小心。PulseAudio 的输出缓冲区中可能有未播放的素材。这需要在程序退出前排空。这是通过函数pa_stream_drain完成的。在文件结束时,首先将流写回调设置为null,这样输出流就不会一直调用更多的输入,然后清空流。完成后将调用流排出完成回调,因此程序可以退出(或做其他事情)。

在这个程序中,我包含了比以前更多的回调函数,以展示可以监控的特性范围。

节目是pacat2.c

/**
 * pacat2.c
 * Jan Newmarch
 */

/***
  This file is based on pacat.c,  part of PulseAudio.

  pacat.c:
  Copyright 2004-2006 Lennart Poettering
  Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB

  PulseAudio is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation; either version 2.1 of the License,
  or (at your option) any later version.

  PulseAudio 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 Lesser General Public License
  along with PulseAudio; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  USA.
***/

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

// ???
#define CLEAR_LINE "\n"

// From pulsecore/macro.h JN
#define pa_memzero(x,l) (memset((x), 0, (l)))
#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))

int verbose = 1;
int ret;

static pa_volume_t volume = PA_VOLUME_NORM;
static int volume_is_set = 0;

static int fdin;

static pa_sample_spec sample_spec = {
  .format = PA_SAMPLE_S16LE,
  .rate = 44100,
  .channels = 2
};

static pa_stream *stream = NULL;
static pa_channel_map channel_map;
static pa_proplist *proplist = NULL;

// Define our pulse audio loop and connection variables
static pa_mainloop *mainloop;
static pa_mainloop_api *mainloop_api;
static pa_operation *pa_op;
static pa_context *context = NULL;

static void *buffer = NULL;
static size_t buffer_length = 0, buffer_index = 0;

static pa_io_event* stdio_event = NULL;

// Get device name from e.g. paman
//static char *device = "alsa_output.pci-0000_00_1b.0.analog-stereo";
// Use default device
static char *device = NULL;

static pa_stream_flags_t flags = 0;

static size_t latency = 0, process_time = 0;
static int32_t latency_msec = 1, process_time_msec = 0;

static int raw = 1;

/* Connection draining complete */
static void context_drain_complete(pa_context*c, void *userdata) {
  pa_context_disconnect(c);
}

static void stream_drain_complete(pa_stream*s, int success, void *userdata) {
  pa_operation *o = NULL;

  if (!success) {
    printf("Failed to drain stream: %s", pa_strerror(pa_context_errno(context)));
    exit(1);
  }

  if (verbose)
    printf("Playback stream drained.");

  pa_stream_disconnect(stream);
  pa_stream_unref(stream);
  stream = NULL;

  if (!(o = pa_context_drain(context, context_drain_complete, NULL)))
    pa_context_disconnect(context);

  else {
    pa_operation_unref(o);
    if (verbose)
      printf("Draining connection to server.");
  }
}

/* Start draining */
static void start_drain(void) {
  printf("Draining\n");
  if (stream) {
    pa_operation *o;

    pa_stream_set_write_callback(stream, NULL, NULL);

    if (!(o = pa_stream_drain(stream, stream_drain_complete, NULL))) {
      //printf("pa_stream_drain(): %s", pa_strerror(pa_context_errno(context)));
      exit(1);
      return;
    }

    pa_operation_unref(o);
  } else
    exit(0);
}

/* Write some data to the stream */
static void do_stream_write(size_t length) {
  size_t l;
  assert(length);

  printf("do stream write: Writing %d to stream\n", length);

  if (!buffer || !buffer_length) {
    buffer = pa_xmalloc(length);
    buffer_length = length;
    buffer_index = 0;
    //printf("  return without writing\n");
    //return;

  }

  while (buffer_length > 0) {
    l = read(fdin, buffer + buffer_index, buffer_length);
    if (l <= 0) {
      start_drain();
      return;
    }
    if (pa_stream_write(stream, (uint8_t*) buffer + buffer_index, l, NULL, 0, PA_SEEK_RELATIVE) < 0) {
      printf("pa_stream_write() failed: %s", pa_strerror(pa_context_errno(context)));
      exit(1);
      return;
    }
    buffer_length -= l;
    buffer_index += l;

    if (!buffer_length) {
      pa_xfree(buffer);
      buffer = NULL;
      buffer_index = buffer_length = 0;
    }
  }
}

void stream_state_callback(pa_stream *s, void *userdata) {
  assert(s);

  switch (pa_stream_get_state(s)) {
  case PA_STREAM_CREATING:
    break;
  case PA_STREAM_TERMINATED:

    break;

  case PA_STREAM_READY:

    if (verbose) {
      const pa_buffer_attr *a;
      char cmt[PA_CHANNEL_MAP_SNPRINT_MAX], sst[PA_SAMPLE_SPEC_SNPRINT_MAX];

      printf("Stream successfully created.\n");

      if (!(a = pa_stream_get_buffer_attr(s)))
        printf("pa_stream_get_buffer_attr() failed: %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
      else {
        printf("Buffer metrics: maxlength=%u, fragsize=%u\n", a->maxlength, a->fragsize);

      }
      /*
        printf("Using sample spec '%s', channel map '%s'.",
        pa_sample_spec_snprint(sst, sizeof(sst), pa_stream_get_sample_spec(s)),
        pa_channel_map_snprint(cmt, sizeof(cmt), pa_stream_get_channel_map(s)));
      */

      printf("Connected to device %s (%u, %ssuspended).\n",
             pa_stream_get_device_name(s),
             pa_stream_get_device_index(s),
             pa_stream_is_suspended(s) ? "" : "not ");
    }

    break;

  case PA_STREAM_FAILED:
  default:
    printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
    exit(1); //quit(1);

  }
}

/*********** Stream callbacks **************/

static void stream_success(pa_stream *s, int succes, void *userdata) {
  printf("Succeded\n");
}

static void stream_suspended_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose) {
    if (pa_stream_is_suspended(s))
      fprintf(stderr, "Stream device suspended.%s \n", CLEAR_LINE);
    else
      fprintf(stderr, "Stream device resumed.%s \n", CLEAR_LINE);
  }
}

static void stream_underflow_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream underrun.%s \n",  CLEAR_LINE);
}

static void stream_overflow_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream overrun.%s \n", CLEAR_LINE);
}

static void stream_started_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream started.%s \n", CLEAR_LINE);
}

static void stream_moved_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream moved to device %s (%u, %ssuspended).%s \n", pa_stream_get_device_name(s), pa_stream_get_device_index(s), pa_stream_is_suspended(s) ? "" : "not ",  CLEAR_LINE);
}

static void stream_buffer_attr_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream buffer attributes changed.%s \n",  CLEAR_LINE);
}

static void stream_event_callback(pa_stream *s, const char *name, pa_proplist *pl, void *userdata) {
  char *t;

  assert(s);
  assert(name);
  assert(pl);

  t = pa_proplist_to_string_sep(pl, ", ");
  fprintf(stderr, "Got event '%s', properties '%s'\n", name, t);
  pa_xfree(t);

}

/* This is called whenever new data may be written to the stream */
static void stream_write_callback(pa_stream *s, size_t length, void *userdata) {
  //assert(s);
  //assert(length > 0);

  printf("Stream write callback: Ready to write %d bytes\n", length);

  printf("  do stream write from stream write callback\n");
  do_stream_write(length);

 }

// This callback gets called when our context changes state.  We really only
// care about when it's ready or if it has failed
void state_cb(pa_context *c, void *userdata) {
  pa_context_state_t state;
  int *pa_ready = userdata;

  printf("State changed\n");
  state = pa_context_get_state(c);
  switch  (state) {
    // There are just here for reference
  case PA_CONTEXT_UNCONNECTED:
  case PA_CONTEXT_CONNECTING:
  case PA_CONTEXT_AUTHORIZING:
  case PA_CONTEXT_SETTING_NAME:
  default:
    break;
  case PA_CONTEXT_FAILED:
  case PA_CONTEXT_TERMINATED:
    *pa_ready = 2;
    break;
  case PA_CONTEXT_READY: {
    pa_buffer_attr buffer_attr;

    if (verbose)
      printf("Connection established.%s\n", CLEAR_LINE);

    if (!(stream = pa_stream_new(c, "JanPlayback", &sample_spec, NULL))) {
      printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1); // goto fail;
    }

    pa_stream_set_state_callback(stream, stream_state_callback, NULL);

    pa_stream_set_write_callback(stream, stream_write_callback, NULL);

    //pa_stream_set_read_callback(stream, stream_read_callback, NULL);

    pa_stream_set_suspended_callback(stream, stream_suspended_callback, NULL);
    pa_stream_set_moved_callback(stream, stream_moved_callback, NULL);
    pa_stream_set_underflow_callback(stream, stream_underflow_callback, NULL);
    pa_stream_set_overflow_callback(stream, stream_overflow_callback, NULL);

    pa_stream_set_started_callback(stream, stream_started_callback, NULL);

    pa_stream_set_event_callback(stream, stream_event_callback, NULL);
    pa_stream_set_buffer_attr_callback(stream, stream_buffer_attr_callback, NULL);

    pa_zero(buffer_attr);
    buffer_attr.maxlength = (uint32_t) -1;
    buffer_attr.prebuf = (uint32_t) -1;

    pa_cvolume cv;

    if (pa_stream_connect_playback(stream, NULL, &buffer_attr, flags,
                                   NULL,
                                   NULL) < 0) {
      printf("pa_stream_connect_playback() failed: %s", pa_strerror(pa_context_errno(c)));

      exit(1); //goto fail;
    } else {
      printf("Set playback callback\n");
    }

    pa_stream_trigger(stream, stream_success, NULL);
  }

    break;
  }
}

int main(int argc, char *argv[]) {

  struct stat st;
  off_t size;
  ssize_t nread;

  // We'll need these state variables to keep track of our requests
  int state = 0;
  int pa_ready = 0;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s file\n", argv[0]);
    exit(1);
  }
  // slurp the whole file into buffer
  if ((fdin = open(argv[1],  O_RDONLY)) == -1) {
    perror("open");
    exit(1);
  }

  // Create a mainloop API and connection to the default server
  mainloop = pa_mainloop_new();

  mainloop_api = pa_mainloop_get_api(mainloop);
  context = pa_context_new(mainloop_api, "test");

  // This function connects to the pulse server
  pa_context_connect(context, NULL, 0, NULL);
  printf("Connecting\n");

  // This function defines a callback so the server will tell us it's state.
  // Our callback will wait for the state to be ready.  The callback will
  // modify the variable to 1 so we know when we have a connection and it's
  // ready.
  // If there's an error, the callback will set pa_ready to 2
  pa_context_set_state_callback(context, state_cb, &pa_ready);

  if (pa_mainloop_run(mainloop, &ret) < 0) {
    printf("pa_mainloop_run() failed.");
    exit(1); // goto quit
  }

}

当延迟设置为默认值时,每次回调可以写入的字节数是 65,470 字节。这给出了 65,470 / 44,100 秒的最小延迟,即大约 1500 毫秒。当延迟和处理时间都设置为 1 毫秒时,缓冲区大小约为 1440 字节,延迟为 32 毫秒。

使用 I/O 回调播放文件

将文件写入输出流很简单:从文件读入缓冲区,并通过写入流来保持清空缓冲区。从文件中读取很简单:使用标准的 Unix read函数。您请求读取一些字节,而read函数返回实际读取的字节数。这在上一节中已经讨论过了。

PulseAudio 发行版中的程序使用更复杂的系统。它使用 I/O 就绪回调将一些处理传递给 I/O 回调。这利用了两个功能。

  • pa_stream_writable_size告知可以向流中写入多少字节。
  • 向流中写入一定数量的字节。

逻辑如下:通过从文件中读取来填充缓冲区,同时从缓冲区向流中写入尽可能多的字节,直到达到缓冲区大小的限制或流可以接受的字节数,以较小者为准。

在 PulseAudio 中,这是使用回调函数异步完成的。两个相关的功能如下:

  • 函数pa_stream_set_write_callback()注册了一个回调函数,只要流准备好被写入,这个回调函数就会被调用。注册回调如下所示:

    pa_stream_set_write_callback(stream, stream_write_callback, NULL)
    
    

    向回调传递要写入的流(s)和可以写入的字节数(length)。

    void stream_write_callback(pa_stream *s, size_t length, void *userdata)
    
    
  • 从文件中读取的回调由保存在mainloop_api表中的一个函数注册。注册函数是io_new,并被传递一个文件和回调函数的 Unix 文件描述符。注册回调看起来像这样:

    mainloop_api->io_new(mainloop_api,
                         fdin,
                         PA_IO_EVENT_INPUT,
                         stdin_callback, NULL))
    
    

    回调被传递文件描述符(fd)来读取。

    void stdin_callback(pa_mainloop_api *mainloop_api, pa_io_event *stdio_event,
                        int fd, pa_io_event_flags_t f, void *userdata)
    
    

    注意 PulseAudio 代码从源文件的描述符到STDIN_FILENO做了一个dup2,它匹配函数的名称。我看不出这有什么意义,反正他们的代码用的是fd

这些回调应该在什么时候注册?流写回调可以在流创建后的任何时候注册,这是由pa_stream_new完成的。对于stdin回调,我只能通过在流准备好之后注册它来让它正常工作,也就是说,在流状态回调函数的PA_STREAM_READY分支中。

那么,在所有这些之后,程序的逻辑是什么?

  • stdin回调中:
    • 如果缓冲区里有东西,就返回。再加就没有意义了。
    • 如果缓冲区是空的,那么查询流,看看有多少可以写入其中。
    • 如果流没有更多内容,那么只需将一些内容读入缓冲区并返回。
    • 如果流可以被写入,那么从文件中读入缓冲区并将其写入流。
  • 在流写回调中:
    • 如果缓冲区不为空,将其内容写入流。

从文件中播放的程序目前看起来像pacat.c

/***
  This file is based on pacat.c,  part of PulseAudio

.

  pacat.c:
  Copyright 2004-2006 Lennart Poettering
  Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB

  PulseAudio is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation; either version 2.1 of the License,
  or (at your option) any later version.

  PulseAudio 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 Lesser General Public License
  along with PulseAudio; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  USA.
***/

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#define CLEAR_LINE "\n"

// From pulsecore/macro.h JN
#define pa_memzero(x,l) (memset((x), 0, (l)))
#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))

int verbose = 1;
int ret;

static pa_volume_t volume = PA_VOLUME_NORM;
static int volume_is_set = 0;

static int fdin;

.

static pa_sample_spec sample_spec = {
  .format = PA_SAMPLE_S16LE,
  .rate = 44100,
  .channels = 2
};

static pa_stream *stream = NULL;
static pa_channel_map channel_map;
static pa_proplist *proplist = NULL;

// Define our pulse audio loop and connection variables
static pa_mainloop *mainloop;
static pa_mainloop_api *mainloop_api;
static pa_operation *pa_op;
static pa_context *context = NULL;

static void *buffer = NULL;
static size_t buffer_length = 0, buffer_index = 0;

static pa_io_event* stdio_event = NULL;

static char *device = "alsa_output.pci-0000_00_1b.0.analog-stereo";

static pa_stream_flags_t flags = 0;

static size_t latency = 0, process_time = 0;
static int32_t latency_msec = 0, process_time_msec = 0;

static int raw = 1;

/* Write some data to the stream */
static void do_stream_write(size_t length) {
  size_t l;
  assert(length);

  printf("do stream write: Writing %d to stream\n", length);

  if (!buffer || !buffer_length) {
    printf("  return without writing\n");
    return;

.
  }

  l = length;
  if (l > buffer_length)
    l = buffer_length;
  printf("  writing %d\n", l);
  if (pa_stream_write(stream, (uint8_t*) buffer + buffer_index, l, NULL, 0, PA_SEEK_RELATIVE) < 0) {
    printf("pa_stream_write() failed: %s", pa_strerror(pa_context_errno(context)));
    exit(1);
    return;
  }

  buffer_length -= l;
  buffer_index += l;

  if (!buffer_length) {
    pa_xfree(buffer);
    buffer = NULL;
    buffer_index = buffer_length = 0;
  }

}

/* Connection draining complete */
static void context_drain_complete(pa_context*c, void *userdata) {
  pa_context_disconnect(c);
}

static void stream_drain_complete(pa_stream*s, int success, void *userdata) {
  pa_operation *o = NULL;

  if (!success) {
    printf("Failed to drain stream: %s", pa_strerror(pa_context_errno(context)));
    exit(1);
  }

  if (verbose)
    printf("Playback stream drained.");

  pa_stream_disconnect(stream);
  pa_stream_unref(stream);
  stream = NULL;

  if (!(o = pa_context_drain(context, context_drain_complete, NULL)))
    pa_context_disconnect(context);

.
  else {
    pa_operation_unref(o);
    if (verbose)
      printf("Draining connection to server.");
  }
}

/* Start draining */
static void start_drain(void) {
  printf("Draining\n");
  if (stream) {
    pa_operation *o;

    pa_stream_set_write_callback(stream, NULL, NULL);

    if (!(o = pa_stream_drain(stream, stream_drain_complete, NULL))) {
      //printf("pa_stream_drain(): %s", pa_strerror(pa_context_errno(context)));
      exit(1);
      return;
    }

    pa_operation_unref(o);
  } else
    exit(0);
}

/* New data on STDIN **/
static void stdin_callback(pa_mainloop_api *mainloop_api, pa_io_event *stdio_event, int fd, pa_io_event_flags_t f, void *userdata) {
  size_t l, w = 0;
  ssize_t r;

  printf("In stdin callback\n");
  //pa_assert(a == mainloop_api);
  // pa_assert(e);
  // pa_assert(stdio_event == e);

  if (buffer) {
    mainloop_api->io_enable(stdio_event, PA_IO_EVENT_NULL);
    printf("  Buffer isn't null\n");

.
    return;
  }

  if (!stream || pa_stream_get_state(stream) != PA_STREAM_READY || !(l = w = pa_stream_writable_size(stream)))
    l = 4096;

  buffer = pa_xmalloc(l);

  if ((r = read(fd, buffer, l)) <= 0) {
    if (r == 0) {
      if (verbose)
        printf("Got EOF.\n");

      start_drain();

    } else {
      printf("read() failed: %s\n", strerror(errno));
      exit(1);
    }

    mainloop_api->io_free(stdio_event);
    stdio_event = NULL;
    return;
  }
  printf("  Read %d\n", r);

  buffer_length = (uint32_t) r;
  buffer_index = 0;

  if (w) {
    printf("  do stream write from stdin callback\n");
    do_stream_write(w);
  }
}

void stream_state_callback(pa_stream *s, void *userdata) {
  assert(s);

  switch (pa_stream_get_state(s)) {
  case PA_STREAM_CREATING:

.
    break;
  case PA_STREAM_TERMINATED:
    break;

  case PA_STREAM_READY:

    if (verbose) {
      const pa_buffer_attr *a;
      char cmt[PA_CHANNEL_MAP_SNPRINT_MAX], sst[PA_SAMPLE_SPEC_SNPRINT_MAX];

      printf("Stream successfully created.\n");

      if (!(a = pa_stream_get_buffer_attr(s)))
        printf("pa_stream_get_buffer_attr() failed: %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
      else {
        printf("Buffer metrics: maxlength=%u, fragsize=%u\n", a->maxlength, a->fragsize);

      }
      /*
        printf("Using sample spec '%s', channel map '%s'.",
        pa_sample_spec_snprint(sst, sizeof(sst), pa_stream_get_sample_spec(s)),
        pa_channel_map_snprint(cmt, sizeof(cmt), pa_stream_get_channel_map(s)));

.
      */

      printf("Connected to device %s (%u, %ssuspended).\n",
             pa_stream_get_device_name(s),
             pa_stream_get_device_index(s),
             pa_stream_is_suspended(s) ? "" : "not ");
    }

    // TRY HERE???

    if (!(stdio_event = mainloop_api->io_new(mainloop_api,
                                             fdin, // STDIN_FILENO,
                                             PA_IO_EVENT_INPUT,
                                             stdin_callback, NULL))) {
      printf("io_new() failed.");
      exit(1);
    }

    break;

  case PA_STREAM_FAILED:
  default:
    printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
    exit(1); //quit(1);
  }
}

/*********** Stream callbacks **************/

static void stream_read_callback(pa_stream *s, size_t length, void *userdata) {
  printf("Raedy to read\n");
}

static void stream_success(pa_stream *s, int succes, void *userdata) {
  printf("Succeded\n")

.;
}

static void stream_suspended_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose) {
    if (pa_stream_is_suspended(s))
      fprintf(stderr, "Stream device suspended.%s \n", CLEAR_LINE);
    else
      fprintf(stderr, "Stream device resumed.%s \n", CLEAR_LINE);
  }
}

static void stream_underflow_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream underrun.%s \n",  CLEAR_LINE);
}

static void stream_overflow_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream overrun.%s \n", CLEAR_LINE);
}

static void stream_started_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream started.%s \n", CLEAR_LINE);
}

static void stream_moved_callback(pa_stream *s, void *userdata) {
  assert(s);

.

  if (verbose)
    fprintf(stderr, "Stream moved to device %s (%u, %ssuspended).%s \n", pa_stream_get_device_name(s), pa_stream_get_device_index(s), pa_stream_is_suspended(s) ? "" : "not ",  CLEAR_LINE);
}

static void stream_buffer_attr_callback(pa_stream *s, void *userdata) {
  assert(s);

  if (verbose)
    fprintf(stderr, "Stream buffer attributes changed.%s \n",  CLEAR_LINE);
}

static void stream_event_callback(pa_stream *s, const char *name, pa_proplist *pl, void *userdata) {
  char *t;

  assert(s);
  assert(name);
  assert(pl);

  t = pa_proplist_to_string_sep(pl, ", ");
  fprintf(stderr, "Got event '%s', properties '%s'\n", name, t);
  pa_xfree(t);
}

/* This is called whenever new data may be written to the stream */
static void stream_write_callback(pa_stream *s, size_t length, void *userdata) {
  //assert(s);

.
  //assert(length > 0);

  printf("Stream write callback: Ready to write %d bytes\n", length);

  if (raw) {
    // assert(!sndfile);

    if (stdio_event)
      mainloop_api->io_enable(stdio_event, PA_IO_EVENT_INPUT);

    if (!buffer)
      return;
    printf("  do stream write from stream write callback\n");
    do_stream_write(length);

  }
}

// This callback gets called when our context changes state.  We really only
// care about when it's ready or if it has failed
void state_cb(pa_context *c, void *userdata) {
  pa_context_state_t state;
  int *pa_ready = userdata;

  printf("State changed\n");
  state = pa_context_get_state(c);
  switch  (state) {
    // There are just here for reference
  case PA_CONTEXT_UNCONNECTED:
  case PA_CONTEXT_CONNECTING:
  case PA_CONTEXT_AUTHORIZING:
  case PA_CONTEXT_SETTING_NAME:
  default:
    break;
  case PA_CONTEXT_FAILED:
  case PA_CONTEXT_TERMINATED:
    *pa_ready = 2;
    break;
  case PA_CONTEXT_READY: {
    pa_buffer_attr buffer_attr;

    if (verbose)
      printf("Connection established.%s\n", CLEAR_LINE);

    if (!(stream = pa_stream_new(c, "JanPlayback", &sample_spec, NULL))) {
      printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1); // goto fail;

.
    }

    pa_stream_set_state_callback(stream, stream_state_callback, NULL);

    pa_stream_set_write_callback(stream, stream_write_callback, NULL);

    pa_stream_set_read_callback(stream, stream_read_callback, NULL);

    pa_stream_set_suspended_callback(stream, stream_suspended_callback, NULL);
    pa_stream_set_moved_callback(stream, stream_moved_callback, NULL);
    pa_stream_set_underflow_callback(stream, stream_underflow_callback, NULL);
    pa_stream_set_overflow_callback(stream, stream_overflow_callback, NULL);

    pa_stream_set_started_callback(stream, stream_started_callback, NULL);

    pa_stream_set_event_callback(stream, stream_event_callback, NULL);
    pa_stream_set_buffer_attr_callback(stream, stream_buffer_attr_callback, NULL);

    pa_zero(buffer_attr);
    buffer_attr.maxlength = (uint32_t) -1;
    buffer_attr.prebuf = (uint32_t) -1;

    buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) -1;
    buffer_attr.minreq = (uint32_t) -1;

    pa_cvolume cv;

    if (pa_stream_connect_playback(stream, NULL, &buffer_attr, flags,
                                   NULL,
                                   NULL) < 0) {
      printf("pa_stream_connect_playback() failed: %s", pa_strerror(pa_context_errno(c)));

.
      exit(1); //goto fail;
    } else {
      printf("Set playback callback\n");
    }

    pa_stream_trigger(stream, stream_success, NULL);
  }

    break;
  }
}

int main(int argc, char *argv[]) {

  struct stat st;
  off_t size;
  ssize_t nread;

  // We'll need these state variables to keep track of our requests
  int state = 0;
  int pa_ready = 0;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s file\n", argv[0]);
    exit(1);
  }
  // slurp the whole file into buffer
  if ((fdin = open(argv[1],  O_RDONLY)) == -1) {
    perror("open");
    exit(1);
  }

  // Create a mainloop API and connection to the default server
  mainloop = pa_mainloop_new();
  mainloop_api = pa_mainloop_get_api(mainloop);
  context = pa_context_new(mainloop_api, "test");

  // This function connects to the pulse server
  pa_context_connect(context, NULL, 0, NULL);
  printf("Connecting\n");

  // This function defines a callback so the server will tell us it's state.
  // Our callback will wait for the state to be ready.  The callback will
  // modify the variable to 1 so we know when we have a connection and it's
  // ready.
  // If there's an error, the callback will set pa_ready to 2
  pa_context_set_state_callback(context, state_cb, &pa_ready);

.

  if (pa_mainloop_run(mainloop, &ret) < 0) {
    printf("pa_mainloop_run() failed.");
    exit(1); // goto quit
  }
}

控制延迟

www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/LactencyControl 中描述了管理延迟。

在您的代码中,当调用pa_stream_connect_playback() resp 时,您必须执行以下操作。pa_stream_connect_record():

  • 在 flags 参数中传递PA_STREAM_ADJUST_LATENCY。只有设置了该标志,PA才会重新配置低级设备的缓冲区大小,并将其调整到您指定的延迟。
  • buffer_attr参数中传递一个pa_buffer_attr结构。在该结构的字段中,确保将每个字段初始化为(uint32_t) -1),除了tlength(用于回放)resp。fragsize(录音用)。将它们初始化为您想要达到的延迟。使用pa_usec_to_bytes(&ss, ...)将延时从时间单位转换为字节。

额外的代码如下:

    // Set properties of the record buffer
    pa_zero(buffer_attr);
    buffer_attr.maxlength = (uint32_t) -1;
    buffer_attr.prebuf = (uint32_t) -1;

    if (latency_msec > 0) {
      buffer_attr.fragsize = buffer_attr.tlength = pa_usec_to_bytes(latency_msec * PA_USEC_PER_MSEC, &sample_spec);
      flags |= PA_STREAM_ADJUST_LATENCY;
    } else if (latency > 0) {
      buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) latency;
      flags |= PA_STREAM_ADJUST_LATENCY;
    } else
      buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) -1;

    if (process_time_msec > 0) {
      buffer_attr.minreq = pa_usec_to_bytes(process_time_msec * PA_USEC_PER_MSEC, &sample_spec);
    } else if (process_time > 0)
      buffer_attr.minreq = (uint32_t) process_time;
    else
      buffer_attr.minreq = (uint32_t) -1;

PulseAudio 还具有估计设备延迟的机制。它使用来自计时事件的信息。必须声明计时器事件回调,如下所示:

 pa_context_rttime_new(context, pa_rtclock_now() + TIME_EVENT_USEC, time_event_callback, NULL))

计时器事件回调是“单次”回调。它安装一个流更新计时器回调,并设置另一个计时器回调。

void time_event_callback(pa_mainloop_api *m,
                                pa_time_event *e, const struct timeval *t,
                                void *userdata) {
    if (stream && pa_stream_get_state(stream) == PA_STREAM_READY) {
        pa_operation *o;
        if (!(o = pa_stream_update_timing_info(stream, stream_update_timing_callback, NULL)))
          1; //pa_log(_("pa_stream_update_timing_info() failed: %s"), pa_strerror(pa_context_errno(context)));
        else
            pa_operation_unref(o);
    }

    pa_context_rttime_restart(context, e, pa_rtclock_now() + TIME_EVENT_USEC);

然后,流更新定时器回调可以估计等待时间。

void stream_update_timing_callback(pa_stream *s, int success, void *userdata) {
    pa_usec_t l, usec;
    int negative = 0;

    // pa_assert(s);

    fprintf(stderr, "Update timing\n");

    if (!success ||
        pa_stream_get_time(s, &usec) < 0 ||
        pa_stream_get_latency(s, &l, &negative) < 0) {
        fprintf(stderr, "Failed to get latency\n");
        return;
    }

    fprintf(stderr, _("Time: %0.3f sec; Latency: %0.0f usec."),
            (float) usec / 1000000,
            (float) l * (negative?-1.0f:1.0f));
    fprintf(stderr, "        \r");
}

通过将fragsizetlength设置为-1,将延迟留给 PulseAudio,我得到如下结果:

Time: 0.850 sec; Latency: 850365 usec.
Time: 0.900 sec; Latency: 900446 usec.
Time: 0.951 sec; Latency: 950548 usec.
Time: 1.001 sec; Latency: 1000940 usec.
Time: 1.051 sec; Latency: 50801 usec.
Time: 1.101 sec; Latency: 100934 usec.
Time: 1.151 sec; Latency: 151007 usec.
Time: 1.201 sec; Latency: 201019 usec.
Time: 1.251 sec; Latency: 251150 usec.
Time: 1.301 sec; Latency: 301160 usec.
Time: 1.351 sec; Latency: 351218 usec.
Time: 1.401 sec; Latency: 401329 usec.
Time: 1.451 sec; Latency: 451400 usec.
Time: 1.501 sec; Latency: 501465 usec.
Time: 1.551 sec; Latency: 551587 usec.
Time: 1.602 sec; Latency: 601594 usec

.

将它们设置为 1 毫秒,我得到了以下结果:

Time: 1.599 sec; Latency: 939 usec.
Time: 1.649 sec; Latency: 1105 usec.
Time: 1.699 sec; Latency: -158 usec.
Time: 1.750 sec; Latency: 1020 usec.
Time: 1.800 sec; Latency: 397 usec.
Time: 1.850 sec; Latency: -52 usec.
Time: 1.900 sec; Latency: 1827 usec.
Time: 1.950 sec; Latency: 529 usec.
Time: 2.000 sec; Latency: -90 usec.
Time: 2.050 sec; Latency: 997 usec.
Time: 2.100 sec; Latency: 436 usec.
Time: 2.150 sec; Latency: 866 usec.
Time: 2.200 sec; Latency: 406 usec.
Time: 2.251 sec; Latency: 1461 usec.
Time: 2.301 sec; Latency: 107 usec.
Time: 2.351 sec; Latency: 1257 usec.

完成这一切的程序是parec-latency.c

/* parec-latency.c */

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define CLEAR_LINE "\n"
#define _(x) x

#define TIME_EVENT_USEC 50000

// From pulsecore/macro.h
#define pa_memzero(x,l) (memset((x), 0, (l)))
#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))

int fdout;
char *fname = "tmp.pcm";

int verbose = 1;
int ret;

pa_context *context;

static pa_sample_spec sample_spec = {
  .format = PA_SAMPLE_S16LE,
  .rate = 44100,

  .channels = 2
};

static pa_stream *stream = NULL;

/* This is my builtin card. Use paman to find yours
   or set it to NULL to get the default device
*/
static char *device = "alsa_input.pci-0000_00_1b.0.analog-stereo";

static pa_stream_flags_t flags = 0;

static size_t latency = 0, process_time = 0;
static int32_t latency_msec = 0, process_time_msec = 0;

void stream_state_callback(pa_stream *s, void *userdata) {
  assert(s);

  switch (pa_stream_get_state(s)) {
  case PA_STREAM_CREATING:
    // The stream has been created, so
    // let's open a file to record to
    printf("Creating stream\n");
    fdout = creat(fname,  0711);
    break;

  case PA_STREAM_TERMINATED:

    close(fdout);
    break;

  case PA_STREAM_READY:

    // Just for info: no functionality in this branch
    if (verbose) {
      const pa_buffer_attr *a;
      char cmt[PA_CHANNEL_MAP_SNPRINT_MAX], sst[PA_SAMPLE_SPEC_SNPRINT_MAX];

      printf("Stream successfully created.");

      if (!(a = pa_stream_get_buffer_attr(s)))
        printf("pa_stream_get_buffer_attr() failed: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
      else {
        printf("Buffer metrics: maxlength=%u, fragsize=%u", a->maxlength, a->fragsize);

      }

      printf("Connected to device %s (%u, %ssuspended).",
             pa_stream_get_device_name(s),
             pa_stream_get_device_index(s),
             pa_stream_is_suspended(s) ? "" : "not ");
    }

    break;

  case PA_STREAM_FAILED:
  default:
    printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
    exit(1);
  }
}

/* Show the current latency */
static void stream_update_timing_callback(pa_stream *s, int success, void *userdata) {
    pa_usec_t l, usec;
    int negative = 0;

    // pa_assert(s);

    fprintf(stderr, "Update timing\n");

    if (!success ||
        pa_stream_get_time(s, &usec) < 0 ||
        pa_stream_get_latency(s, &l, &negative) < 0) {
        // pa_log(_("Failed to get latency"));
        //pa_log(_("Failed to get latency: %s"), pa_strerror(pa_context_errno(context)));
        // quit(1);
        return;
    }

    fprintf(stderr, _("Time: %0.3f sec; Latency: %0.0f usec.\n"),
            (float) usec / 1000000,
            (float) l * (negative?-1.0f:1.0f));
    //fprintf(stderr, "        \r");
}

static void time_event_callback(pa_mainloop_api *m,
                                pa_time_event *e, const struct timeval *t,
                                void *userdata) {
    if (stream && pa_stream_get_state(stream) == PA_STREAM_READY) {
        pa_operation *o;
        if (!(o = pa_stream_update_timing_info(stream, stream_update_timing_callback, NULL)))
          1; //pa_log(_("pa_stream_update_timing_info() failed: %s"), pa_strerror(pa_context_errno(context)));
        else
            pa_operation_unref(o);
    }

    pa_context_rttime_restart(context, e, pa_rtclock_now() + TIME_EVENT_USEC);
}

void get_latency(pa_stream *s) {
  pa_usec_t latency;
  int neg;
  pa_timing_info *timing_info;

  timing_info = pa_stream_get_timing_info(s);

  if (pa_stream_get_latency(s, &latency, &neg) != 0) {
    fprintf(stderr, __FILE__": pa_stream_get_latency() failed\n");

    return;
  }

  fprintf(stderr, "%0.0f usec    \r", (float)latency);
}

/*********** Stream callbacks **************/

/* This is called whenever new data is available */
static void stream_read_callback(pa_stream *s, size_t length, void *userdata) {

  assert(s);
  assert(length > 0);

  // Copy the data from the server out to a file
  //fprintf(stderr, "Can read %d\n", length);

  while (pa_stream_readable_size(s) > 0) {
    const void *data;
    size_t length;

    //get_latency(s);

    // peek actually creates and fills the data vbl
    if (pa_stream_peek(s, &data, &length) < 0) {
      fprintf(stderr, "Read failed\n");
      exit(1);
      return;
    }
    fprintf(stderr, "Writing %d\n", length);
    write(fdout, data, length);

    // swallow the data peeked at before
    pa_stream_drop(s);

  }
}

// This callback gets called when our context changes state.  We really only
// care about when it's ready or if it has failed
void state_cb(pa_context *c, void *userdata) {
  pa_context_state_t state;
  int *pa_ready = userdata;

  printf("State changed\n");
  state = pa_context_get_state(c);
  switch  (state) {
    // There are just here for reference
  case PA_CONTEXT_UNCONNECTED:
  case PA_CONTEXT_CONNECTING:
  case PA_CONTEXT_AUTHORIZING:
  case PA_CONTEXT_SETTING_NAME:
  default:
    break;
  case PA_CONTEXT_FAILED:
  case PA_CONTEXT_TERMINATED:
    *pa_ready = 2;
    break;
  case PA_CONTEXT_READY: {
    pa_buffer_attr buffer_attr;

    if (verbose)
      printf("Connection established.%s\n", CLEAR_LINE);

    if (!(stream = pa_stream_new(c, "JanCapture", &sample_spec, NULL))) {
      printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1);
    }

    // Watch for changes in the stream state to create the output file
    pa_stream_set_state_callback(stream, stream_state_callback, NULL);

    // Watch for changes in the stream's read state to write to the output file
    pa_stream_set_read_callback(stream, stream_read_callback, NULL);

    // timing info
    pa_stream_update_timing_info(stream, stream_update_timing_callback, NULL);

    // Set properties of the record buffer
    pa_zero(buffer_attr);
    buffer_attr.maxlength = (uint32_t) -1;
    buffer_attr.prebuf = (uint32_t) -1;

    if (latency_msec > 0) {
      buffer_attr.fragsize = buffer_attr.tlength = pa_usec_to_bytes(latency_msec * PA_USEC_PER_MSEC, &sample_spec);
      flags |= PA_STREAM_ADJUST_LATENCY;
    } else if (latency > 0) {
      buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) latency;
      flags |= PA_STREAM_ADJUST_LATENCY;
    } else
      buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) -1;

    if (process_time_msec > 0) {
      buffer_attr.minreq = pa_usec_to_bytes(process_time_msec * PA_USEC_PER_MSEC, &sample_spec);
    } else if (process_time > 0)
      buffer_attr.minreq = (uint32_t) process_time;
    else
      buffer_attr.minreq = (uint32_t) -1;

    flags |= PA_STREAM_INTERPOLATE_TIMING;

    get_latency(stream);

    // and start recording
    if (pa_stream_connect_record(stream, device, &buffer_attr, flags) < 0) {
      printf("pa_stream_connect_record() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1);

    }
  }

    break;
  }
}

int main(int argc, char *argv[]) {

  // Define our pulse audio loop and connection variables
  pa_mainloop *pa_ml;
  pa_mainloop_api *pa_mlapi;
  pa_operation *pa_op;
  pa_time_event *time_event;

  // Create a mainloop API and connection to the default server
  pa_ml = pa_mainloop_new();
  pa_mlapi = pa_mainloop_get_api(pa_ml);
  context = pa_context_new(pa_mlapi, "test");

  // This function connects to the pulse server
  pa_context_connect(context, NULL, 0, NULL);

  // This function defines a callback so the server will tell us its state.
  pa_context_set_state_callback(context, state_cb, NULL);

  if (!(time_event = pa_context_rttime_new(context, pa_rtclock_now() + TIME_EVENT_USEC, time_event_callback, NULL))) {
    //pa_log(_("pa_context_rttime_new() failed."));
    //goto quit;
  }

  if (pa_mainloop_run(pa_ml, &ret) < 0) {
    printf("pa_mainloop_run() failed.");
    exit(1);
  }
}

向扬声器播放麦克风

结合你目前所拥有的,你得到pa-mic-2-speaker.c

/*
 * Copy from microphone to speaker
 * pa-mic-2-speaker.c
 */

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define CLEAR_LINE "\n"
#define BUFF_LEN 4096

// From pulsecore/macro.h
#define pa_memzero(x,l) (memset((x), 0, (l)))
#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))

static void *buffer = NULL;
static size_t buffer_length = 0, buffer_index = 0;

int verbose = 1;
int ret;

static pa_sample_spec sample_spec = {
  .format = PA_SAMPLE_S16LE,
  .rate = 44100,
  .channels = 2
};

static pa_stream *istream = NULL,
                 *ostream = NULL;

// This is my builtin card. Use paman to find yours
//static char *device = "alsa_input.pci-0000_00_1b.0.analog-stereo";
static char *idevice = NULL;
static char *odevice = NULL;

static pa_stream_flags_t flags = 0;

static size_t latency = 0, process_time = 0;
static int32_t latency_msec = 1, process_time_msec = 0;

void stream_state_callback(pa_stream *s, void *userdata) {
  assert(s);

  switch (pa_stream_get_state(s)) {
  case PA_STREAM_CREATING:
    // The stream has been created, so
    // let's open a file to record to
    printf("Creating stream\n");
    // fdout = creat(fname,  0711);
    buffer = pa_xmalloc(BUFF_LEN);
    buffer_length = BUFF_LEN;
    buffer_index = 0;
    break;

  case PA_STREAM_TERMINATED:
    // close(fdout);
    break;

  case PA_STREAM_READY:

    // Just for info: no functionality in this branch
    if (verbose) {
      const pa_buffer_attr *a;
      char cmt[PA_CHANNEL_MAP_SNPRINT_MAX], sst[PA_SAMPLE_SPEC_SNPRINT_MAX];

      printf("Stream successfully created.");

      if (!(a = pa_stream_get_buffer_attr(s)))
        printf("pa_stream_get_buffer_attr() failed: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
      else {
        printf("Buffer metrics: maxlength=%u, fragsize=%u", a->maxlength, a->fragsize);

      }

      printf("Connected to device %s (%u, %ssuspended).",
             pa_stream_get_device_name(s),
             pa_stream_get_device_index(s),
             pa_stream_is_suspended(s) ? "" : "not ");
    }

    break;

  case PA_STREAM_FAILED:
  default:
    printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
    exit(1);
  }
}

/*********** Stream callbacks **************/

/* This is called whenever new data is available */
static void stream_read_callback(pa_stream *s, size_t length, void *userdata) {

  assert(s);
  assert(length > 0);

  // Copy the data from the server out to a file
  fprintf(stderr, "Can read %d\n", length);

  while (pa_stream_readable_size(s) > 0) {
    const void *data;
    size_t length, lout;

    // peek actually creates and fills the data vbl
    if (pa_stream_peek(s, &data, &length) < 0) {
      fprintf(stderr, "Read failed\n");
      exit(1);
      return;
    }

    fprintf(stderr, "read %d\n", length);
    lout =  pa_stream_writable_size(ostream);
    fprintf(stderr, "Writable: %d\n", lout);
    if (lout == 0) {
      fprintf(stderr, "can't write, zero writable\n");
      return;
    }
    if (lout < length) {
      fprintf(stderr, "Truncating read\n");
      length = lout;
  }

  if (pa_stream_write(ostream, (uint8_t*) data, length, NULL, 0, PA_SEEK_RELATIVE) < 0) {
    fprintf(stderr, "pa_stream_write() failed\n");
    exit(1);
    return;
  }

    // STICK OUR CODE HERE TO WRITE OUT
    //fprintf(stderr, "Writing %d\n", length);
    //write(fdout, data, length);

    // swallow the data peeked at before
    pa_stream_drop(s);
  }
}

/* This is called whenever new data may be written to the stream */
// We don't actually write anything this time
static void stream_write_callback(pa_stream *s, size_t length, void *userdata) {
  //assert(s);
  //assert(length > 0);

  printf("Stream write callback: Ready to write %d bytes\n", length);
 }

// This callback gets called when our context changes state.  We really only
// care about when it's ready or if it has failed
void state_cb(pa_context *c, void *userdata) {
  pa_context_state_t state;
  int *pa_ready = userdata;

  printf("State changed\n");
  state = pa_context_get_state(c);
  switch  (state) {
    // There are just here for reference
  case PA_CONTEXT_UNCONNECTED:
  case PA_CONTEXT_CONNECTING:
  case PA_CONTEXT_AUTHORIZING:
  case PA_CONTEXT_SETTING_NAME:
  default:
    break;
  case PA_CONTEXT_FAILED:
  case PA_CONTEXT_TERMINATED:
    *pa_ready = 2;
    break;
  case PA_CONTEXT_READY: {
    pa_buffer_attr buffer_attr;

    if (verbose)
      printf("Connection established.%s\n", CLEAR_LINE);

    if (!(istream = pa_stream_new(c, "JanCapture", &sample_spec, NULL))) {
      printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1);
    }

    if (!(ostream = pa_stream_new(c, "JanPlayback", &sample_spec, NULL))) {
      printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));

      exit(1);
    }

    // Watch for changes in the stream state to create the output file
    pa_stream_set_state_callback(istream, stream_state_callback, NULL);

    // Watch for changes in the stream's read state to write to the output file
    pa_stream_set_read_callback(istream, stream_read_callback, NULL);

    pa_stream_set_write_callback(ostream, stream_write_callback, NULL);

    // Set properties of the record buffer
    pa_zero(buffer_attr);
    buffer_attr.maxlength = (uint32_t) -1;
    buffer_attr.prebuf = (uint32_t) -1;

    if (latency_msec > 0) {
      buffer_attr.fragsize = buffer_attr.tlength = pa_usec_to_bytes(latency_msec * PA_USEC_PER_MSEC, &sample_spec);
      flags |= PA_STREAM_ADJUST_LATENCY;
    } else if (latency > 0) {
      buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) latency;
      flags |= PA_STREAM_ADJUST_LATENCY;
    } else
      buffer_attr.fragsize = buffer_attr.tlength = (uint32_t) -1;

    if (process_time_msec > 0) {
      buffer_attr.minreq = pa_usec_to_bytes(process_time_msec * PA_USEC_PER_MSEC, &sample_spec);
    } else if (process_time > 0)
      buffer_attr.minreq = (uint32_t) process_time;
    else
      buffer_attr.minreq = (uint32_t) -1;

    // and start recording
    if (pa_stream_connect_record(istream, idevice, &buffer_attr, flags) < 0) {
      printf("pa_stream_connect_record() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1);
    }

    if (pa_stream_connect_playback(ostream, odevice, &buffer_attr, flags,
                                   NULL,
                                   NULL) < 0) {
      printf("pa_stream_connect_playback() failed: %s", pa_strerror(pa_context_errno(c)));
      exit(1); //goto fail;
    } else {
      printf("Set playback callback\n");
    }

  }

    break;
  }
}

int main(int argc, char *argv[]) {

  // Define our pulse audio loop and connection variables
  pa_mainloop *pa_ml;
  pa_mainloop_api *pa_mlapi;

  pa_operation *pa_op;
  pa_context *pa_ctx;

  // Create a mainloop API and connection to the default server
  pa_ml = pa_mainloop_new();
  pa_mlapi = pa_mainloop_get_api(pa_ml);
  pa_ctx = pa_context_new(pa_mlapi, "test");

  // This function connects to the pulse server
  pa_context_connect(pa_ctx, NULL, 0, NULL);

  // This function defines a callback so the server will tell us its state.
  pa_context_set_state_callback(pa_ctx, state_cb, NULL);

  if (pa_mainloop_run(pa_ml, &ret) < 0) {
    printf("pa_mainloop_run() failed.");
    exit(1);
  }
}

当延迟设置为 1 毫秒时,实际延迟大约为 16 毫秒到 28 毫秒。我无法察觉。

设置设备的音量

每个设备都可以通过 PulseAudio 控制其输入或输出音量。被调用的主要接收器是pa_context_set_sink_volume_by_namepa_context_set_sink_volume_by_index,对源的调用类似。

这些调用利用了一个叫做pa_cvolume的结构。可以使用如下调用来操作此结构:

  • pa_cvolume_init
  • pa_cvolume_set
  • pa_cvolume_mute

在下面的程序中,您通过从stdin中读取整数值并使用这些值来设置特定设备上的音量。这种循环最好在 PulseAudio 框架的单独线程中进行。我没有在这里介绍应用线程,而是使用了另一组 PulseAudio 调用,为 PulseAudio 主循环设置了一个单独的线程。这些呼叫如下:

  • pa_threaded_mainloop而不是pa_mainloop
  • pa_threaded_mainloop_get_api而不是pa_mainloop_get_api
  • pa_threaded_mainloop_start而不是pa_mainloop_start

线程调用允许您在自己的线程中启动 PulseAudio,并让当前线程读取音量值。这给出了相对简单的程序pavolume.c

/**
 * pavolume.c
 * Jan Newmarch
 */

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define _(x) x

char *device = "alsa_output.pci-0000_00_1b.0.analog-stereo";

int ret;

pa_context *context;

void show_error(char *s) {
    fprintf(stderr, "%s\n", s);
}

void volume_cb(pa_context *c, int success, void *userdata) {
    if (success)
        printf("Volume set\n");
    else
        printf("Volume not set\n");
}

void context_state_cb(pa_context *c, void *userdata) {

    switch (pa_context_get_state(c)) {
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
        break;

    case PA_CONTEXT_READY: {
        pa_operation *o;

        break;
    }

    case PA_CONTEXT_FAILED:
    case PA_CONTEXT_TERMINATED:

    default:
        return;
    }
}

int main(int argc, char *argv[]) {
    long volume = 0;
    char buf[128];
    struct pa_cvolume v;

    // Define our pulse audio loop and connection variables
    pa_threaded_mainloop *pa_ml;
    pa_mainloop_api *pa_mlapi;

    // Create a mainloop API and connection to the default server
    //pa_ml = pa_mainloop_new();
    pa_ml = pa_threaded_mainloop_new();
    pa_mlapi = pa_threaded_mainloop_get_api(pa_ml);
    context = pa_context_new(pa_mlapi, "Voulme control");

    // This function connects to the pulse server
    pa_context_connect(context, NULL, 0, NULL);

    // This function defines a callback so the server will tell us its state.
    pa_context_set_state_callback(context, context_state_cb, NULL);

    pa_threaded_mainloop_start(pa_ml);
    printf("Enter volume for device %s\n");

    pa_cvolume_init(&v);
    while (1) {
        puts("Enter an integer 0-65536\n");
        fgets(buf, 128, stdin);
        volume = atoi(buf);
        pa_cvolume_set(&v, 2, volume);

        pa_context_set_sink_volume_by_name(context,
                                           device,
                                           &v,
                                           volume_cb,
                                           NULL
                                           );
    }
}

列出客户

PulseAudio 是一个服务器,它在底层与设备对话,在顶层与客户端对话。客户是音频的生产者和消费者。PulseAudio 的作用之一是将来自不同源客户端的信号混合到共享输出设备。为此,PulseAudio 跟踪客户端的注册,并通过适当的回调将这些注册提供给其他客户端。

程序palist_clients.c类似于程序palist_devices.c。主要区别在于,当上下文将状态更改为PA_CONTEXT_READY时,应用订阅PA_SUBSCRIPTION_MASK_CLIENT而不是(PA_SUBSCRIPTION_MASK_SINK|PA_SUBSCRIPTION_MASK_SOURCE),订阅回调请求pa_context_get_client_info而不是pa_context_get_source_info

程序palist_clients.c如下:

/**
 * palist_clients.c
 * Jan Newmarch
 */

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define CLEAR_LINE "\n"
#define _(x) x

// From pulsecore/macro.h
//#define pa_memzero(x,l) (memset((x), 0, (l)))
//#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))

int ret;

pa_context *context;

void show_error(char *s) {
    /* stub */
}

void print_properties(pa_proplist *props) {
    void *state = NULL;

    printf("  Properties are: \n");
    while (1) {
        char *key;
        if ((key = pa_proplist_iterate(props, &state)) == NULL) {
            return;
        }
        char *value = pa_proplist_gets(props, key);
        printf("   key %s, value %s\n", key, value);
    }
}

void add_client_cb(pa_context *context, const pa_client_info *i, int eol, void *userdata) {

    if (eol < 0) {
        if (pa_context_errno(context) == PA_ERR_NOENTITY)
            return;

        show_error(_("Client callback failure"));
        return;
    }

    if (eol > 0) {
        return;
    }

    printf("Found a new client index %d name %s eol %d\n", i->index, i->name, eol);

    print_properties(i->proplist);
}

void remove_client_cb(pa_context *context, const pa_client_info *i, int eol, void *userdata) {

    if (eol < 0) {
        if (pa_context_errno(context) == PA_ERR_NOENTITY)
            return;

        show_error(_("Client callback failure"));
        return;
    }

    if (eol > 0) {
        return;
    }

    printf("Removing a client index %d name %s\n", i->index, i->name);
    print_properties(i->proplist);
}

void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) {

    switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {

    case PA_SUBSCRIPTION_EVENT_CLIENT:
        if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
            printf("Remove event at index %d\n", index);
            pa_operation *o;
            if (!(o = pa_context_get_client_info(c, index, remove_client_cb, NULL))) {
                show_error(_("pa_context_get_client_info() failed"));
                return;
            }
            pa_operation_unref(o);

        } else {
            pa_operation *o;
            if (!(o = pa_context_get_client_info(c, index, add_client_cb, NULL))) {
                show_error(_("pa_context_get_client_info() failed"));
                return;
            }
            pa_operation_unref(o);
        }
        break;
    }
}

void context_state_cb(pa_context *c, void *userdata) {

    switch (pa_context_get_state(c)) {
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
        break;

    case PA_CONTEXT_READY: {

        pa_operation *o;

        pa_context_set_subscribe_callback(c, subscribe_cb, NULL);

        if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)
                                       (PA_SUBSCRIPTION_MASK_CLIENT), NULL, NULL))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        if (!(o = pa_context_get_client_info_list(context,
                                                  add_client_cb,
                                                  NULL
        ) )) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        break;
    }

    case PA_CONTEXT_FAILED:
        return;

    case PA_CONTEXT_TERMINATED:
    default:
        return;
    }
}

void stream_state_callback(pa_stream *s, void *userdata) {
    assert(s);

    switch (pa_stream_get_state(s)) {
    case PA_STREAM_CREATING:

        break;

    case PA_STREAM_TERMINATED:
        break;

    case PA_STREAM_READY:

        break;

    case PA_STREAM_FAILED:
    default:
        printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
        exit(1);
    }
}

int main(int argc, char *argv[]) {

    // Define our pulse audio loop and connection variables
    pa_mainloop *pa_ml;
    pa_mainloop_api *pa_mlapi;
    pa_operation *pa_op;
    pa_time_event *time_event;

    // Create a mainloop API and connection to the default server
    pa_ml = pa_mainloop_new();
    pa_mlapi = pa_mainloop_get_api(pa_ml);
    context = pa_context_new(pa_mlapi, "test");

    // This function connects to the pulse server
    pa_context_connect(context, NULL, 0, NULL);

    // This function defines a callback so the server will tell us its state.
    //pa_context_set_state_callback(context, state_cb, NULL);
    pa_context_set_state_callback(context, context_state_cb, NULL);

    if (pa_mainloop_run(pa_ml, &ret) < 0) {
        printf("pa_mainloop_run() failed.");
        exit(1);
    }
}

我的系统上的输出如下(省略):

Found a new client index 0 name ConsoleKit Session /org/freedesktop/ConsoleKit/Session2 eol 0
  Properties are:
   key application.name, value ConsoleKit Session /org/freedesktop/ConsoleKit/Session2
   key console-kit.session, value /org/freedesktop/ConsoleKit/Session2
Found a new client index 4 name XSMP Session on gnome-session as 1057eba7239ba1ec3d136359809598590100000018790044 eol 0
  Properties are:
   key application.name, value XSMP Session on gnome-session as 1057eba7239ba1ec3d136359809598590100000018790044
   key xsmp.vendor, value gnome-session

   key xsmp.client.id, value 1057eba7239ba1ec3d136359809598590100000018790044
Found a new client index 5 name GNOME Volume Control Media Keys eol 0
  Properties are:
   ...
Found a new client index 7 name GNOME Volume Control Applet eol 0
  Properties are:
   ...
Found a new client index 53 name Metacity eol 0
  Properties are:
  ...
Found a new client index 54 name Firefox eol 0
  Properties are:
    ...
Found a new client index 248 name PulseAudio Volume Control eol 0
  Properties are:
    ...
Found a new client index 341 name test eol 0
  Properties are:
    ...

列出客户端源和接收器

客户可以充当信息源;像 MPlayer 和 VLC 这样的程序就是这么做的,它们向 PulseAudio 发送流。其他客户端可以充当接收器。客户端本身由前面的程序监控。为了监控他们的活动,您将pa_subscribe_callback上的屏蔽设置为(PA_SUBSCRIPTION_MASK_CLIENT | PA_SUBSCRIPTION_MASK_SINK_INPUT | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)。在订阅回调中,调用PA_SUBSCRIPTION_EVENT_SINK_INPUT分支中的pa_context_get_sink_input_info,对源输出进行同样的操作。

向接收器输入回调传递结构pa_sink_input_info。这包含熟悉的nameindex字段,但也有一个名为client的整数字段。这将接收器输入链接回负责接收器的客户端的索引。在下面的程序中,您还列出了所有客户端,以便可以直观地跟踪这些链接。编程上,PulseAudio 让你自己保留很多信息(比如什么客户端有什么索引);这里忽略这个。

列出客户机并监视其输入和输出流变化的程序是pamonitor_clients.c

/**
 * pamonitor_clients.c
 * Jan Newmarch
 */

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define CLEAR_LINE "\n"
#define _(x) x

// From pulsecore/macro.h
#define pa_memzero(x,l) (memset((x), 0, (l)))
#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))

int ret;

pa_context *context;

void show_error(char *s) {
    /* stub */
}

void add_client_cb(pa_context *context, const pa_client_info *i, int eol, void *userdata)

{

    if (eol < 0) {
        if (pa_context_errno(context) == PA_ERR_NOENTITY)
            return;

        show_error(_("Client callback failure"));
        return;
    }

    if (eol > 0) {
        return;
    }

    printf("Found a new client index %d name %s eol %d\n", i->index, i->name, eol);
}

void remove_client_cb(pa_context *context, const pa_client_info *i, int eol, void *userdata) {

    if (eol < 0) {
        if (pa_context_errno(context) == PA_ERR_NOENTITY)
            return;

        show_error(_("Client callback failure"));
        return;
    }

    if (eol > 0) {
        return;
    }

    printf("Removing a client index %d name %s\n", i->index, i->name);
}

void sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata) {
    if (eol < 0) {
        if (pa_context_errno(context) == PA_ERR_NOENTITY)
            return;

        show_error(_("Sink input callback failure"));
        return;
    }

    if (eol > 0) {
        return;
    }
    printf("Sink input found index %d name %s for client %d\n", i->index, i->name, i->client);
}

void source_output_cb(pa_context *c, const pa_source_output_info *i, int eol, void *userdata) {
    if (eol < 0) {
        if (pa_context_errno(context) == PA_ERR_NOENTITY)
            return;

        show_error(_("Source output callback failure"));
        return;

    }

    if (eol > 0) {
        return;
    }
    printf("Source output found index %d name %s for client %d\n", i->index, i->name, i->client);
}

void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) {

    switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {

    case PA_SUBSCRIPTION_EVENT_CLIENT:
        if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
            printf("Remove event at index %d\n", index);
            pa_operation *o;
            if (!(o = pa_context_get_client_info(c, index, remove_client_cb, NULL))) {
                show_error(_("pa_context_get_client_info() failed"));
                return;
            }
            pa_operation_unref(o);

        } else {
            pa_operation *o;
            if (!(o = pa_context_get_client_info(c, index, add_client_cb, NULL))) {
                show_error(_("pa_context_get_client_info() failed"));
                return;

            }
            pa_operation_unref(o);
        }
        break;

    case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
        if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
            printf("Removing sink input %d\n", index);
        else {
            pa_operation *o;
            if (!(o = pa_context_get_sink_input_info(context, index, sink_input_cb, NULL))) {
                show_error(_("pa_context_get_sink_input_info() failed"));
                return;
            }
            pa_operation_unref(o);
        }
        break;

    case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
        if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
            printf("Removing source output %d\n", index);
        else {
            pa_operation *o;
            if (!(o = pa_context_get_source_output_info(context, index, source_output_cb, NULL))) {
                show_error(_("pa_context_get_sink_input_info() failed"));
                return;
            }
            pa_operation_unref(o);
        }
        break;
    }
}

void context_state_cb(pa_context *c, void *userdata) {

    switch (pa_context_get_state(c)) {
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
        break;

    case PA_CONTEXT_READY: {
        pa_operation *o;

        pa_context_set_subscribe_callback(c, subscribe_cb, NULL);

        if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)
                                       (PA_SUBSCRIPTION_MASK_CLIENT |
                                        PA_SUBSCRIPTION_MASK_SINK_INPUT |
                                        PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT), NULL, NULL))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        if (!(o = pa_context_get_client_info_list(context,
                                                  add_client_cb,
                                                  NULL
        ) )) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        pa_operation_unref(o);

        break;
    }

    case PA_CONTEXT_FAILED:
        return;

    case PA_CONTEXT_TERMINATED:
    default:
        // Gtk::Main::quit();
        return;
    }
}

void stream_state_callback(pa_stream *s, void *userdata) {
    assert(s);

    switch (pa_stream_get_state(s)) {
    case PA_STREAM_CREATING:

        break;

    case PA_STREAM_TERMINATED:
        break;

    case PA_STREAM_READY:
        break;

    case PA_STREAM_FAILED:
    default:
        printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
        exit(1);
    }
}

int main(int argc, char *argv[]) {

    // Define our pulse audio loop and connection variables
    pa_mainloop *pa_ml;
    pa_mainloop_api *pa_mlapi;
    pa_operation *pa_op;
    pa_time_event *time_event;

    // Create a mainloop API and connection to the default server
    pa_ml = pa_mainloop_new();
    pa_mlapi = pa_mainloop_get_api(pa_ml);
    context = pa_context_new(pa_mlapi, "test");

    // This function connects to the pulse server
    pa_context_connect(context, NULL, 0, NULL);

    // This function defines a callback so the server will tell us its state.
    //pa_context_set_state_callback(context, state_cb, NULL);
    pa_context_set_state_callback(context, context_state_cb, NULL);

    if (pa_mainloop_run(pa_ml, &ret) < 0) {
        printf("pa_mainloop_run() failed.");
        exit(1);
    }
}

我的系统上的输出如下:

Found a new client index 0 name ConsoleKit Session /org/freedesktop/ConsoleKit/Session2 eol 0
Found a new client index 4 name XSMP Session on gnome-session as 1057eba7239ba1ec3d136359809598590100000018790044 eol 0
Found a new client index 5 name GNOME Volume Control Media Keys eol 0
Found a new client index 7 name GNOME Volume Control Applet eol 0
Found a new client index 53 name Metacity eol 0
Found a new client index 54 name Firefox eol 0

Found a new client index 248 name PulseAudio Volume Control eol 0
Found a new client index 342 name test eol 0

控制接收客户端的音量

PulseAudio 的一个显著特点是,它不仅可以将流混合到一个设备中,还可以控制每个流的音量。这是对每个设备的音量控制的补充。在pavucontrol中,你可以在播放选项卡上看到这一点,在这里可以调整播放客户端的音量。

从程序上来说,这是通过调用带有参数的pa_context_set_sink_input_volume来完成的,这些参数是接收输入和音量的索引。在下面的程序中,我按照在pavolume_client.c程序中所做的那样,将 PulseAudio 设置为在单独的线程中运行,并在主线程中输入音量值。一个细微的区别是,你必须等待客户端启动接收输入,这是通过休眠来完成的,直到接收输入回调将一个非零值赋给sink_index变量。粗糙,是的。在像pavucontrol这样的程序中,GUI 无论如何都是在单独的线程中运行的,你不需要求助于这样简单的技巧。

节目是pavolume_sink.c。例如,如果你使用 MPlayer 播放一个文件,那么它的音量可以通过这个程序来调整。

/**
 * pavolume_sink.c
 * Jan Newmarch
 */

#include <stdio.h>
#include <string.h>
#include <pulse/pulseaudio.h>

#define CLEAR_LINE "\n"
#define _(x) x

int ret;

// sink we will control volume on when it is non-zero
int sink_index = 0;
int sink_num_channels;

pa_context *context;

void show_error(char *s) {
    /* stub */
}

void sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata) {
    if (eol < 0) {
        if (pa_context_errno(context) == PA_ERR_NOENTITY)
            return;

        show_error(_("Sink input callback failure"));
        return;
    }

    if (eol > 0) {
        return;
    }
    printf("Sink input found index %d name %s for client %d\n", i->index, i->name, i->client);
    sink_num_channels = i->channel_map.channels;
    sink_index = i->index;
}

void volume_cb(pa_context *c, int success, void *userdata) {
    if (success)
        printf("Volume set\n");
    else
        printf("Volume not set\n");
}

void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) {

    switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {

    case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
        if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
            printf("Removing sink input %d\n", index);
        else {
            pa_operation *o;
            if (!(o = pa_context_get_sink_input_info(context, index, sink_input_cb, NULL))) {
                show_error(_("pa_context_get_sink_input_info() failed"));
                return;
            }
            pa_operation_unref(o);
        }
        break;
    }
}

void context_state_cb(pa_context *c, void *userdata) {

    switch (pa_context_get_state(c)) {
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
        break;

    case PA_CONTEXT_READY: {
        pa_operation *o;

        pa_context_set_subscribe_callback(c, subscribe_cb, NULL);

        if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)
                                       (PA_SUBSCRIPTION_MASK_SINK_INPUT), NULL, NULL))) {
            show_error(_("pa_context_subscribe() failed"));
            return;
        }
        break;
    }

    case PA_CONTEXT_FAILED:
        return;

    case PA_CONTEXT_TERMINATED:
    default:
        // Gtk::Main::quit();
        return;
    }
}

void stream_state_callback(pa_stream *s, void *userdata) {
    assert(s);

    switch (pa_stream_get_state(s)) {
    case PA_STREAM_CREATING:
        break;

    case PA_STREAM_TERMINATED:
        break;

    case PA_STREAM_READY:
        break;

    case PA_STREAM_FAILED:
    default:
        printf("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
        exit(1);
    }
}

int main(int argc, char *argv[]) {

    // Define our pulse audio loop and connection variables
    pa_threaded_mainloop *pa_ml;
    pa_mainloop_api *pa_mlapi;
    pa_operation *pa_op;
    pa_time_event *time_event;
    long volume = 0;
    char buf[128];
    struct pa_cvolume v;

    // Create a mainloop API and connection to the default server
    pa_ml = pa_threaded_mainloop_new();
    pa_mlapi = pa_threaded_mainloop_get_api(pa_ml);
    context = pa_context_new(pa_mlapi, "test");

    // This function connects to the pulse server
    pa_context_connect(context, NULL, 0, NULL);

    // This function defines a callback so the server will tell us its state.
    //pa_context_set_state_callback(context, state_cb, NULL);
    pa_context_set_state_callback(context, context_state_cb, NULL);

    pa_threaded_mainloop_start(pa_ml);

    /* wait till there is a sink */
    while (sink_index == 0) {
        sleep(1);
    }

    printf("Enter volume for sink %d\n", sink_index);
    pa_cvolume_init(&v);

    while (1) {
        puts("Enter an integer 0-65536");
        fgets(buf, 128, stdin);
        volume = atoi(buf);
        pa_cvolume_set(&v, sink_num_channels, volume);
        pa_context_set_sink_input_volume(context,
                                           sink_index,
                                           &v,
                                           volume_cb,
                                           NULL
                                           );
    }
}

结论

本章介绍了 PulseAudio。这是目前消费者 Linux 的标准声音系统。有许多实用程序可用于探索 PulseAudio。有两种 API:简单 API 和异步 API。本章介绍了如何使用这些 API 来演奏和录音。还研究了脉搏音频的其他方面。

延迟不是目标,也不是为实时音频设计的。但是,您可以要求延迟较小,如果 PulseAudio 可以做到这一点,它将为您提供合理的性能。但是,PulseAudio 不保证延迟,因此如果最大延迟很关键,那么 PulseAudio 可能不合适。

PulseAudio 目前构建在 ALSA 之上,通常通过使自己成为默认的 ALSA 插件来进行交互。

七、Jack

在 Linux 中,声音服务器的作用是从多个源获取输入,并将它们路由到多个接收器。Linux 中有几种音频服务器,主要的是 PulseAudio 和 Jack。它们是为不同的角色设计的:PulseAudio 是为消费音频系统设计的,而 Jack 是为专业音频设计的。在 http://0pointer.de/blog/projects/when-pa-and-when-not.html 吟诗的伦纳德拟定了一张差异表。主要的一点是,Jack 适用于低延迟至关重要的环境,Jack 在音频链中引入的延迟不到 5 毫秒,而 PulseAudio 可引入高达 2 秒的延迟。其他的区别是 PulseAudio 可以运行在包括手机在内的低质量系统上,而 Jack 通常运行在高质量的音频设备上。文章“认识 Jack”对 Jack 进行了委婉的介绍。本章着眼于专门为 Jack 构建的工具,应用如何使用 Jack,最后是用 Jack 编程。

资源

以下是一些资源:

启动千斤顶

大多数发行版的软件仓库中都有 Jack。您希望安装 Jack2 而不是 Jack1。对于编程,您还需要 libjack2 dev 包,它可能与 jack2 包一起安装。

Jack 服务器是jackd。它有一个必需的参数,这是一个声音后端,如 ALSA。最小的命令如下:

jackd -dalsa

跟随选项-dalsa会出现 ALSA 选项。在我的一台电脑上,aplay -l显示卡 0 有设备 3、7 和 8,我需要指定其中一个:

jackd -dalsa -d hw:0,3

如果您使用的是普通的 Linux 发行版,比如 Fedora 或 Ubuntu,那么如果 PulseAudio 系统正在运行,这很可能会失败。在运行 Jack 时,这可能需要停止,或者至少暂停。停止 PulseAudio 参见上一章。要暂停它,我通常在终端窗口中运行:

pasuspender cat

这将暂停 PulseAudio,直到cat终止,当您输入 Ctrl-D 时就会终止。

jackd将尝试开始使用 Linux 实时调度程序。如果您想在没有它的情况下运行,请使用以下选项:

jackd --no-realtime -dalsa

如果你想用实时调度程序运行,有几种方法。

  • 从 root 用户运行服务器。

  • 将用户添加到audiojackuser组,如下所示:

sudo jackd -dalsa

useradd -G audio newmarch
useradd -G jackuser newmarch

(在此生效之前,您需要注销并重新登录。)

请注意,如果您以 root 用户身份运行服务器,那么您将无法从不在jackuser组中的客户端连接到它。

Jack 没有明显的 systemd 或 upstart 脚本,但是有在 http://gentoo-en.vfose.ru/wiki/JACK#Starting_JACK_at_boot_time 引导时启动 Jack 的指令。以下说明摘自 GPL 许可下的说明(最后一次修改是在 2012 年):

#!/sbin/runscript
 # This programm will be used by init in order to launch jackd with the privileges
 # and id of the user defined into /etc/conf.d/jackd

 depend() {
        need alsasound
 }

 start() {
        if ! test -f "${JACKDHOME}/.jackdrc"; then
                eerror "You must start and configure jackd before launch it. Sorry."
                eerror "You can use qjackctl for that."
                return 1
        else JACKDOPTS=$(cat "${JACKDHOME}/.jackdrc"|sed -e 's\/usr/bin/jackd \\')
        fi

        if [ -e /var/run/jackd.pid ]; then
                 rm /var/run/jackd.pid
        fi

        ebegin "Starting JACK Daemon"
        env HOME="${JACKDHOME}" start-stop-daemon --start \
                --quiet --background \
                --make-pidfile --pidfile /var/run/jackd.pid \
                -c ${JACKDUSER} \
                -x /usr/bin/jackd -- ${JACKDOPTS} >${LOG}

        sleep 2
        if ! pgrep -u ${JACKDUSER} jackd > /dev/null; then
                eerror "JACK daemon can't be started! Check logfile: ${LOG}"
        fi
        eend $?
 }

 stop() {
        ebegin "Stopping JACK daemon -- please wait"
        start-stop-daemon --stop --pidfile /var/run/jackd.pid &>/dev/null
        eend $?
 }

 restart() {
        svc_stop
        while `pgrep -u ${JACKDUSER} jackd >/dev/null`; do
                sleep 1
        done
        svc_start
 }

文件:/etc/conf.d/jackd:

 # owner of jackd process (Must be an existing user.)
 JACKDUSER="dom"

 # .jackdrc location for that user (Must be existing, JACKDUSER can use
 # qjackctl in order to create it.)
 JACKDHOME="/home/${JACKDUSER}"

 # logfile (/dev/null for nowhere)
 LOG=/var/log/jackd.log

创建并保存这两个文件。别忘了把JACKDUSER调整到想要的用户名(我猜和你的一样;[作者:是的,这就是 Gentoo 指令所说的!]).我们需要使/etc/init.d/jackd可执行:

# chmod +x /etc/init.d/jackd

将脚本添加到默认运行级别:

# rc-update add jackd default

在重启你的系统或者启动这个脚本之前,你必须确定jackd已经为$JACKUSER配置好了,否则jackd将会失败。这是因为脚本会读/ home/${USER}/.jackdrc。如果这个文件不存在,创建它的最简单的方法是运行 QJackCtl,如上所述。

关于实时的说明:由于 start-stop-daemon 实现的限制,如果使用pam_limits. start-stop-daemon没有实现对pam_sessions的支持,则不能通过这种方法以非根用户的身份在实时模式下启动jackd,这意味着对limits.conf的更改在这种情况下没有影响。

用户工具

Jack 只需要使用一个工具:qjackctl。这提供了正在播放的 Jack 应用的图形视图,并允许您链接输入和输出。

使用qjackctl的一个简单教程是 how to qjackctlconnections(https://help.ubuntu.com/community/HowToQjackCtlConnections)。它实际上使用起来非常简单:单击一个源,然后通过单击目的地将它链接到目的地。将显示一条连接它们的线。你要做的就是这些。很多 Jack 应用会帮你做这个,所以你只要观察结果就好了。这方面的说明将在本章后面给出。

使用插孔的应用

使用 JACK 的软件有很多,在“使用 Jack 的应用”( http://jackaudio.org/applications )中有描述。

mplayer

要使用千斤顶运行mplayer,添加选项-ao jack

mplayer -ao jack 54154.mp3

用这种方式将mplayer连接到插孔system输出装置。要输出到另一个插孔应用,如jack-rack,将输出应用附加到音频输出命令。

mplayer -ao jack:port=jack_rack 54154.mp3

可见光通讯

如果包含插孔模块( https://wiki.videolan.org/Documentation:Modules/jack/ ),VLC 将播放插孔输出。这是一个可下载的 Debian 包,名为vlc-plugin-jack。你可以通过查看jack是否在vlc --list显示 ALSA 而不是 Jack 中被列为一个模块来检查你是否有它。

通过执行以下操作,使用 Jack 播放文件:

vlc --aout jack 54154.mp3

您应该能够使用选项--jack-connect-regex <string>连接到特定的 Jack 应用。

TiMidity

TiMidity 是第二十一章中讨论的 MIDI 播放器。它可以用这个来播放插孔输出设备:

timidity -Oj 54154.mid

Jack 提供的程序

Jack 带来了大量的客户。

jack_alias                  jack_midisine
jack_bufsize                jack_monitor_client
jack_connect                jack_multiple_metro
jack_control                jack_net_master
jack_cpu                    jack_net_slave
jack_cpu_load               jack_netsource
jackd                       jack_rec
jackdbus                    jack_samplerate
jack_disconnect             jack_server_control
jack_evmon                  jack_session_notify
jack_freewheel              jack_showtime
jack_iodelay                jack_simple_client
jack_latent_client          jack_simple_session_client
jack_load                   jack_test
jack_lsp                    jack_thru
jack_metro                  jack_transport
jack_midi_dump              jack_unload
jack_midi_latency_test      jack_wait
jack_midiseq                jack_zombie

对于其中的许多,源代码可以在 Jack 源代码发行版中获得,并且每个都有一个man页面。

比方说,jack_thru运行将系统捕获端口连接到jack_thru输入端口,将jack_thru输出端口连接到系统回放端口。然后,您可以使用client:port作为端口名来断开端口连接,如下所示:

jack_disconnect jack_thru:output_1 system:playback_1

这些命令行工具允许您执行与qjackctl相同的操作。

其他 Jack 程序

使用插孔的应用页面( http://jackaudio.org/applications )列出了许多使用插孔的应用。

linuxaudio.org的页面 Jack MIDI Apps ( http://apps.linuxaudio.org/apps/categories/jack_midi )列出了很多使用 Jack 的 MIDI 应用。

使用不同的声卡

Jack 的默认 ALSA 设备将是hw:0。如果您想使用不同的声卡,那么您可以在启动 Jack 时指定它,如下所示:

jackd -dalsa -dhw:0

我有一个 USB 声霸卡,需要一些额外的参数。

jackd -dalsa -dhw:2 -r 48000 -S

这不是很好;我听到有规律的“滴答”声。

如果没有-S (16 位)标志,我只能得到这样一行代码:

ALSA: cannot set hardware parameters for playback

或者,我可以这样运行:

jackd -dalsa -dplughw:2 -r 48000

当我以这种方式开始时,Jack 建议不要使用 ALSA 插头设备,但它迄今为止工作得最好。

如何在 Jack 上使用多个声卡?

插孔用于专业音频使用。在这样的系统中,通常只有一个数字采样“时钟”在这个“理想”的插孔世界中,不会有多个独立的声卡,每个都有自己的时钟。我就来说说这个理想世界。如果您需要在有多个声卡的情况下运行 JACK,那么请参阅“我如何将多个声卡用于 Jack?”( http://jackaudio.org/multiple_devices )。

混合音频

如果来自两个不同源的两个输出端口连接到同一个输入端口,那么 Jack 将为您混合它们。这可以让你毫不费力地跟着你最喜欢的 MP3 文件一起唱。

  1. 将麦克风采集端口连接到回放端口。避免在笔记本电脑的麦克风和扬声器之间建立反馈回路,例如,插入耳机。

  2. 启动一个播放器,比如mplayer,它也将连接到播放端口,如下所示:

    mplayer -ao jack <MP3 file >
    
    
  3. 开始唱歌。

当然,每个信号源都没有音量控制。你可以在你的发行版中插入一个混音器,比如 jack_mixer ( http://home.gna.org/jackmixer/ ),然后用它来控制每个源的音量,如图 7-1 中的qjackctl屏幕所示。

A435426_1_En_7_Fig1_HTML.jpg

图 7-1。

qjackctl showing a mixer of mplayer and system

用 Jack 编写音频应用

插孔的设计在其主要作者保罗·戴维斯的插孔音频连接套件( http://lac.linuxaudio.org/2003/zkm/slides/paul_davis-jack/title.html )中进行了讨论。目标如下:

  • Jack 应该允许独立应用之间的低延迟、高带宽数据流。
  • 虽然不是必需的,但 Jack 应该支持任何流数据类型,而不仅仅是音频。
  • 在 active Jack 设置中,将有一台服务器和一个或多个 Jack 插件。可以运行多个 Jack 服务器,但每个服务器将形成一个独立的 Jack 设置。Jack 不会在 Jack 服务器之间定义任何接口。
  • 使用 Jack 连接的应用可能有自己的图形界面。Jack 不会对不同的 GUI 工具包或库做任何说明。由于这一要求,下入式千斤顶设置的不同部分可能分布在多个过程中。
  • Jack 应该提供完全的、样本精确的同步(换句话说,所有客户机插件的完全同步执行)。
  • 为了表示音频数据,Jack 应该使用 32 位 IEEE 浮点数,标准化为值域[-1,1]。
  • 仅支持非交错音频流。
  • 一个 Jack 客户端可以消耗或产生多个数据流。
  • Jack API 应该在 ANSI C 中指定,对于如何实现服务器和客户机没有限制。
  • 应该可以连接已经运行的应用。
  • 应该可以在服务器运行时添加或删除 Jack 客户端。

从这个角度来看,主要目标如下:

  • Jack 应该允许独立应用之间的低延迟、高带宽数据流。
  • Jack 应该提供完全的、样本精确的同步(换句话说,所有客户机插件的完全同步执行)。

第二个由 Jack 框架保证。第一个是由 Jack 框架提供的,只要应用编码正确。

在幕后,Jack 使用快速 Linux (Unix)管道将数据从一个应用传输到另一个应用。每个 Jack 应用中都有一个实时循环,它从输入管道获取数据,并将数据发送到输出管道。为了避免延迟,在读取和写入数据之间应该基本上没有(或者尽可能少)处理;理想的情况是将指针数据从输入传递到输出,或者最多只是做一个memcpy

那么,如何进行加工呢?将读取的数据复制到另一个数据结构并将处理传递给另一个线程,或者将在另一个线程中处理的数据复制到输出管道。任何其他因素都会导致延迟,这可能会变得很明显。特别是某些系统调用本质上是被禁止的:malloc可以引起交换;sleep是明显的大忌;read/write等等,都会引起磁盘 I/O;而pthread_cond_wait会……等。

Jack 应用本质上是多线程的。在 Linux 世界中,这意味着 Posix 线程,幸运的是有 Bil Lewis 和 Daniel J. Berg 的书《PThreads Primer ( http://www8.cs.umu.se/kurser/TDBC64/VT03/pthreads/pthread-primer.pdf )告诉你关于 Posix 线程的一切!

以下是设置 Jack 应用的机制:

  1. 打开与 Jack 服务器的连接:jack_client_open
  2. 如果需要,检查连接和紧急援助的状态。
  3. 安装一个进程回调处理器来管理 I/O: jack_set_process_callback
  4. 安装一个关机回调:jack_on_shutdown
  5. 向 Jack 服务器注册输入和输出端口:jack_port_register。请注意,每个端口只携带一个单声道通道,因此对于立体声,您将获得两个输入端口。这还没有将它们与管道联系起来。
  6. 激活端口。换句话说,告诉 Jack 启动它的处理线程:jack_activate
  7. 将端口连接到管道:jack_connect
  8. 以某种方式坐在那里。对于文本客户端,只需循环休眠即可。GUI 客户端可能有一个 GUI 处理循环。

收集

下面的例子需要链接到各种库。这些是 jack、sndfile、pthread 和 math 库。适当的标志如下:

INCLUDES = $(shell pkg-config --cflags jack sndfile)
LDLIBS =  $(shell pkg-config --libs jack sndfile) -lpthread -lm

港口信息

Jack 使用传输单声道 32 位数据的端口。每个端口都有一个字符串形式的名称和属性,如输入和输出。一旦连接到 Jack 服务器,就可以使用jack_get_ports查询服务器已知的端口。如果参数是NULL或零,那么返回所有端口,或者可以使用模式来限制返回的端口名称。一旦找到一个端口名,就可以把它变成一个jack_port_t,并且可以查询它的属性。

完成这项工作的程序是listports.c,如下所示:

/** @file listports.c
 *
 * @brief This client delays one channel by 4096 framse.
 */

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <signal.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include <jack/jack.h>

jack_client_t *client;

void print_port_info(char *name) {
    printf("Port name is %s\n", name);
    jack_port_t *port = jack_port_by_name (client, name);
    if (port == NULL) {
        printf("No port by name %s\n", name);
        return;
    }
    printf("  Type is %s\n", jack_port_type(port));

    int flags = jack_port_flags(port);
    if (flags & JackPortIsInput)
        printf("  Is an input port\n");
    else
        printf("  Is an output port\n");
    char **connections = jack_port_get_connections(port);
    char **c = connections;
    printf("  Connected to:\n");
    while ((c != NULL) && (*c != NULL)) {
        printf("    %s\n", *c++);
    }
    if (connections != NULL)
        jack_free(connections);
}

int
main ( int argc, char *argv[] )
{
    int i;
    const char **ports;
    const char *client_name;
    const char *server_name = NULL;
    jack_options_t options = JackNullOption;
    jack_status_t status;

    if ( argc >= 2 )        /* client name specified? */
    {
        client_name = argv[1];
        if ( argc >= 3 )    /* server name specified? */
        {
            server_name = argv[2];
            options |= JackServerName;
        }
    }
    else              /* use basename of argv[0] */
    {
        client_name = strrchr ( argv[0], '/' );
        if ( client_name == 0 )
        {
            client_name = argv[0];
        }
        else
        {
            client_name++;
        }
    }

    /* open a client connection to the JACK server */

    client = jack_client_open ( client_name, options, &status, server_name );
    if ( client == NULL )
    {
        fprintf ( stderr, "jack_client_open() failed, "
                  "status = 0x%2.0x\n", status );
        if ( status & JackServerFailed )
        {
            fprintf ( stderr, "Unable to connect to JACK server\n" );
        }
        exit ( 1 );
    }
    if ( status & JackServerStarted )
    {
        fprintf ( stderr, "JACK server started\n" );
    }
    if ( status & JackNameNotUnique )
    {
        client_name = jack_get_client_name ( client );
        fprintf ( stderr, "unique name `%s' assigned\n", client_name );
    }

    if ( jack_activate ( client ) )
    {
        fprintf ( stderr, "cannot activate client" );
        exit ( 1 );
    }

     ports = jack_get_ports ( client, NULL, NULL, 0 );
    if ( ports == NULL )
    {
        fprintf ( stderr, "no ports\n" );
        exit ( 1 );
    }
    char **p = ports;
    while (*p != NULL)
        print_port_info(*p++);
    jack_free(ports);

    jack_client_close ( client );
    exit ( 0 );
}

将输入复制到输出

Jack 源代码发行版有一个example clients子目录。这个子目录中包含客户端thru_client.c,它只是将输入复制到输出。这个例子的处理核心是函数process。该函数将输入和输出上可用的若干帧作为参数,并且该函数循环通过(立体声)通道,获得相应的输入和输出缓冲器(用于输入和输出管道),并且将数据从输入复制到相应的输出。

代码如下:

/** @file thru_client.c
 *
 * @brief This simple through client demonstrates the basic features of JACK
 * as they would be used by many applications.
 */

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <signal.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include <jack/jack.h>

jack_port_t **input_ports;
jack_port_t **output_ports;
jack_client_t *client;

static void signal_handler ( int sig )
{
    jack_client_close ( client );
    fprintf ( stderr, "signal received, exiting ...\n" );
    exit ( 0 );
}

/**
 * The process callback for this JACK application is called in a
 * special realtime thread once for each audio cycle.
 *
 * This client follows a simple rule: when the JACK transport is
 * running, copy the input port to the output.  When it stops, exit.
 */

int
process ( jack_nframes_t nframes, void *arg )
{
    int i;
    jack_default_audio_sample_t *in, *out;
    for ( i = 0; i < 2; i++ )
    {
        in = jack_port_get_buffer ( input_ports[i], nframes );
        out = jack_port_get_buffer ( output_ports[i], nframes );
        memcpy ( out, in, nframes * sizeof ( jack_default_audio_sample_t ) );
    }
    return 0;
}

/**
 * JACK calls this shutdown_callback if the server ever shuts down or
 * decides to disconnect the client.
 */
void
jack_shutdown ( void *arg )
{
    free ( input_ports );
    free ( output_ports );
    exit ( 1 );
}

int
main ( int argc, char *argv[] )
{
    int i;
    const char **ports;
    const char *client_name;
    const char *server_name = NULL;
    jack_options_t options = JackNullOption;
    jack_status_t status;

    if ( argc >= 2 )        /* client name specified? */
    {
        client_name = argv[1];
        if ( argc >= 3 )    /* server name specified? */
        {
            server_name = argv[2];
            options |= JackServerName;
        }
    }
    else              /* use basename of argv[0] */
    {
        client_name = strrchr ( argv[0], '/' );
        if ( client_name == 0 )
        {
            client_name = argv[0];
        }
        else
        {
            client_name++;
        }
    }

    /* open a client connection to the JACK server */

    client = jack_client_open ( client_name, options, &status, server_name );
    if ( client == NULL )
    {
        fprintf ( stderr, "jack_client_open() failed, "
                  "status = 0x%2.0x\n", status );
        if ( status & JackServerFailed )
        {
            fprintf ( stderr, "Unable to connect to JACK server\n" );
        }
        exit ( 1 );
    }
    if ( status & JackServerStarted )
    {
        fprintf ( stderr, "JACK server started\n" );
    }
    if ( status & JackNameNotUnique )
    {
        client_name = jack_get_client_name ( client );
        fprintf ( stderr, "unique name `%s' assigned\n", client_name );
    }

    /* tell the JACK server to call `process()' whenever
       there is work to be done.
    */

    jack_set_process_callback ( client, process, 0 );

    /* tell the JACK server to call `jack_shutdown()' if
       it ever shuts down, either entirely, or if it
       just decides to stop calling us.
    */

    jack_on_shutdown ( client, jack_shutdown, 0 );

    /* create two ports pairs*/
    input_ports = ( jack_port_t** ) calloc ( 2, sizeof ( jack_port_t* ) );
    output_ports = ( jack_port_t** ) calloc ( 2, sizeof ( jack_port_t* ) );

    char port_name[16];
    for ( i = 0; i < 2; i++ )
    {
        sprintf ( port_name, "input_%d", i + 1 );
        input_ports[i] = jack_port_register ( client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0 );
        sprintf ( port_name, "output_%d", i + 1 );
        output_ports[i] = jack_port_register ( client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0 );
        if ( ( input_ports[i] == NULL ) || ( output_ports[i] == NULL ) )
        {
            fprintf ( stderr, "no more JACK ports available\n" );
            exit ( 1 );
        }
    }

    /* Tell the JACK server that we are ready to roll.  Our
     * process() callback will start running now. */

    if ( jack_activate ( client ) )
    {
        fprintf ( stderr, "cannot activate client" );
        exit ( 1 );
    }

    /* Connect the ports.  You can't do this before the client is
     * activated, because we can't make connections to clients
     * that aren't running.  Note the confusing (but necessary)
     * orientation of the driver backend ports: playback ports are
     * "input" to the backend, and capture ports are "output" from
     * it.
     */

    ports = jack_get_ports ( client, NULL, NULL, JackPortIsPhysical|JackPortIsOutput );
    if ( ports == NULL )
    {
        fprintf ( stderr, "no physical capture ports\n" );
        exit ( 1 );
    }

    for ( i = 0; i < 2; i++ )
        if ( jack_connect ( client, ports[i], jack_port_name ( input_ports[i] ) ) )
            fprintf ( stderr, "cannot connect input ports\n" );

    free ( ports );

    ports = jack_get_ports ( client, NULL, NULL, JackPortIsPhysical|JackPortIsInput );
    if ( ports == NULL )
    {
        fprintf ( stderr, "no physical playback ports\n" );
        exit ( 1 );
    }

    for ( i = 0; i < 2; i++ )
        if ( jack_connect ( client, jack_port_name ( output_ports[i] ), ports[i] ) )
            fprintf ( stderr, "cannot connect input ports\n" );

    free ( ports );

    /* install a signal handler to properly quits jack client */
#ifdef WIN32
    signal ( SIGINT, signal_handler );
    signal ( SIGABRT, signal_handler );
    signal ( SIGTERM, signal_handler );
#else
    signal ( SIGQUIT, signal_handler );
    signal ( SIGTERM, signal_handler );
    signal ( SIGHUP, signal_handler );
    signal ( SIGINT, signal_handler );
#endif

    /* keep running until the transport stops */

    while (1)
    {
#ifdef WIN32
        Sleep ( 1000 );
#else
        sleep ( 1 );
#endif
    }

    jack_client_close ( client );
    exit ( 0 );
}

延迟音频

虽然这本书不是关于音频效果的,但是你可以通过延迟声音很容易地引入一种效果——延迟。现在这—以及任何耗时的行动—都违背了精神(和实现!)的 Jack,所以只有配合 Jack 模型才能做到。

最简单的想法就是在正确的地方插入sleep命令。这将假设对process回调的调用是异步发生的,但事实并非如此——它们是在 Jack 处理线程中同步发生的。耗费时间的活动是不允许的。如果你尝试它,你将会在最好的情况下结束大量的 xruns,在最坏的情况下结束 Jack 的癫痫发作。

在这种情况下,解决方案很简单:保留一个保存以前输入的缓冲区,并在请求输出时从该缓冲区中读取旧的条目。一个“足够大”的回绕数组可以做到这一点,旧的条目被读出,新的条目被读入。

以下程序delay.c将实时复制左声道,但将左声道延迟 4,096 个样本:

/** @file delay.c
 *
 * @brief This client delays one channel by 4096 framse.
 */

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <signal.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include <jack/jack.h>

jack_port_t **input_ports;
jack_port_t **output_ports;
jack_client_t *client;

#define SIZE 8192
#define DELAY 4096
jack_default_audio_sample_t buffer[SIZE];
int idx, delay_idx;

static void signal_handler ( int sig )
{
    jack_client_close ( client );
    fprintf ( stderr, "signal received, exiting ...\n" );
    exit ( 0 );
}

static void copy2out( jack_default_audio_sample_t *out,
                      jack_nframes_t nframes) {
    if (delay_idx + nframes < SIZE) {
        memcpy(out, buffer + delay_idx,
               nframes * sizeof ( jack_default_audio_sample_t ) );
    } else {
        int frames_to_end = SIZE - delay_idx;
        int overflow = delay_idx + nframes - SIZE;
        memcpy(out, buffer + delay_idx,
               frames_to_end * sizeof ( jack_default_audio_sample_t ) );
        memcpy(out, buffer, overflow * sizeof(jack_default_audio_sample_t));
    }
    delay_idx = (delay_idx + nframes) % SIZE;
}

static void copy2buffer( jack_default_audio_sample_t *in,
                      jack_nframes_t nframes) {
    if (idx + nframes < SIZE) {
        memcpy(buffer + idx, in,
               nframes * sizeof ( jack_default_audio_sample_t ) );
    } else {
        int frames_to_end = SIZE - idx;
        int overflow = idx + nframes - SIZE;
        memcpy(buffer + idx, in,
               frames_to_end * sizeof ( jack_default_audio_sample_t ) );
        memcpy(buffer, in, overflow * sizeof(jack_default_audio_sample_t));
    }
    idx = (idx + nframes) % SIZE;
}

/**
 * The process callback for this JACK application is called in a
 * special realtime thread once for each audio cycle.
 *
 * This client follows a simple rule: when the JACK transport is
 * running, copy the input port to the output.  When it stops, exit.
 */

int
process ( jack_nframes_t nframes, void *arg )
{
    int i;
    jack_default_audio_sample_t *in, *out;

    in = jack_port_get_buffer ( input_ports[0], nframes );
    out = jack_port_get_buffer ( output_ports[0], nframes );
    memcpy ( out, in, nframes * sizeof ( jack_default_audio_sample_t ) );

    in = jack_port_get_buffer ( input_ports[1], nframes );
    out = jack_port_get_buffer ( output_ports[1], nframes );
    copy2out(out, nframes);
    copy2buffer(in, nframes);

    return 0;
}

/**
 * JACK calls this shutdown_callback if the server ever shuts down or
 * decides to disconnect the client.
 */
void
jack_shutdown ( void *arg )
{
    free ( input_ports );
    free ( output_ports );
    exit ( 1 );
}

int
main ( int argc, char *argv[] )
{
    int i;
    const char **ports;
    const char *client_name;
    const char *server_name = NULL;
    jack_options_t options = JackNullOption;
    jack_status_t status;

    if ( argc >= 2 )        /* client name specified? */
    {
        client_name = argv[1];
        if ( argc >= 3 )    /* server name specified? */
        {
            server_name = argv[2];
            options |= JackServerName;
        }
    }
    else              /* use basename of argv[0] */
    {
        client_name = strrchr ( argv[0], '/' );
        if ( client_name == 0 )
        {
            client_name = argv[0];
        }
        else
        {
            client_name++;
        }
    }

    /* open a client connection to the JACK server */

    client = jack_client_open ( client_name, options, &status, server_name );
    if ( client == NULL )
    {
        fprintf ( stderr, "jack_client_open() failed, "
                  "status = 0x%2.0x\n", status );
        if ( status & JackServerFailed )
        {
            fprintf ( stderr, "Unable to connect to JACK server\n" );
        }
        exit ( 1 );
    }
    if ( status & JackServerStarted )
    {
        fprintf ( stderr, "JACK server started\n" );
    }
    if ( status & JackNameNotUnique )
    {
        client_name = jack_get_client_name ( client );
        fprintf ( stderr, "unique name `%s' assigned\n", client_name );
    }

    /* tell the JACK server to call `process()' whenever
       there is work to be done.
    */

    jack_set_process_callback ( client, process, 0 );

    /* tell the JACK server to call `jack_shutdown()' if
       it ever shuts down, either entirely, or if it
       just decides to stop calling us.
    */

    jack_on_shutdown ( client, jack_shutdown, 0 );

    /* create two ports pairs*/
    input_ports = ( jack_port_t** ) calloc ( 2, sizeof ( jack_port_t* ) );
    output_ports = ( jack_port_t** ) calloc ( 2, sizeof ( jack_port_t* ) );

    char port_name[16];
    for ( i = 0; i < 2; i++ )
    {
        sprintf ( port_name, "input_%d", i + 1 );
        input_ports[i] = jack_port_register ( client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0 );
        sprintf ( port_name, "output_%d", i + 1 );
        output_ports[i] = jack_port_register ( client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0 );
        if ( ( input_ports[i] == NULL ) || ( output_ports[i] == NULL ) )
        {
            fprintf ( stderr, "no more JACK ports available\n" );
            exit ( 1 );
        }
    }

    bzero(buffer, SIZE * sizeof ( jack_default_audio_sample_t ));
    delay_idx = 0;
    idx = DELAY;

    /* Tell the JACK server that we are ready to roll.  Our
     * process() callback will start running now. */

    if ( jack_activate ( client ) )
    {
        fprintf ( stderr, "cannot activate client" );
        exit ( 1 );
    }

    /* Connect the ports.  You can't do this before the client is
     * activated, because we can't make connections to clients
     * that aren't running.  Note the confusing (but necessary)
     * orientation of the driver backend ports: playback ports are
     * "input" to the backend, and capture ports are "output" from
     * it.
     */

    ports = jack_get_ports ( client, NULL, NULL, JackPortIsPhysical|JackPortIsOutput );
    if ( ports == NULL )
    {
        fprintf ( stderr, "no physical capture ports\n" );
        exit ( 1 );
    }

    for ( i = 0; i < 2; i++ )
        if ( jack_connect ( client, ports[i], jack_port_name ( input_ports[i] ) ) )
            fprintf ( stderr, "cannot connect input ports\n" );

    free ( ports );

    ports = jack_get_ports ( client, NULL, NULL, JackPortIsPhysical|JackPortIsInput );
    if ( ports == NULL )
    {
        fprintf ( stderr, "no physical playback ports\n" );
        exit ( 1 );
    }

    for ( i = 0; i < 2; i++ )
        if ( jack_connect ( client, jack_port_name ( output_ports[i] ), ports[i] ) )
            fprintf ( stderr, "cannot connect input ports\n" );

    free ( ports );

    /* install a signal handler
to properly quits jack client */
#ifdef WIN32
    signal ( SIGINT, signal_handler );
    signal ( SIGABRT, signal_handler );
    signal ( SIGTERM, signal_handler );
#else
    signal ( SIGQUIT, signal_handler );
    signal ( SIGTERM, signal_handler );
    signal ( SIGHUP, signal_handler );
    signal ( SIGINT, signal_handler );
#endif

    /* keep running until the transport stops */

    while (1)
    {
#ifdef WIN32
        Sleep ( 1000 );
#else
        sleep ( 1 );
#endif
    }

    jack_client_close ( client );
    exit ( 0 );
}

勇敢面对 Jack

Audacity 是 Jack-aware。您可以使用它来捕获和显示 Jack 流。但这并不意味着对用户来说,它的播放方式很好!对于正在运行的 Jack 系统,启动 Audacity 会向 Jack 注册它,但是没有输入或输出端口。只有当您使用 Audacity 开始记录会话时,这些才会显示出来。然后它在 Jack 内部建立自己的链接。

例如,thru_client作为 Jack 中唯一的客户端,qjackctl显示连接,如图 7-2 所示。

A435426_1_En_7_Fig2_HTML.jpg

图 7-2。

Qjackctl showing thru_client

在该图中,捕捉设备连接到thru_client输入,而thru_client输出连接到回放输出。

仅仅启动 Audacity 而不记录任何东西并不会对这个连接图产生任何变化。

但是当 Audacity 在thru_client已经运行的情况下开始记录时,qjackctl显示已经建立的链接,如图 7-3 所示。

A435426_1_En_7_Fig3_HTML.jpg

图 7-3。

Qjackctl with thru_client and Audacity

这要复杂得多:Audacity 显示为 PortAudio 设备,捕获设备链接到 PortAudio 输入,PortAudio 输出链接到回放设备。现有的thru_client环节基本废弃。要设置您想要的情况,这些必须根据需要重新链接。

为了演示延迟一个通道的效果,启动 Jack,启动delay,然后启动 Audacity。根据图 7-4 重新连接端口。

A435426_1_En_7_Fig4_HTML.jpg

图 7-4。

Qjackctl wih delay

也就是说,捕捉端口链接到delay输入端口,delay输出端口链接到 PortAudio (Audacity)输入端口,PortAudio 输出端口链接到回放端口。

Audacity 捕获的波形清楚地显示了左声道相对于右声道的延迟(图 7-5 )。

A435426_1_En_7_Fig5_HTML.jpg

图 7-5。

Audacity showing delay

演奏正弦波

复制示例没有显示缓冲区中内容的细节:内容来自jack_default_audio_sample_t。宏JACK_DEFAULT_AUDIO_TYPE中描述了这些内容,默认值为“32 位浮点单声道音频”

除了简单地传递音频之外,您还需要处理这种格式的数据。示例程序simple_client.c用 32 位浮点正弦曲线值填充数组。在每次调用process时,它将数据从正弦曲线数组复制到输出缓冲区。对于左声道和右声道,正弦曲线阵列的增量是不同的,以在每个声道上给出不同的音符。

注意,正弦曲线数组的计算不是在process函数中完成的。那太慢了,会造成延迟。

该计划如下:

/** @file simple_client.c
 *
 * @brief This simple client demonstrates the basic features of JACK
 * as they would be used by many applications.
 */

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <signal.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include <jack/jack.h>

jack_port_t *output_port1, *output_port2;
jack_client_t *client;

#ifndef M_PI
#define M_PI  (3.14159265)
#endif

#define TABLE_SIZE   (200)
typedef struct
{
    float sine[TABLE_SIZE];
    int left_phase;
    int right_phase;
}
paTestData;

static void signal_handler(int sig)
{
        jack_client_close(client);
        fprintf(stderr, "signal received, exiting ...\n");
        exit(0);
}

/**
 * The process callback for this JACK application is called in a
 * special realtime thread once for each audio cycle.
 *
 * This client follows a simple rule: when the JACK transport is
 * running, copy the input port to the output.  When it stops, exit.
 */

int
process (jack_nframes_t nframes, void *arg)
{
        jack_default_audio_sample_t *out1, *out2;
        paTestData *data = (paTestData*)arg;
        int i;

        out1 = (jack_default_audio_sample_t*)jack_port_get_buffer (output_port1, nframes);
        out2 = (jack_default_audio_sample_t*)jack_port_get_buffer (output_port2, nframes);

        for( i=0; i<nframes; i++ )
    {
        out1[i] = data->sine[data->left_phase];  /* left */
        out2[i] = data->sine[data->right_phase];  /* right */
        data->left_phase += 1;
        if( data->left_phase >= TABLE_SIZE ) data->left_phase -= TABLE_SIZE;
        data->right_phase += 3; /* higher pitch so we can distinguish left and right. */
        if( data->right_phase >= TABLE_SIZE ) data->right_phase -= TABLE_SIZE;
    }

        return 0;
}

/**
 * JACK calls this shutdown_callback if the server ever shuts down or
 * decides to disconnect the client.
 */
void
jack_shutdown (void *arg)
{
        exit (1);
}

int
main (int argc, char *argv[])
{
        const char **ports;
        const char *client_name;
        const char *server_name = NULL;
        jack_options_t options = JackNullOption;
        jack_status_t status;
        paTestData data;
        int i;

        if (argc >= 2) {                /* client name specified? */
                client_name = argv[1];
                if (argc >= 3) {        /* server name specified? */
                        server_name = argv[2];
            int my_option = JackNullOption | JackServerName;
                        options = (jack_options_t)my_option;
                }
        } else {                        /* use basename of argv[0] */
                client_name = strrchr(argv[0], '/');
                if (client_name == 0) {
                        client_name = argv[0];
                } else {
                        client_name++;
                }
        }

        for( i=0; i<TABLE_SIZE; i++ )
    {
        data.sine[i] = 0.2 * (float) sin( ((double)i/(double)TABLE_SIZE) * M_PI * 2\. );
    }
    data.left_phase = data.right_phase = 0;

        /* open a client connection to the JACK server */

        client = jack_client_open (client_name, options, &status, server_name);
        if (client == NULL) {
                fprintf (stderr, "jack_client_open() failed, "
                         "status = 0x%2.0x\n", status);
                if (status & JackServerFailed) {
                        fprintf (stderr, "Unable to connect to JACK server\n");
                }
                exit (1);
        }
        if (status & JackServerStarted) {
                fprintf (stderr, "JACK server started\n");
        }
        if (status & JackNameNotUnique) {
                client_name = jack_get_client_name(client);
                fprintf (stderr, "unique name `%s' assigned\n", client_name);
        }

        /* tell the JACK server to call `process()' whenever
           there is work to be done.
        */

        jack_set_process_callback (client, process, &data);

        /* tell the JACK server to call `jack_shutdown()' if
           it ever shuts down, either entirely, or if it
           just decides to stop calling us.
        */

        jack_on_shutdown (client, jack_shutdown, 0);

        /* create two ports */

        output_port1 = jack_port_register (client, "output1",
                                          JACK_DEFAULT_AUDIO_TYPE,
                                          JackPortIsOutput, 0);

        output_port2 = jack_port_register (client, "output2",
                                          JACK_DEFAULT_AUDIO_TYPE,
                                          JackPortIsOutput, 0);

        if ((output_port1 == NULL) || (output_port2 == NULL)) {
                fprintf(stderr, "no more JACK ports available\n");
                exit (1);
        }

        /* Tell the JACK server that we are ready to roll.  Our
         * process() callback will start running now. */

        if (jack_activate (client)) {
                fprintf (stderr, "cannot activate client");
                exit (1);
        }

        /* Connect the ports.  You can't do this before the client is
         * activated, because we can't make connections to clients
         * that aren't running.  Note the confusing (but necessary)
         * orientation of the driver backend ports: playback ports are
         * "input" to the backend, and capture ports are "output" from
         * it.
         */

        ports = jack_get_ports (client, NULL, NULL,
                                JackPortIsPhysical|JackPortIsInput);
        if (ports == NULL) {
                fprintf(stderr, "no physical playback ports\n");
                exit (1);
        }

        if (jack_connect (client, jack_port_name (output_port1), ports[0])) {
                fprintf (stderr, "cannot connect output ports\n");
        }

        if (jack_connect (client, jack_port_name (output_port2), ports[1])) {
                fprintf (stderr, "cannot connect output ports\n");
        }

        free (ports);

    /* install a signal handler to properly quits jack client */
#ifdef WIN32
        signal(SIGINT, signal_handler);
    signal(SIGABRT, signal_handler);
        signal(SIGTERM, signal_handler);
#else
        signal(SIGQUIT, signal_handler);
        signal(SIGTERM, signal_handler);
        signal(SIGHUP, signal_handler);
        signal(SIGINT, signal_handler);
#endif

        /* keep running until the Ctrl+C */

        while (1) {
        #ifdef WIN32
                Sleep(1000);
        #else
                sleep (1);
        #endif
        }

        jack_client_close (client);
        exit (0);
}

将输入保存到磁盘

磁盘 I/O 不能在 Jack 处理循环中执行;只是太慢了。将输入保存到文件需要使用单独的线程来管理磁盘 I/O,并在插孔和磁盘线程之间传递控制。

示例中的程序capture_client.c就是这样做的。

/*
    Copyright (C) 2001 Paul Davis
    Copyright (C) 2003 Jack O'Quin

    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., 675 Mass Ave, Cambridge, MA 02139, USA.

    * 2002/08/23 - modify for libsndfile 1.0.0 <andy@alsaplayer.org>
    * 2003/05/26 - use ringbuffers - joq
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sndfile.h>
#include <pthread.h>
#include <signal.h>
#include <getopt.h>
#include <jack/jack.h>
#include <jack/ringbuffer.h>

typedef struct _thread_info {
    pthread_t thread_id;
    SNDFILE *sf;
    jack_nframes_t duration;
    jack_nframes_t rb_size;
    jack_client_t *client;
    unsigned int channels;
    int bitdepth;
    char *path;
    volatile int can_capture;
    volatile int can_process;
    volatile int status;
} jack_thread_info_t;

/* JACK data */
unsigned int nports;
jack_port_t **ports;
jack_default_audio_sample_t **in;
jack_nframes_t nframes;
const size_t sample_size = sizeof(jack_default_audio_sample_t);

/* Synchronization between process thread and disk thread. */
#define DEFAULT_RB_SIZE 16384           /* ringbuffer size in frames */
jack_ringbuffer_t *rb;
pthread_mutex_t disk_thread_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  data_ready = PTHREAD_COND_INITIALIZER;
long overruns = 0;
jack_client_t *client;

static void signal_handler(int sig)
{
        jack_client_close(client);
        fprintf(stderr, "signal received, exiting ...\n");
        exit(0);
}

static void *
disk_thread (void *arg)
{
        jack_thread_info_t *info = (jack_thread_info_t *) arg;
        static jack_nframes_t total_captured = 0;
        jack_nframes_t samples_per_frame = info->channels;
        size_t bytes_per_frame = samples_per_frame * sample_size;
        void *framebuf = malloc (bytes_per_frame);

        pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
        pthread_mutex_lock (&disk_thread_lock);

        info->status = 0;

        while (1) {

                /* Write the data one frame at a time.  This is
                 * inefficient, but makes things simpler. */
                while (info->can_capture &&
                       (jack_ringbuffer_read_space (rb) >= bytes_per_frame)) {

                        jack_ringbuffer_read (rb, framebuf, bytes_per_frame);

                        if (sf_writef_float (info->sf, framebuf, 1) != 1) {
                                char errstr[256];
                                sf_error_str (0, errstr, sizeof (errstr) - 1);
                                fprintf (stderr,
                                         "cannot write sndfile (%s)\n",
                                         errstr);
                                info->status = EIO; /* write failed */
                                goto done;
                        }

                        if (++total_captured >= info->duration) {
                                printf ("disk thread finished\n");
                                goto done;
                        }
                }

                /* wait until process() signals more data */
                pthread_cond_wait (&data_ready, &disk_thread_lock);
        }

 done:
        pthread_mutex_unlock (&disk_thread_lock);
        free (framebuf);
        return 0;
}

static int
process (jack_nframes_t nframes, void *arg)
{
        int chn;
        size_t i;
        jack_thread_info_t *info = (jack_thread_info_t *) arg;

        /* Do nothing until we're ready to begin. */
        if ((!info->can_process) || (!info->can_capture))
                return 0;

        for (chn = 0; chn < nports; chn++)
                in[chn] = jack_port_get_buffer (ports[chn], nframes);

        /* Sndfile requires interleaved data.  It is simpler here to
         * just queue interleaved samples to a single ringbuffer. */
        for (i = 0; i < nframes; i++) {
                for (chn = 0; chn < nports; chn++) {
                        if (jack_ringbuffer_write (rb, (void *) (in[chn]+i),
                                              sample_size)
                            < sample_size)
                                overruns++;
                }
        }

        /* Tell the disk thread there is work to do.  If it is already
         * running, the lock will not be available.  We can't wait
         * here in the process() thread, but we don't need to signal
         * in that case, because the disk thread will read all the
         * data queued before waiting again. */
        if (pthread_mutex_trylock (&disk_thread_lock) == 0) {
            pthread_cond_signal (&data_ready);
            pthread_mutex_unlock (&disk_thread_lock);
        }

        return 0;
}

static void
jack_shutdown (void *arg)
{
        fprintf(stderr, "JACK shut down, exiting ...\n");
        exit(1);
}

static void
setup_disk_thread (jack_thread_info_t *info)
{
        SF_INFO sf_info;
        int short_mask;

        sf_info.samplerate = jack_get_sample_rate (info->client);
        sf_info.channels = info->channels;

        switch (info->bitdepth) {
                case 8: short_mask = SF_FORMAT_PCM_U8;
                        break;
                case 16: short_mask = SF_FORMAT_PCM_16;
                         break;
                case 24: short_mask = SF_FORMAT_PCM_24;
                         break;
                case 32: short_mask = SF_FORMAT_PCM_32;
                         break;
                default: short_mask = SF_FORMAT_PCM_16;
                         break;
        }
        sf_info.format = SF_FORMAT_WAV|short_mask;

        if ((info->sf = sf_open (info->path, SFM_WRITE, &sf_info)) == NULL) {
                char errstr[256];
                sf_error_str (0, errstr, sizeof (errstr) - 1);
                fprintf (stderr, "cannot open sndfile \"%s\" for output (%s)\n", info->path, errstr);
                jack_client_close (info->client);
                exit (1);
        }

        info->duration *= sf_info.samplerate;
        info->can_capture = 0;

        pthread_create (&info->thread_id, NULL, disk_thread, info);
}

static void
run_disk_thread (jack_thread_info_t *info)
{
        info->can_capture = 1;
        pthread_join (info->thread_id, NULL);
        sf_close (info->sf);
        if (overruns > 0) {
                fprintf (stderr,
                         "jackrec failed with %ld overruns.\n", overruns);
                fprintf (stderr, " try a bigger buffer than -B %"
                         PRIu32 ".\n", info->rb_size);
                info->status = EPIPE;
        }
}

static void
setup_ports (int sources, char *source_names[], jack_thread_info_t *info)
{
        unsigned int i;
        size_t in_size;

        /* Allocate data structures that depend on the number of ports. */
        nports = sources;
        ports = (jack_port_t **) malloc (sizeof (jack_port_t *) * nports);
        in_size =  nports * sizeof (jack_default_audio_sample_t *);
        in = (jack_default_audio_sample_t **) malloc (in_size);
        rb = jack_ringbuffer_create (nports * sample_size * info->rb_size);

        /* When JACK is running realtime, jack_activate() will have
         * called mlockall() to lock our pages into memory.  But, we
         * still need to touch any newly allocated pages before
         * process() starts using them.  Otherwise, a page fault could
         * create a delay that would force JACK to shut us down. */
        memset(in, 0, in_size);
        memset(rb->buf, 0, rb->size);

        for (i = 0; i < nports; i++) {
                char name[64];

                sprintf (name, "input%d", i+1);

                if ((ports[i] = jack_port_register (info->client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0)) == 0) {
                        fprintf (stderr, "cannot register input port \"%s\"!\n", name);
                        jack_client_close (info->client);
                        exit (1);
                }
        }

        for (i = 0; i < nports; i++) {
                if (jack_connect (info->client, source_names[i], jack_port_name (ports[i]))) {
                        fprintf (stderr, "cannot connect input port %s to %s\n", jack_port_name (ports[i]), source_names[i]);
                        jack_client_close (info->client);
                        exit (1);
                }
        }

        info->can_process = 1;          /* process() can start, now */
}

int
main (int argc, char *argv[])
{
    jack_thread_info_t thread_info;
        int c;
        int longopt_index = 0;
        extern int optind, opterr;
        int show_usage = 0;
        char *optstring = "d:f:b:B:h";
        struct option long_options[] = {
                { "help", 0, 0, 'h' },
                { "duration", 1, 0, 'd' },
                { "file", 1, 0, 'f' },
                { "bitdepth", 1, 0, 'b' },
                { "bufsize", 1, 0, 'B' },
                { 0, 0, 0, 0 }
        };

        memset (&thread_info, 0, sizeof (thread_info));
        thread_info.rb_size = DEFAULT_RB_SIZE;
        opterr = 0;

        while ((c = getopt_long (argc, argv, optstring, long_options, &longopt_index)) != -1) {
                switch (c) {
                case 1:
                        /* getopt signals end of '-' options */
                        break;

                case 'h':
                        show_usage++;
                        break;
                case 'd':
                        thread_info.duration = atoi (optarg);
                        break;
                case 'f':
                        thread_info.path = optarg;
                        break;
                case 'b':
                        thread_info.bitdepth = atoi (optarg);
                        break;
                case 'B':
                        thread_info.rb_size = atoi (optarg);
                        break;
                default:
                        fprintf (stderr, "error\n");
                        show_usage++;
                        break;
                }
        }

        if (show_usage || thread_info.path == NULL || optind == argc) {
                fprintf (stderr, "usage: jackrec -f filename [ -d second ] [ -b bitdepth ] [ -B bufsize ] port1 [ port2 ... ]\n");
                exit (1);
        }

        if ((client = jack_client_open ("jackrec", JackNullOption, NULL)) == 0) {
                fprintf (stderr, "JACK server not running?\n");
                exit (1);
        }

        thread_info.client = client;
        thread_info.channels = argc - optind;
        thread_info.can_process = 0;

        setup_disk_thread (&thread_info);

        jack_set_process_callback (client, process, &thread_info);
        jack_on_shutdown (client, jack_shutdown, &thread_info);

        if (jack_activate (client)) {
                fprintf (stderr, "cannot activate client");
        }

        setup_ports (argc - optind, &argv[optind], &thread_info);

     /* install a signal handler to properly quits jack client */
    signal(SIGQUIT, signal_handler);
        signal(SIGTERM, signal_handler);
        signal(SIGHUP, signal_handler);
        signal(SIGINT, signal_handler);

        run_disk_thread (&thread_info);

        jack_client_close (client);

        jack_ringbuffer_free (rb);

        exit (0);
}

与 ALSA 设备交互

Jack 最终将从设备获取输入,并将输出发送到设备。目前,它们最有可能是 ALSA 的设备。因此,在 Jack 处理和 ALSA 输入输出之间必须有一个桥梁。这将涉及 ALSA 编程的所有复杂性。

幸运的是,有 Jack 客户端可以做到这一点。Jack 框架将按照启动 Jack 服务器时指定的那样与它们对话。

jackd -dalsa

所以,你不需要担心那个接口。对于勇敢和好奇的人来说,Jack source 有一个示例目录,其中包括文件alsa_in.calsa_out.c。它们包含作者的评论,比如"// Alsa stuff… i dont want to touch this bullshit in the next years.... please…",给你一个合理的警告,对于一般的 Jack 编程来说,这并不容易,也没有必要。

结论

这一章从用户的角度讲述了 Jack 的使用,也讲述了 Jack 客户端的编程。

八、会话管理

一个复杂的声音系统可能由多个声源、多个滤波器和多个输出组成。如果每次使用它们都必须重新设置,那么就会出现错误,浪费时间,等等。会话管理试图解决这些问题。

资源

以下是一些资源:

会话管理问题

每当有多个模块以某种方式链接时,就需要管理这些模块及其链接。这些需求在 Jack 环境中很快出现,它是为多重链接而设计的。管理变得繁琐并不需要复杂的插孔模块排列。例如,考虑图 8-1 中上一章的混音器会话。

A435426_1_En_8_Fig1_HTML.jpg

图 8-1。

Jack connecting multiple applications

从头开始设置需要以下条件:

  1. 启动jackdqjackctl
  2. 开始jack_mixer
  3. 打开混音器上的两组新输入端口。
  4. 将主混音器输出端口连接到回放端口。
  5. 将麦克风端口连接到一组混音器输入端口。
  6. 启动mplayer,自动连接到播放端口。
  7. 从回放端口断开mplayer输出端口,并将它们重新连接到另一组混音器输入端口。

你不想每次放一首歌都这样吧!

LADISH 会话管理器通过会话管理器( http://ladish.org/wiki/levels )识别应用的不同控制级别。除去对特定管理器和框架的显式引用,级别如下:

  • 级别 0:应用没有链接到会话处理库。用户必须手动保存应用项目,或者依赖应用的自动保存支持。
  • 级别 1:应用没有链接到会话处理库。当接收到特定的消息或信号时,应用进行保存。
  • 第 2 级:应用链接到会话管理库。由于会话管理器的限制,它与会话处理程序的交互是有限的。
  • 级别 3:应用链接到复杂的会话管理器。它与会话处理程序有完全的交互。

正如 Dave Phillips 所指出的,“使用这些级别是为了对任何 Linux 音频应用的各种可能条件进行分类和调整。这些条件包括符合 JACK 的程度、任何 WINE 或 DOS 要求、网络操作、现有 API 的多样性等等。”

当前一批用于 Linux 音频的会话管理框架包括

  • 讽刺
  • Jack 会话管理
  • 拉迪什
  • 非会话管理器
  • 奇诺

多个管理器的存在意味着大多数应用将只支持一个或至多几个协议。如果您选择一个特定的管理器,那么您将被限制在您可以在其控制下运行的应用。

插孔 _ 连接

程序jack_connectjack_disconnect可用于重新配置客户端之间的连接。例如,MIDI 播放器 TiMidity 会将其输出端口连接到第一个可用的插孔输入端口,这些端口通常是连接到声卡的系统端口。如果你想把 TiMidity 连接到,比方说,jack-rack,那么它的输出端口必须首先断开,然后再连接到正确的端口。另一方面,jack-rack默认情况下不连接任何东西,因此可能需要连接到系统端口。这是通过以下方式完成的:

jack_disconnect TiMidity:port_1 system:playback_1
jack_disconnect TiMidity:port_2 system:playback_2

jack_connect TiMidity:port_1 jack_rack:in_1
jack_connect TiMidity:port_2 jack_rack:in_2

jack_connect jack_rack:out_1 system:playback_1
jack_connect jack_rack:out_2 system:playback_2

讽刺

这是最早成功的用于 Linux 音频的会话管理器,但是已经不再使用了。它似乎已经不在 Ubuntu 的仓库里了。

需要 LASH 的应用之一是jack_mixer。更糟糕的是,它使用了来自python-lash.2.7.4-0ubuntu包的 Python LASH模块。我能找到的唯一一个副本需要 Python 2.7 以下的版本,Python 的安装版本是 2.7.4。这是一个目前无法从当前会话管理工具中获益的应用。虽然它可以作为第 1 级运行,但对于其他会话管理器,它只能在第 0 级运行。

因此,有一些 Jack 应用需要 LASH 来进行会话管理,但是这种支持似乎不再存在了。

Jack·塞申斯

您可以在 http://wiki.linuxaudio.org/apps/categories/jack_session 找到截至 2016 年的 Jack 会话感知应用列表。

qjackctl有一个会话管理器,允许您保存和恢复会话。您可以通过单击会话按钮,然后选择会话名称和目录来保存会话。qjackctl将会话信息作为 XML 文件存储在您保存它的任何目录中。对于上一个会话,如下所示:

<!DOCTYPE qjackctlSession>
<session name="session2">
 <client name="jack_mixer">
  <port type="out" name="MAIN L">
   <connect port="playback_1" client="system"/>
  </port>
  <port type="out" name="MAIN R">
   <connect port="playback_2" client="system"/>
  </port>
  <port type="in" name="midi in"/>
  <port type="out" name="Monitor L"/>
  <port type="out" name="Monitor R"/>
  <port type="in" name="Mixer L">
   <connect port="capture_1" client="system"/>
  </port>
  <port type="in" name="Mixer R">
   <connect port="capture_2" client="system"/>
  </port>
  <port type="out" name="Mixer Out L"/>
  <port type="out" name="Mixer Out R"/>
  <port type="in" name="mixer2 L">
   <connect port="out_0" client="MPlayer [8955]"/>
  </port>
  <port type="in" name="mixer2 R">
   <connect port="out_1" client="MPlayer [8955]"/>
  </port>
  <port type="out" name="mixer2 Out L"/>
  <port type="out" name="mixer2 Out R"/>
 </client>
 <client name="system">
  <port type="out" name="capture_1">
   <connect port="Mixer L" client="jack_mixer"/>
  </port>
  <port type="out" name="capture_2">
   <connect port="Mixer R" client="jack_mixer"/>
  </port>
  <port type="in" name="playback_1">
   <connect port="MAIN L" client="jack_mixer"/>
  </port>
  <port type="in" name="playback_2">
   <connect port="MAIN R" client="jack_mixer"/>
  </port>
 </client>
 <client name="MPlayer [8955]">
  <port type="out" name="out_0">
   <connect port="mixer2 L" client="jack_mixer"/>
  </port>
  <port type="out" name="out_1">
   <connect port="mixer2 R" client="jack_mixer"/>
  </port>
 </client>
</session>

在加载会话时,它看起来如图 8-2 所示。

A435426_1_En_8_Fig2_HTML.jpg

图 8-2。

qjackctl showing Jack session

如你所见,有许多红色的 x。恢复会话不会启动这些特定的应用。如果你手动重启jack_mixer,那么它会在它的主输出端口和系统回放端口之间建立链接,几个红色的 x 会消失。但是它不会创建之前创建的额外端口。您需要重复创建具有正确名称的新输入端口的工作;然后qjackctl重新建立连接,更多的红色 x 消失。

如果你再次运行mplayer,它只是建立自己的默认连接到播放端口,并且必须手动重新映射。它甚至不符合 0 级,因为qjackctl不会自动重新映射它的连接。

这里的问题是mplayerjack_mixer不使用 Jack 会话管理协议。会话管理器会重置某些应用建立的任何连接,但不是所有应用。后面给出了一个将 Jack 会话管理添加到应用中的例子,然后它将被正确地重启和重新连接。

拉迪什

LADISH 被设计为 LASH 的继任者,可以在资源库中获得。

LADISH 可以启动、停止和配置会话。特别是,它可以设置不同的插孔配置。这意味着你不能先启动 Jack,然后再启动 LADISH 反过来:启动 GUI 工具gladish,配置 Jack,然后启动一个会话。该过程在 LADI 会话处理程序 Wiki ( http://ladish.org/wiki/tutorial )中有描述。跟踪它,特别是把 Jack 和 ALSA 联系起来。否则,你将听不到声音!另请参见企鹅制作人的 LADI 会话处理程序( www.penguinproducer.com/Blog/2011/12/the-ladi-session-handler/ )。

一旦你设置好 LADISH,启动一个新的工作室,然后从它的应用菜单中启动应用。要运行mplayer,您需要给出如下完整命令:

          mplayer -ao jack 54154.mp3

你可以从应用菜单启动jack_mixer,然后添加两组新的输入端口,如第七章所示。重新连接它们之后,你就得到如图 8-3 所示的连接图。

A435426_1_En_8_Fig3_HTML.jpg

图 8-3。

LADISH session

连接图作为 XML 文件存储在$HOME/.ladish中。例如,图 8-3 中的图形存储如下:

<?xml version="1.0"?>
<!--
ladish Studio configuration.
-->
<!-- Sun Sep 29 10:49:54 2013 -->
<studio>
  <jack>
    <conf>
      <parameter path="/engine/driver">alsa</parameter>
      <parameter path="/engine/client-timeout">500</parameter>
      <parameter path="/engine/port-max">64</parameter>
    </conf>
    <clients>
      <client name="system" uuid="5ef937c6-46f7-45cd-8441-8ff6e2aee4eb">
        <ports>
          <port name="capture_1" uuid="9432f206-44c3-45cb-8024-3ba7160962bc" />
          <port name="capture_2" uuid="3c9acf5c-c91d-4692-add2-e3defb7c508a" />
          <port name="playback_1" uuid="95c68011-dab9-401c-8904-b3d149e20570" />
          <port name="playback_2" uuid="5b8e9215-3ff4-4973-8c0b-1eb5ab7ccc9b" />
        </ports>
      </client>
      <client name="jack_mixer-3" uuid="4538833e-d7e7-47d0-8a43-67ee25d17898">
        <ports>
          <port name="midi in" uuid="17d04191-f59d-4d16-970c-55030162aae7" />
          <port name="MAIN L" uuid="9d986401-c303-4f35-89b7-a32e10120ce4" />
          <port name="MAIN R" uuid="fae94d01-00ef-449d-8e05-f95df84c5357" />
          <port name="Monitor L" uuid="1758d824-75cd-46b3-8e53-82c6be1ca200" />
          <port name="Monitor R" uuid="d14815e9-d3bc-457b-8e4f-29ad29ea36f7" />
          <port name="Mixer L" uuid="07d388ed-d00a-4ee0-92aa-3ae79200e11e" />
          <port name="Mixer R" uuid="d1eb3400-75ce-422d-b9b8-b7e670f95428" />
          <port name="Mixer Out L" uuid="fad2a77e-6146-4919-856f-b6f7befdb84d" />
          <port name="Mixer Out R" uuid="920c5d12-9f62-46aa-b191-52bfbb94065d" />
          <port name="mixer2 L" uuid="c2b96996-9cd1-41dd-a750-192bb5717438" />
          <port name="mixer2 R" uuid="3de52738-d7e8-4733-bf08-3ea2b6372a4c" />
          <port name="mixer2 Out L" uuid="4e08eba4-a0c1-4e76-9dff-c14f76d5328e" />
          <port name="mixer2 Out R" uuid="9d2f79a5-e2d0-484b-b094-98ef7a4f61a7" />
        </ports>
      </client>
      <client name="mplayer" uuid="66e0d45f-2e21-4fbf-ac34-5d3658ee018a">
        <ports>
          <port name="out_0" uuid="83152a6e-e6f6-4357-93ce-020ba58b7d00" />
          <port name="out_1" uuid="55a05594-174d-48a5-805b-96d2c9e77cf1" />
        </ports>
      </client>
    </clients>
  </jack>
  <clients>
    <client name="Hardware Capture" uuid="47c1cd18-7b21-4389-bec4-6e0658e1d6b1" naming="app">
      <ports>
        <port name="capture_1" uuid="9432f206-44c3-45cb-8024-3ba7160962bc" type="audio" direction="output" />
        <port name="capture_2" uuid="3c9acf5c-c91d-4692-add2-e3defb7c508a" type="audio" direction="output" />
      </ports>
      <dict>
        <key name="http://ladish.org/ns/canvas/x">1364.000000</key>
        <key name="http://ladish.org/ns/canvas/y">1083.000000</key>
      </dict>
    </client>
    <client name="Hardware Playback" uuid="b2a0bb06-28d8-4bfe-956e-eb24378f9629" naming="app">
      <ports>
        <port name="playback_1" uuid="95c68011-dab9-401c-8904-b3d149e20570" type="audio" direction="input" />
        <port name="playback_2" uuid="5b8e9215-3ff4-4973-8c0b-1eb5ab7ccc9b" type="audio" direction="input" />
      </ports>
      <dict>
        <key name="http://ladish.org/ns/canvas/x">1745.000000</key>
        <key name="http://ladish.org/ns/canvas/y">1112.000000</key>
      </dict>
    </client>
    <client name="jack_mixer-3" uuid="4b198f0f-5a77-4486-9f54-f7ec044d9bf2" naming="app" app="98729282-8b18-4bcf-b929-41bc53f2b4ed">
      <ports>
        <port name="midi in" uuid="17d04191-f59d-4d16-970c-55030162aae7" type="midi" direction="input" />
        <port name="MAIN L" uuid="9d986401-c303-4f35-89b7-a32e10120ce4" type="audio" direction="output" />
        <port name="MAIN R" uuid="fae94d01-00ef-449d-8e05-f95df84c5357" type="audio" direction="output" />
        <port name="Monitor L" uuid="1758d824-75cd-46b3-8e53-82c6be1ca200" type="audio" direction="output" />
        <port name="Monitor R" uuid="d14815e9-d3bc-457b-8e4f-29ad29ea36f7" type="audio" direction="output" />
        <port name="Mixer L" uuid="07d388ed-d00a-4ee0-92aa-3ae79200e11e" type="audio" direction="input" />
        <port name="Mixer R" uuid="d1eb3400-75ce-422d-b9b8-b7e670f95428" type="audio" direction="input" />
        <port name="Mixer Out L" uuid="fad2a77e-6146-4919-856f-b6f7befdb84d" type="audio" direction="output" />
        <port name="Mixer Out R" uuid="920c5d12-9f62-46aa-b191-52bfbb94065d" type="audio" direction="output" />
        <port name="mixer2 L" uuid="c2b96996-9cd1-41dd-a750-192bb5717438" type="audio" direction="input" />
        <port name="mixer2 R" uuid="3de52738-d7e8-4733-bf08-3ea2b6372a4c" type="audio" direction="input" />
        <port name="mixer2 Out L" uuid="4e08eba4-a0c1-4e76-9dff-c14f76d5328e" type="audio" direction="output" />
        <port name="mixer2 Out R" uuid="9d2f79a5-e2d0-484b-b094-98ef7a4f61a7" type="audio" direction="output" />
      </ports>
      <dict>
        <key name="http://ladish.org/ns/canvas/x">1560.000000</key>
        <key name="http://ladish.org/ns/canvas/y">1104.000000</key>
      </dict>
    </client>
    <client name="mplayer" uuid="2f15cfec-7f6d-41b4-80e8-e1ae80c3be9e" naming="app" app="7a9be17b-eb40-4be3-a9dc-82f36bbceeeb">
      <ports>
        <port name="out_0" uuid="83152a6e-e6f6-4357-93ce-020ba58b7d00" type="audio" direction="output" />
        <port name="out_1" uuid="55a05594-174d-48a5-805b-96d2c9e77cf1" type="audio" direction="output" />
      </ports>
      <dict>
        <key name="http://ladish.org/ns/canvas/x">1350.000000</key>
        <key name="http://ladish.org/ns/canvas/y">1229.000000</key>
      </dict>
    </client>
  </clients>
  <connections>
    <connection port1="9432f206-44c3-45cb-8024-3ba7160962bc" port2="07d388ed-d00a-4ee0-92aa-3ae79200e11e" />
    <connection port1="3c9acf5c-c91d-4692-add2-e3defb7c508a" port2="d1eb3400-75ce-422d-b9b8-b7e670f95428" />
    <connection port1="fad2a77e-6146-4919-856f-b6f7befdb84d" port2="95c68011-dab9-401c-8904-b3d149e20570" />
    <connection port1="920c5d12-9f62-46aa-b191-52bfbb94065d" port2="5b8e9215-3ff4-4973-8c0b-1eb5ab7ccc9b" />
    <connection port1="83152a6e-e6f6-4357-93ce-020ba58b7d00" port2="c2b96996-9cd1-41dd-a750-192bb5717438" />
    <connection port1="55a05594-174d-48a5-805b-96d2c9e77cf1" port2="3de52738-d7e8-4733-bf08-3ea2b6372a4c" />
  </connections>
  <applications>
    <application name="jack_mixer-3" uuid="98729282-8b18-4bcf-b929-41bc53f2b4ed" terminal="false" level="0" autorun="true">jack_mixer</application>
    <application name="mplayer" uuid="7a9be17b-eb40-4be3-a9dc-82f36bbceeeb" terminal="true" level="0" autorun="true">mplayer -ao jack %2Fhome%2Fhttpd%2Fhtml%2FLinuxSound%2FKaraoke%2FSubtitles%2Fsongs%2F54154.mp3</application>
  </applications>
</studio>

重启mplayer的完整命令存储在该文件中,所有连接也是如此。

在停止和重新启动会话时,mplayer使用相同的 MP3 文件启动,但具有默认连接。它忽略了 LADISH 会话的连接。类似地,jack_mixer重新启动,但是必须手动重新创建附加端口。这不是一个 LADISH 感知的应用,所以它运行在 0 级。然而,一旦创建,拉迪什重连就形成了。

你可以在 http://wiki.linuxaudio.org/apps/all/ladish 找到 LADISH 感知应用列表。

从用户的角度来看,这些会话管理器之间的差异如下:

  • Jack 应用可以以任何方式启动,并将被 Jack 会话管理器拾取。但是,任何特定的命令行参数都将丢失。
  • 应用需要由 LADISH 会话管理器启动,以便由它来管理。但是,它可以记录命令行参数,并使用它们重新启动应用。

从开发人员的角度来看,这些会话管理器之间的区别如下:

  • Jack 会话感知应用可以以任何方式启动,并将在程序中对重启它们所需的命令行进行编码。

Jack 会话 API

可以由 Jack sessions (JS)管理的应用可以是第 1 级的 Jack session-aware 或 Jack session-unaware。对于那些没有意识到的人来说,最好的办法就是由会话管理器来启动和停止他们。对于支持 Jack 会话的应用,它们必须设置为执行以下操作:

  • 向 Jack 会话管理器注册
  • 响应来自 Jack 会话管理器的消息
  • 可以从会话信息开始

对 Jack 会话消息的响应通常会执行以下操作:

  • 将应用的状态保存到一个文件中,该文件的目录由会话管理器给出。
  • 使用可用于重启应用的命令回复会话管理器,并提供足够的信息以恢复其状态(通常是存储其状态信息的文件的名称)。

Jack 会话感知客户端通过唯一的通用标识符(UUID)向会话管理器标识自己。这是什么或者是如何产生的,似乎并不重要。只要它是一个用字符串表示的整数,客户端应用就可以创建它。这在注册时传递给会话管理器,但在会话管理器重新启动客户端时也应该传递回客户端。这是通过应用的命令行参数完成的,命令行的格式也由客户端决定。

一个简单的例子可能是两个选项(-u用于 UUID,-f用于保存的状态文件)。这将使用getopt进行解析,如下所示:

int main(int argc, char **argv) {
  int c;
  char *file = NULL;
  char *uuid = "13";
  while ((c = getopt (argc, argv, "f:u:")) != -1)
    switch (c) {
      case 'u':
        uuid = optarg;
        break;
      case 'f':
        file = optarg;
        break;
      ...
    }
  }
  ...
}

然后,应用可以使用先前存储在状态文件中的信息来恢复其状态,然后使用以下信息再次向会话管理器注册:

jack_client *client;
client = jack_client_open("myapp", JackSessionID, NULL, uuid);
jack_set_session_callback(client, session_callback, NULL);

每当会话管理器需要与应用通信时,就调用回调函数session_callback。它需要一个jack_session_event和作为最后一个参数传递给jack_set_session_callback的任何东西。

回调的工作是保存状态信息,将信息传递回会话管理器,并可能退出。

int session_callback(jack_session_event_t *ev) {
  char filename[256];
  char command[256];

  snprintf(filename, sizeof(filename), "%smyfile.state", ev->session_dir);
  snprintf(command,  sizeof(command),
           "my_app -u %s -f ${SESSION_DIR}myfile.state", ev->client_uuid);
  your_save_function(filename);
  ev->command_line = strdup(command);
  jack_session_reply(jack_client, ev);
  if(ev->type == JackSessionSaveAndQuit)
      quit();
  jack_session_event_free(ev);
  return 0;
}

trac 建议( http://trac.jackaudio.org/wiki/WalkThrough/Dev/JackSession )如果这是在 GTK 这样的多线程环境中运行,那么应该在其他线程空闲的时候运行,比如用g_idel_add

我可以用第七章中的delay程序来说明这一点。添加额外的代码给出了修改后的delay.c。我用#ifdef JACK_SESSION附上了额外的代码,以便于查看变化。

/** @file delay.c
 *
 * @brief This client delays one channel by 4096 framse.
 */

#define JACK_SESSION

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <signal.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include <jack/jack.h>

#ifdef JACK_SESSION

#include <jack/session.h>

#endif

jack_port_t **input_ports;
jack_port_t **output_ports;
jack_client_t *client;

#define SIZE 8192
#define DELAY 4096
jack_default_audio_sample_t buffer[SIZE];
int idx, delay_idx;

static void signal_handler ( int sig )
{
    jack_client_close ( client );
    fprintf ( stderr, "signal received, exiting ...\n" );
    exit ( 0 );
}

static void copy2out( jack_default_audio_sample_t *out,
                      jack_nframes_t nframes) {
    if (delay_idx + nframes < SIZE) {
        memcpy(out, buffer + delay_idx,
               nframes * sizeof ( jack_default_audio_sample_t ) );
    } else {
        int frames_to_end = SIZE - delay_idx;
        int overflow = delay_idx + nframes - SIZE;
        memcpy(out, buffer + delay_idx,
               frames_to_end * sizeof ( jack_default_audio_sample_t ) );
        memcpy(out, buffer, overflow * sizeof(jack_default_audio_sample_t));
    }
    delay_idx = (delay_idx + nframes) % SIZE;
}

static void copy2buffer( jack_default_audio_sample_t *in,
                         jack_nframes_t nframes) {
    if (idx + nframes < SIZE) {
        memcpy(buffer + idx, in,
               nframes * sizeof ( jack_default_audio_sample_t ) );
    } else {
        int frames_to_end = SIZE - idx;
        int overflow = idx + nframes - SIZE;
        memcpy(buffer + idx, in,
               frames_to_end * sizeof ( jack_default_audio_sample_t ) );
        memcpy(buffer, in, overflow * sizeof(jack_default_audio_sample_t));
    }
    idx = (idx + nframes) % SIZE;
}

/**
 * The process callback for this JACK application is called in a
 * special realtime thread once for each audio cycle.
 *
 * This client follows a simple rule: when the JACK transport is
 * running, copy the input port to the output.  When it stops, exit.
 */

int
process ( jack_nframes_t nframes, void *arg )
{
    int i;
    jack_default_audio_sample_t *in, *out;

    in = jack_port_get_buffer ( input_ports[0], nframes );
    out = jack_port_get_buffer ( output_ports[0], nframes );
    memcpy ( out, in, nframes * sizeof ( jack_default_audio_sample_t ) );

    in = jack_port_get_buffer ( input_ports[1], nframes );
    out = jack_port_get_buffer ( output_ports[1], nframes );
    copy2out(out, nframes);
    copy2buffer(in, nframes);

    return 0;
}

/**
 * JACK calls this shutdown_callback if the server ever shuts down or
 * decides to disconnect the client.
 */
void
jack_shutdown ( void *arg ) {
    free ( input_ports );
    free ( output_ports );
    exit ( 1 );
}

#ifdef JACK_SESSION

/*

 * Callback function for JS

 */

void session_callback(jack_session_event_t *ev, void *args) {

    char command[256];

    snprintf(command,  sizeof(command),

             "/home/httpd/html/LinuxSound/Sampled/SessionManagement/delay -u %s",

             ev->client_uuid);

    ev->flags = JackSessionNeedTerminal;

    ev->command_line = strdup(command);

    jack_session_reply(client, ev);

    if(ev->type == JackSessionSaveAndQuit)

         jack_shutdown(NULL);

    jack_session_event_free(ev);

}

#endif

int main ( int argc, char *argv[] ) {
    int i;
    const char **ports;
    const char *client_name;
    const char *server_name = NULL;
    jack_status_t status;

#ifdef JACK_SESSION

    /*

     * Extra code for JS

     */

    int c;

    char *uuid = "13";

    while ((c = getopt (argc, argv, "u:")) != -1)

        switch (c) {

        case 'u':

            uuid = optarg;

            break;

        }

    printf("UUID is %s\n", uuid);

#endif

    client_name = strrchr ( argv[0], '/' );
    if ( client_name == 0 ) {
        client_name = argv[0];
    }
    else {
        client_name++;
    }

    /* open a client connection to the JACK server */
    /* Changed args for JS */

#ifdef JACK_SESSION

    client = jack_client_open ( client_name, JackSessionID, &status, uuid);

#else

    client = jack_client_open ( client_name, JackNullOption, &status);

#endif

    if ( client == NULL )
        {
            fprintf ( stderr, "jack_client_open() failed, "
                      "status = 0x%2.0x\n", status );
            if ( status & JackServerFailed )
                {
                    fprintf ( stderr, "Unable to connect to JACK server\n" );
                }
            exit ( 1 );
        }
    if ( status & JackServerStarted )
        {
            fprintf ( stderr, "JACK server started\n" );
        }
    if ( status & JackNameNotUnique )
        {
            client_name = jack_get_client_name ( client );
            fprintf ( stderr, "unique name `%s' assigned\n", client_name );
        }

#ifdef JACK_SESSION

    /* Set callback function for JS

     */

    jack_set_session_callback(client, session_callback, NULL);

#endif

    /* tell the JACK server to call `process()' whenever
       there is work to be done.
    */
    jack_set_process_callback ( client, process, 0 );

    /* tell the JACK server to call `jack_shutdown()' if
       it ever shuts down, either entirely, or if it
       just decides to stop calling us.
    */

    jack_on_shutdown ( client, jack_shutdown, 0 );

    /* create two ports pairs*/
    input_ports = ( jack_port_t** ) calloc ( 2, sizeof ( jack_port_t* ) );
    output_ports = ( jack_port_t** ) calloc ( 2, sizeof ( jack_port_t* ) );

    char port_name[16];
    for ( i = 0; i < 2; i++ )
        {
            sprintf ( port_name, "input_%d", i + 1 );
            input_ports[i] = jack_port_register ( client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0 );
            sprintf ( port_name, "output_%d", i + 1 );
            output_ports[i] = jack_port_register ( client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0 );
            if ( ( input_ports[i] == NULL ) || ( output_ports[i] == NULL ) )
                {
                    fprintf ( stderr, "no more JACK ports available\n" );
                    exit ( 1 );
                }
        }

    bzero(buffer, SIZE * sizeof ( jack_default_audio_sample_t ));
    delay_idx = 0;
    idx = DELAY;

    /* Tell the JACK server that we are ready to roll.  Our
     * process() callback will start running now. */

    if ( jack_activate ( client ) )
        {
            fprintf ( stderr, "cannot activate client" );
            exit ( 1 );
        }

    /* Connect the ports.  You can't do this before the client is
     * activated, because we can't make connections to clients
     * that aren't running.  Note the confusing (but necessary)
     * orientation of the driver backend ports: playback ports are
     * "input" to the backend, and capture ports are "output" from
     * it.
     */

    ports = jack_get_ports ( client, NULL, NULL, JackPortIsPhysical|JackPortIsOutput );
    if ( ports == NULL )
        {
            fprintf ( stderr, "no physical capture ports\n" );
            exit ( 1 );
        }

    for ( i = 0; i < 2; i++ )
        if ( jack_connect ( client, ports[i], jack_port_name ( input_ports[i] ) ) )
            fprintf ( stderr, "cannot connect input ports\n" );

    free ( ports );

    ports = jack_get_ports ( client, NULL, NULL, JackPortIsPhysical|JackPortIsInput );
    if ( ports == NULL )
        {
            fprintf ( stderr, "no physical playback ports\n" );
            exit ( 1 );
        }

    for ( i = 0; i < 2; i++ )
        if ( jack_connect ( client, jack_port_name ( output_ports[i] ), ports[i] ) )
            fprintf ( stderr, "cannot connect input ports\n" );

    free ( ports );

    /* install a signal handler to properly quits jack client */
#ifdef WIN32
    signal ( SIGINT, signal_handler );
    signal ( SIGABRT, signal_handler );
    signal ( SIGTERM, signal_handler );
#else
    signal ( SIGQUIT, signal_handler );
    signal ( SIGTERM, signal_handler );
    signal ( SIGHUP, signal_handler );
    signal ( SIGINT, signal_handler );
#endif

    /* keep running until the transport stops */

    while (1)
        {
#ifdef WIN32
            Sleep ( 1000 );
#else
            sleep ( 1 );
#endif
        }

    jack_client_close ( client );
    exit ( 0 );
}

拉迪什蜜蜂

如果一个应用支持 Jack 会话,那么 LADISH GUI 工具gladish可以将该应用作为 1 级应用来管理。换句话说,gladish可以平等地管理 Jack 会话和 LADISH 客户端。从这个意义上说,除非您更喜欢 LADISH 的会话管理方式,否则没有必要在应用中额外添加 LADISH 意识。

关于如何在 1 级构建 LADISH-aware 应用,参见 http://ladish.org/wiki/code_examples 。对于 LADI 会话处理程序,参见 http://ladish.org/

结论

本章介绍了一些会话管理系统。所涵盖的会话管理器集并不详尽。访问 http://lwn.net/Articles/533594/ 获得更多列表,例如非会话管理器和 Chino。然而,情况并不特别令人满意,还有很大的改进余地。

九、Java 声音

本章讲述了使用 Java Sound API 对采样数据进行编程的要点。本章假设读者具备良好的 Java 应用知识。Java 声音在 Java 早期就已经存在了。它处理采样和 MIDI 数据,是一个综合的系统。

资源

许多资源可用于 Java Sound。

关键 Java 声音类

这些是关键类:

  • AudioSystem类是所有采样音频类的入口点。
  • AudioFormat类指定了关于格式的信息,比如采样率。
  • AudioInputStream类从混合器的目标行提供一个输入流。
  • Mixer类代表一个音频设备。
  • SourceDataLine类代表一个设备的输入线。
  • TargetDataLine类代表一个设备的输出线。

关于设备的信息

每个设备由一个Mixer对象表示。向AudioSystem询问这些内容的列表。每个混音器都有一组目标(输出)线和源(输入)线。分别询问每个混音器。下面这个节目叫做DeviceInfo.java:

import javax.sound.sampled.*;

public class DeviceInfo {

    public static void main(String[] args) throws Exception {

        Mixer.Info[] minfoSet = AudioSystem.getMixerInfo();
        System.out.println("Mixers:");
        for (Mixer.Info minfo: minfoSet) {
            System.out.println("   " + minfo.toString());

            Mixer m = AudioSystem.getMixer(minfo);
            System.out.println("    Mixer: " + m.toString());
            System.out.println("      Source lines");
            Line.Info[] slines = m.getSourceLineInfo();
            for (Line.Info s: slines) {
                System.out.println("        " + s.toString());
            }

            Line.Info[] tlines = m.getTargetLineInfo();
            System.out.println("      Target lines");
            for (Line.Info t: tlines) {
                System.out.println("        " + t.toString());
            }
        }
    }
}

以下是我的系统上的部分输出:

Mixers:
   PulseAudio Mixer, version 0.02
      Source lines
        interface SourceDataLine supporting 42 audio formats, and buffers of 0 to 1000000 bytes
        interface Clip supporting 42 audio formats, and buffers of 0 to 1000000 bytes
      Target lines
        interface TargetDataLine supporting 42 audio formats, and buffers of 0 to 1000000 bytes
   default [default], version 1.0.24
      Source lines
        interface SourceDataLine supporting 512 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 512 audio formats, and buffers of at least 32 bytes
      Target lines
        interface TargetDataLine supporting 512 audio formats, and buffers of at least 32 bytes
   PCH [plughw:0,0], version 1.0.24
      Source lines
        interface SourceDataLine supporting 24 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 24 audio formats, and buffers of at least 32 bytes
      Target lines
        interface TargetDataLine supporting 24 audio formats, and buffers of at least 32 bytes
   NVidia [plughw:1,3], version 1.0.24
      Source lines
        interface SourceDataLine supporting 96 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 96 audio formats, and buffers of at least 32 bytes
      Target lines
   NVidia [plughw:1,7], version 1.0.24
      Source lines
        interface SourceDataLine supporting 96 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 96 audio formats, and buffers of at least 32 bytes
      Target lines
   NVidia [plughw:1,8], version 1.0.24
      Source lines
        interface SourceDataLine supporting 96 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 96 audio formats, and buffers of at least 32 bytes
      Target lines

这显示了脉冲音频和 ALSA 混频器。例如,进一步的查询可以显示支持的格式。

播放文件中的音频

要从文件中播放,必须创建适当的对象,以便从文件中读取和写入输出设备。这些措施如下:

  • AudioSystem请求一个AudioInputStream。它是用文件名作为参数创建的。
  • 为输出创建源数据行。术语可能会混淆:程序产生输出,但这是数据线的输入。因此,数据线必须是输出设备的源。数据线的创建是一个多步骤的过程。
    • 首先创建一个AudioFormat对象来指定数据线的参数。
    • 为 audion 格式的源数据线创建一个DataLine.Info
    • 从将处理DataLine.InfoAudioSystem请求源数据线。

按照这些步骤,可以从输入流中读取数据,并将其写入数据线。图 9-1 显示了相关类的 UML 类图。

A435426_1_En_9_Fig1_HTML.gif

图 9-1。

Class diagram for playing audio from a file

import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

public class PlayAudioFile {
    /** Plays audio from given file names. */
    public static void main(String [] args) {
        // Check for given sound file names.
        if (args.length < 1) {
            System.out.println("Usage: java Play <sound file names>*");
            System.exit(0);
        }

        // Process arguments.
        for (int i = 0; i < args.length; i++)
            playAudioFile(args[i]);

        // Must exit explicitly since audio creates non-daemon threads.
        System.exit(0);
    } // main

    public static void playAudioFile(String fileName) {
        File soundFile = new File(fileName);

        try {
            // Create a stream from the given file.
            // Throws IOException or UnsupportedAudioFileException
            AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(soundFile);
            // AudioSystem.getAudioInputStream(inputStream); // alternate audio stream from inputstream
            playAudioStream(audioInputStream);
        } catch (Exception e) {
            System.out.println("Problem with file " + fileName + ":");
            e.printStackTrace();
        }
    } // playAudioFile

    /** Plays audio from the given audio input stream. */
    public static void playAudioStream(AudioInputStream audioInputStream) {
        // Audio format provides information like sample rate, size, channels.
        AudioFormat audioFormat = audioInputStream.getFormat();
        System.out.println("Play input audio format=" + audioFormat);

        // Open a data line to play our type of sampled audio.
        // Use SourceDataLine for play and TargetDataLine for record.
        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
        if (!AudioSystem.isLineSupported(info)) {
            System.out.println("Play.playAudioStream does not handle this type of audio on this system.");
            return;
        }

        try {
            // Create a SourceDataLine for play back (throws LineUnavailableException).
            SourceDataLine dataLine = (SourceDataLine) AudioSystem.getLine(info);
            // System.out.println("SourceDataLine class=" + dataLine.getClass());

            // The line acquires system resources (throws LineAvailableException).
            dataLine.open(audioFormat);

            // Adjust the volume on the output line.
            if(dataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
                FloatControl volume = (FloatControl) dataLine.getControl(FloatControl.Type.MASTER_GAIN);
                volume.setValue(6.0F);
            }

            // Allows the line to move data in and out to a port.
            dataLine.start();

            // Create a buffer for moving data from the audio stream to the line.
            int bufferSize = (int) audioFormat.getSampleRate() * audioFormat.getFrameSize();
            byte [] buffer = new byte[ bufferSize ];

            // Move the data until done or there is an error.
            try {
                int bytesRead = 0;
                while (bytesRead >= 0) {
                    bytesRead = audioInputStream.read(buffer, 0, buffer.length);
                    if (bytesRead >= 0) {
                        // System.out.println("Play.playAudioStream bytes read=" + bytesRead +
                        // ", frame size=" + audioFormat.getFrameSize() + ", frames read=" + bytesRead / audioFormat.getFrameSize());
                        // Odd sized sounds throw an exception if we don't write the same amount.
                        int framesWritten = dataLine.write(buffer, 0, bytesRead);
                    }
                } // while
            } catch (IOException e) {
                e.printStackTrace();
            }

            System.out.println("Play.playAudioStream draining line.");
            // Continues data line I/O until its buffer is drained.
            dataLine.drain();

            System.out.println("Play.playAudioStream closing line.");
            // Closes the data line, freeing any resources such as the audio device.
            dataLine.close();
        } catch (LineUnavailableException e) {
            e.printStackTrace();
        }
    } // playAudioStream
} // PlayAudioFile

将音频录制到文件

做这件事的大部分工作是准备音频输入流。一旦完成,方法AudioSystemwrite将从音频输入流复制输入到输出文件。

要准备音频输入流,请执行以下步骤:

  1. 创建一个描述输入参数的AudioFormat对象。
  2. 麦克风产生音频。所以,它需要一个TargetDataLine。所以,为目标数据线创建一个DataLine.Info
  3. AudioSystem询问满足信息的行。
  4. AudioInputStream把线包起来。

输出只是一个 Java File

然后使用AudioSystem函数write()将流复制到文件中。图 9-2 显示了 UML 类图。

A435426_1_En_9_Fig2_HTML.gif

图 9-2。

UML diagram for recording audio to a file

该计划如下:

import javax.sound.sampled.*;
import java.io.File;

/**
 * Sample audio recorder
 */
public class Recorder extends Thread
{
    /**
     * The TargetDataLine that we’ll use to read data from
     */
    private TargetDataLine line;

    /**
     * The audio format type that we’ll encode the audio data with
     */
    private AudioFileFormat.Type targetType = AudioFileFormat.Type.WAVE;

    /**
     * The AudioInputStream that we’ll read the audio data from
     */
    private AudioInputStream inputStream;

    /**
     * The file that we’re going to write data out to
     */
    private File file;

    /**
     * Creates a new Audio Recorder
     */
    public Recorder(String outputFilename)
    {
        try {
            // Create an AudioFormat that specifies how the recording will be performed
            // In this example we’ll 44.1Khz, 16-bit, stereo
            AudioFormat audioFormat = new AudioFormat(
            AudioFormat.Encoding.PCM_SIGNED,           // Encoding technique
            44100.0F,                                  // Sample Rate
            16,                                        // Number of bits in each channel
            2,                                         // Number of channels (2=stereo)
            4,                                         // Number of bytes in each frame
            44100.0F,                                  // Number of frames per second
            false);                                    // Big-endian (true) or little-
            // endian (false)

            // Create our TargetDataLine that will be used to read audio data by first
            // creating a DataLine instance for our audio format type
            DataLine.Info info = new DataLine.Info(TargetDataLine.class, audioFormat);

            // Next we ask the AudioSystem to retrieve a line that matches the
            // DataLine Info
            this.line = (TargetDataLine)AudioSystem.getLine(info);

            // Open the TargetDataLine with the specified format
            this.line.open(audioFormat);

            // Create an AudioInputStream that we can use to read from the line
            this.inputStream = new AudioInputStream(this.line);

            // Create the output file
            this.file = new File(outputFilename);
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    public void startRecording() {
        // Start the TargetDataLine
        this.line.start();

        // Start our thread
        start();
    }

    public void stopRecording() {
        // Stop and close the TargetDataLine
        this.line.stop();
        this.line.close();
    }

    public void run() {
        try {
            // Ask the AudioSystem class to write audio data from the audio input stream
            // to our file in the specified data type (PCM 44.1Khz, 16-bit, stereo)
            AudioSystem.write(this.inputStream, this.targetType, this.file);
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println("Usage: Recorder <filename>");
            System.exit(0);
        }

        try {
            // Create a recorder that writes WAVE data to the specified filename
            Recorder r = new Recorder(args[0]);
            System.out.println("Press ENTER to start recording");
            System.in.read();

            // Start the recorder
            r.startRecording();

            System.out.println("Press ENTER to stop recording");
            System.in.read();

            // Stop the recorder
            r.stopRecording();

            System.out.println("Recording complete");
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }

}

向扬声器播放麦克风

这是前两个节目的结合。准备一个AudioInputStream用于从麦克风读取。一个SourceDataLine是准备写给演讲者的。通过从音频输入流读取数据并写入源数据线,将数据从第一个复制到第二个。图 9-3 显示了 UML 类图。

A435426_1_En_9_Fig3_HTML.gif

图 9-3。

UML diagram for sending microphone input to a speaker

该计划如下:

import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.Line.Info;
import javax.sound.sampled.TargetDataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

public class PlayMicrophone {
    private static final int FRAMES_PER_BUFFER = 1024;

    public static void main(String[] args) throws Exception {

        new PlayMicrophone().

playAudio();

    }

    private void out(String strMessage)
    {
        System.out.println(strMessage);
    }

  //This method creates and returns an
  // AudioFormat object for a given set of format
  // parameters.  If these parameters don't work
  // well for you, try some of the other
  // allowable parameter values, which are shown
  // in comments following the declarations.
  private  AudioFormat getAudioFormat(){
    float sampleRate = 44100.0F;    //8000,11025,16000,22050,44100
    int sampleSizeInBits = 16;      //8,16
    int channels = 1;               //1,2
    boolean signed = true;          //true,false
    boolean bigEndian = false;      //true,false
    return new AudioFormat(sampleRate,
                           sampleSizeInBits,
                           channels,
                           signed,
                           bigEndian);
  }//end getAudioFormat

    public void playAudio() throws Exception {
        AudioFormat audioFormat;
        TargetDataLine targetDataLine;

        audioFormat = getAudioFormat();
        DataLine.Info dataLineInfo =
            new DataLine.Info(
                              TargetDataLine.class,
                              audioFormat);
        targetDataLine = (TargetDataLine)
            AudioSystem.getLine(dataLineInfo);

        /*
        Line.Info lines[] = AudioSystem.getTargetLineInfo(dataLineInfo);
        for (int n = 0; n < lines.length; n++) {
            System.out.println("Target " + lines[n].toString() + " " + lines[n].getLineClass());
        }
        targetDataLine = (TargetDataLine)
            AudioSystem.getLine(lines[0]);
        */

        targetDataLine.open(audioFormat,
                            audioFormat.getFrameSize() * FRAMES_PER_BUFFER);
        targetDataLine.start();

        playAudioStream(new AudioInputStream(targetDataLine));

        /*
        File soundFile = new File( fileName );

        try {
            // Create a stream from the given file.
            // Throws IOException or UnsupportedAudioFileException
            AudioInputStream audioInputStream = AudioSystem.getAudioInputStream( soundFile );
            // AudioSystem.getAudioInputStream( inputStream ); // alternate audio stream from inputstream
            playAudioStream( audioInputStream );
        } catch ( Exception e ) {
            System.out.println( "Problem with file " + fileName + ":" );
            e.printStackTrace();
        }
        */
    } // playAudioFile

    /** Plays audio from the given audio input stream. */
    public void playAudioStream( AudioInputStream audioInputStream ) {
        // Audio format provides information like sample rate, size, channels.
        AudioFormat audioFormat = audioInputStream.getFormat();
        System.out.println( "Play input audio format=" + audioFormat );

        // Open a data line to play our type of sampled audio.
        // Use SourceDataLine for play and TargetDataLine for record.
        DataLine.Info info = new DataLine.Info( SourceDataLine.class, audioFormat );

        Line.Info lines[] = AudioSystem.getSourceLineInfo(info);
        for (int n = 0; n < lines.length; n++) {
            System.out.println("Source " + lines[n].toString() + " " + lines[n].getLineClass());
        }

        if ( !AudioSystem.isLineSupported( info ) ) {
            System.out.println( "Play.playAudioStream does not handle this type of audio on this system." );
            return;
        }

        try {
            // Create a SourceDataLine for play back (throws LineUnavailableException).
            SourceDataLine dataLine = (SourceDataLine) AudioSystem.getLine( info );
            // System.out.println( "SourceDataLine class=" + dataLine.getClass() );

            // The line acquires system resources (throws LineAvailableException).
            dataLine.open( audioFormat,
                           audioFormat.getFrameSize() * FRAMES_PER_BUFFER);

            // Adjust the volume on the output line.
            if( dataLine.isControlSupported( FloatControl.Type.MASTER_GAIN ) ) {
                FloatControl volume = (FloatControl) dataLine.getControl( FloatControl.Type.MASTER_GAIN );
                volume.setValue( 6.0F );
            }

            // Allows the line to move data in and out to a port.
            dataLine.start();

            // Create a buffer for moving data from the audio stream to the line.
            int bufferSize = (int) audioFormat.getSampleRate() * audioFormat.getFrameSize();
            bufferSize =  audioFormat.getFrameSize() * FRAMES_PER_BUFFER;
            System.out.println("Buffer size: " + bufferSize);
            byte [] buffer = new byte[ bufferSize ];

            // Move the data until done or there is an error.
            try {
                int bytesRead = 0;
                while ( bytesRead >= 0 ) {
                    bytesRead = audioInputStream.read( buffer, 0, buffer.length );
                    if ( bytesRead >= 0 ) {
                        System.out.println( "Play.playAudioStream bytes read=" + bytesRead +
                        ", frame size=" + audioFormat.getFrameSize() + ", frames read=" + bytesRead / audioFormat.getFrameSize() );
                        // Odd sized sounds throw an exception if we don't write the same amount.
                        int framesWritten = dataLine.write( buffer, 0, bytesRead );
                    }
                } // while
            } catch ( IOException e ) {
                e.printStackTrace();
            }

            System.out.println( "Play.playAudioStream draining line." );
            // Continues data line I/O until its buffer is drained.
            dataLine.drain();

            System.out.println( "Play.playAudioStream closing line." );
            // Closes the data line, freeing any resources such as the audio device.
            dataLine.close();
        } catch ( LineUnavailableException e ) {
            e.printStackTrace();
        }
    } // playAudioStream

}

JavaSound 从哪里获得设备?

本章的第一个程序显示了调音台设备及其属性的列表。Java 是如何获得这些信息的?本节涵盖了 JDK 1.8,OpenJDK 大概也会类似。您将需要来自 Oracle 的 Java 源代码来跟踪这一点。或者,继续前进。

文件jre/lib/resources.jar包含 JRE 运行时使用的资源列表。这是一个 zip 文件,包含文件META-INF/services/javax.sound.sampled.spi.MixerProvider。在我的系统上,这个文件的内容如下:

# last mixer is default mixer
com.sun.media.sound.PortMixerProvider
com.sun.media.sound.DirectAudioDeviceProvider

com.sun.media.sound.PortMixerProvider在我系统上的文件java/media/src/share/native/com/sun/media/sound/PortMixerProvider.java中。它扩展了MixerProvider,并实现了Mixer.Info[] getMixerInfo等方法。这个类存储设备信息。

这个类完成的大部分工作实际上是由 C 文件java/media/src/share/native/com/sun/media/sound/PortMixerProvider.c中的本地方法执行的,它实现了PortMixerProvider类使用的两个方法nGetNumDevicesnNewPortMixerInfo。不幸的是,在这个 C 文件中找不到多少乐趣,因为它只是调用 C 函数PORT_GetPortMixerCountPORT_GetPortMixerDescription

有三个文件包含这些函数。

java/media/src/windows/native/com/sun/media/sound/PLATFORM_API_WinOS_Ports.c
java/media/src/solaris/native/com/sun/media/sound/PLATFORM_API_SolarisOS_Ports.c
java/media/src/solaris/native/com/sun/media/sound/PLATFORM_API_LinuxOS_ALSA_Ports.c

在文件PLATFORM_API_LinuxOS_ALSA_Ports.c中,你会看到第五章中描述的对 ALSA 的函数调用。这些调用填充了供 JavaSound 使用的 ALSA 设备的信息。

结论

Java Sound API 是有据可查的。我在这里展示了四个简单的程序,但是更复杂的程序也是可能的。简要讨论了与基本音响系统的联系。

十、GStreamer

GStreamer 是一个组件库,可以在复杂的管道中连接在一起。它可以用于过滤、转换格式和混音。它可以处理音频和视频格式,但本章只讨论音频。它着眼于使用 GStreamer 的用户级机制,以及链接 GStreamer 组件的编程模型。为编写新组件提供了参考。

资源

以下是一些资源:

概观

GStreamer 使用管道模型来连接元素,这些元素是源、过滤器和接收器。图 10-1 为模型。

A435426_1_En_10_Fig1_HTML.gif

图 10-1。

GStreamer pipeline model

每个元素有零个或多个焊盘,可以是产生数据的源焊盘,也可以是消耗数据的宿焊盘,如图 10-2 所示。

A435426_1_En_10_Fig2_HTML.gif

图 10-2。

GStreamer source and sink pads

pad 可以是静态的,也可以是响应事件而动态创建或销毁的。例如,要处理一个容器文件(如 MP4 ),元素必须先读取足够多的文件内容,然后才能确定所包含对象的格式,如 H.264 视频。完成后,它可以为下一阶段创建一个源 pad 来使用数据。

GStreamer 并不局限于像命令语言bash这样的线性流水线。例如,解复用器可能需要将音频和视频分开,并分别进行处理,如图 10-3 所示。

A435426_1_En_10_Fig3_HTML.gif

图 10-3。

Complex GStreamer pipeline

元素遵循状态模型,如下所示:

  • GST_STATE_NULL
  • GST_STATE_READY
  • GST_STATE_PAUSED
  • GST_STATE_PLAYING

通常会创建元素并将其从NULL移动到PLAYING。其他状态可以进行更精确的控制。

元素还可以生成包含数据流状态信息的事件。事件通常在内部处理,但也可能被监视,例如表示数据流结束或数据流格式的事件。

插件是可加载的代码块。通常,一个插件包含单个元素的实现,但它可能包含更多元素。

每个 pad 都有一个相关的功能列表。每个功能都是关于 pad 可以处理什么的陈述。这包括有关数据类型(例如,audio/raw)、格式(S32LE、U32LE、S16LE、U16LE 等)、数据速率(例如,每秒 1-2147483647 位)等信息。当源焊盘链接到宿焊盘时,这些能力用于确定元件将如何通信。

命令行处理

处理 GStreamer 有三个层次:通过使用命令行,通过编写 C 程序(或者 Python、Perl、C++等等)链接元素,或者通过编写新元素。本节介绍命令行工具。

商品及服务税-检查

不带参数的命令gst-inspect(在我的 Ubuntu 系统上,gst-inspect-1.0)显示了插件列表、它们的元素和简短描述。简要摘录如下:

...
audiomixer:  liveadder: AudioMixer
audioparsers:  aacparse: AAC audio stream parser
audioparsers:  ac3parse: AC3 audio stream parser
audioparsers:  amrparse: AMR audio stream parser
audioparsers:  dcaparse: DTS Coherent Acoustics audio stream parser
audioparsers:  flacparse: FLAC audio parser
audioparsers:  mpegaudioparse: MPEG1 Audio Parser
audioparsers:  sbcparse: SBC audio parser
audioparsers:  wavpackparse: Wavpack audio stream parser
audiorate:  audiorate: Audio rate adjuster
...

这表明插件audioparsers包含许多元素,比如aacparse,它是一个“AAC 音频流解析器”

当以插件作为参数运行时,gst-inspect显示了关于插件的更多细节。

$gst-inspect-1.0 audioparsers
Plugin Details:
  Name                     audioparsers
  Description              Parsers for various audio formats
  Filename                 /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstaudioparsers.so
  Version                  1.8.1
  License                  LGPL
  Source module            gst-plugins-good
  Source release date      2016-04-20
  Binary package           GStreamer Good Plugins (Ubuntu)
  Origin URL               https://launchpad.net/distros/ubuntu/+source/gst-plugins-good1.0

  aacparse: AAC audio stream parser
  amrparse: AMR audio stream parser
  ac3parse: AC3 audio stream parser
  dcaparse: DTS Coherent Acoustics audio stream parser
  flacparse: FLAC audio parser
  mpegaudioparse: MPEG1 Audio Parser
  sbcparse: SBC audio parser
  wavpackparse: Wavpack audio stream parser

  8 features:
  +-- 8 elements

特别要注意的是,它来自模块gst-plugins-good。插件按照稳定性、许可等进行分类。

当以元素作为参数运行时,gst-inspect显示了关于元素的大量信息。

$gst-inspect-1.0 aacparse
Factory Details:
  Rank                     primary + 1 (257)
  Long-name                AAC audio stream parser
  Klass                    Codec/Parser/Audio
  Description              Advanced Audio Coding parser
  Author                   Stefan Kost <stefan.kost@nokia.com>

Plugin Details:
  Name                     audioparsers
  Description              Parsers for various audio formats
  Filename                 /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstaudioparsers.so
  Version                  1.8.1
  License                  LGPL
  Source module            gst-plugins-good
  Source release date      2016-04-20
  Binary package           GStreamer Good Plugins (Ubuntu)
  Origin URL               https://launchpad.net/distros/ubuntu/+source/gst-plugins-good1.0

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstBaseParse
                         +----GstAacParse

Pad Templates:
  SINK template: 'sink'
    Availability: Always
    Capabilities:
      audio/mpeg
            mpegversion: { 2, 4 }

  SRC template: 'src'
    Availability: Always
    Capabilities:
      audio/mpeg
                 framed: true
            mpegversion: { 2, 4 }
          stream-format: { raw, adts, adif, loas }

Element Flags:
  no flags set

Element Implementation:
  Has change_state() function: gst_base_parse_change_state

Element has no clocking capabilities.
Element has no URI handling capabilities.

Pads:
  SINK: 'sink'
    Pad Template: 'sink'
  SRC: 'src'
    Pad Template: 'src'

Element Properties:
  name                : The name of the object
                        flags: readable, writable
                        String. Default: "aacparse0"
  parent              : The parent of the object
                        flags: readable, writable
                        Object of type "GstObject"
  disable-passthrough : Force processing (disables passthrough)
                        flags: readable, writable
                        Boolean. Default: false

这表明它可以采用音频/mpeg 版本 2 或 4,并将数据转换为各种格式的音频/mpeg 版本 2 或 4。

GST-发现者

命令gst-discoverer(在我的系统gst-discoverer-1.0上)可以用来给出关于资源的信息,比如文件或者 URIs。在一个名为audio_01.ogg的音频文件上,它给出了以下信息:

$gst-discoverer-1.0 enigma/audio_01.ogg
Analyzing file:enigma/audio_01.ogg
Done discovering file:enigma/audio_01.ogg

Topology:
  container: Ogg
    audio: Vorbis

Properties:
  Duration: 0:02:03.586666666
  Seekable: yes
  Tags:
      encoder: Xiph.Org libVorbis I 20020717
      encoder version: 0
      audio codec: Vorbis
      nominal bitrate: 112001
      bitrate: 112001
      container format: Ogg

GST-设备-监视器

该命令可以提供关于系统中设备的大量信息:

$gst-device-monitor-1.0
Probing devices...

Device found:

        name  : Monitor of Built-in Audio Digital Stereo (HDMI)
        class : Audio/Source
        caps  : audio/x-raw, format=(string){ S16LE, S16BE, F32LE, F32BE, S32LE, S32BE, S24LE, S24BE, S24_32LE, S24_32BE, U8 }, layout=(string)interleaved, rate=(int)[ 1, 2147483647 ], channels=(int)[ 1, 32 ];
                audio/x-alaw, rate=(int)[ 1, 2147483647 ], channels=(int)[ 1, 32 ];
                audio/x-mulaw, rate=(int)[ 1, 2147483647 ], channels=(int)[ 1, 32 ];
        properties:
                device.description = "Monitor\ of\ Built-in\ Audio\ Digital\ Stereo\ \(HDMI\)"
                device.class = monitor
                alsa.card = 0
                alsa.card_name = "HDA\ Intel\ HDMI"
                alsa.long_card_name = "HDA\ Intel\ HDMI\ at\ 0xf7214000\ irq\ 52"
                alsa.driver_name = snd_hda_intel
                device.bus_path = pci-0000:00:03.0
                sysfs.path = /devices/pci0000:00/0000:00:03.0/sound/card0
                device.bus = pci
                device.vendor.id = 8086
                device.vendor.name = "Intel\ Corporation"
                device.product.id = 160c
                device.product.name = "Broadwell-U\ Audio\ Controller"
                device.form_factor = internal
                device.string = 0
                module-udev-detect.discovered = 1
                device.icon_name = audio-card-pci
...

这是关于我的 HDMI 显示器的音频功能的大量信息,然后是关于我的其他设备的音频和视频功能的其他信息。

商品及服务税-播放

这个程序是一站式播放各种媒体文件和 URIs,如下:

      $gst-play-1.0 enigma/audio_01.ogg

商品及服务税-推出

gst-launch程序允许你建立一个命令管道来处理媒体数据。格式如下:

      gst-launch <elmt> [<args>] ! <elmt> [<args>] ! ...

例如,要通过 ALSA 播放 WAV 文件,请使用以下命令:

      $gst-launch-1.0 filesrc location=enigma/audio_01.wav ! wavparse ! alsasink

使用 GStreamer 管道最困难的部分似乎是选择合适的插件。这看起来有点像艺术。请参阅位于 http://wiki.oz9aec.net/index.php/Gstreamer_cheat_sheet 的 GStreamer 备忘单以获取帮助。

例如,Ogg 文件是一种容器格式,通常包含 Vorbis 音频流和 Theora 视频流(尽管它们可以包含其他数据格式)。它们播放音频或视频,或者两者都播放,必须使用解复用器从容器中提取流,解码,然后播放。播放音频有多种方式,包括以下三种:

    $gst-launch-1.0 filesrc location=enigma/audio_01.ogg ! oggdemux ! vorbisdec ! audioconvert ! alsasink

    $gst-launch-1.0 filesrc location=enigma/audio_01.ogg ! oggdemux ! vorbisdec ! autoaudiosink

    $gst-launch-1.0 uridecodebin uri=file:enigma/audio_01.ogg ! audioconvert ! autoaudiosink

GStreamer 管道的语法允许将一个管道分成多个管道,例如管理音频和视频流。这在 GStreamer 的在线文档中有所介绍。

编程

同样的管道原则也适用于gst-launch,但是当然在 C 编程级别有更多的管道需要关注。以下来自 http://docs.gstreamer.com/display/GstSDK/Basic+tutorials 的 GStreamer SDK 基础教程的程序与最后一个gst-launch示例($gst-launch-1.0 uridecodebin uri=... ! audioconvert ! autoaudiosink)做的一样。

GStreamer 元素是通过如下调用创建的:

data.source = gst_element_factory_make ("uridecodebin", "source");

管道是用这个建造的:

data.pipeline = gst_pipeline_new ("test-pipeline")
gst_bin_add_many (GST_BIN (data.pipeline), data.source, data.convert , data.sink, NULL);

最终所有的元素都必须被连接起来。现在,convertsink可以与以下链接:

gst_element_link (data.convert, data.sink)

要播放的 URI 设置如下:

g_object_set (data.source, "uri", "http://docs.gstreamer.com/media/sintel_trailer-480p.webm", NULL);

数据源是一个容器;在我之前的例子中,它是一个 Ogg 容器,这里它是一个 web 媒体 URL。在读取足够的数据以确定数据格式和参数之前,这不会在数据源元素上创建源填充。因此,C 程序必须为pad-added添加一个事件处理程序,它是这样做的:

g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

当一个 pad 被添加到源时,pad_added_handler将被调用。这做了很多类型检查并得到了新的 pad,但最终完成了链接sourceconvert元素的关键步骤。

gst_pad_link (new_pad, sink_pad)

然后,应用通过将状态改变为PLAYING开始播放,并等待正常终止(GST_MESSAGE_EOS)或其他消息。

gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
bus = gst_element_get_bus (data.pipeline);
msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
        GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

代码的最后一部分进行清理。完整的程序如下:

#include <gst/gst.h>

/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
  GstElement *pipeline;
  GstElement *source;
  GstElement *convert;
  GstElement *sink;
} CustomData;

/* Handler for the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);

int main(int argc, char *argv[]) {
  CustomData data;
  GstBus *bus;
  GstMessage *msg;
  GstStateChangeReturn ret;
  gboolean terminate = FALSE;

  /* Initialize GStreamer */
  gst_init (&argc, &argv);

  /* Create the elements */
  data.source = gst_element_factory_make ("uridecodebin", "source");
  data.convert = gst_element_factory_make ("audioconvert", "convert");
  data.sink = gst_element_factory_make ("autoaudiosink", "sink");

  /* Create the empty pipeline */
  data.pipeline = gst_pipeline_new ("test-pipeline");

  if (!data.pipeline || !data.source || !data.convert || !data.sink) {
    g_printerr ("Not all elements could be created.\n");
    return -1;
  }

  /* Build the pipeline. Note that we are NOT linking the source at this
   * point. We will do it later. */
  gst_bin_add_many (GST_BIN (data.pipeline), data.source, data.convert , data.sink, NULL);
  if (!gst_element_link (data.convert, data.sink)) {
    g_printerr ("Elements could not be linked.\n");
    gst_object_unref (data.pipeline);
    return -1;
  }

  /* Set the URI to play */
  g_object_set (data.source, "uri", "http://docs.gstreamer.com/media/sintel_trailer-480p.webm", NULL);

  /* Connect to the pad-added signal */
  g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

  /* Start playing */
  ret = gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
  if (ret == GST_STATE_CHANGE_FAILURE) {
    g_printerr ("Unable to set the pipeline to the playing state.\n");
    gst_object_unref (data.pipeline);
    return -1;
  }

  /* Listen to the bus */
  bus = gst_element_get_bus (data.pipeline);
  do {
    msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
        GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

    /* Parse message */
    if (msg != NULL) {
      GError *err;
      gchar *debug_info;

      switch (GST_MESSAGE_TYPE (msg)) {
        case GST_MESSAGE_ERROR:
          gst_message_parse_error (msg, &err, &debug_info);
          g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
          g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
          g_clear_error (&err);
          g_free (debug_info);
          terminate = TRUE;
          break;
        case GST_MESSAGE_EOS:
          g_print ("End-Of-Stream reached.\n");
          terminate = TRUE;
          break;
        case GST_MESSAGE_STATE_CHANGED:
          /* We are only interested in state-changed messages from the pipeline */
          if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data.pipeline)) {
            GstState old_state, new_state, pending_state;
            gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
            g_print ("Pipeline state changed from %s to %s:\n",
                gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
          }
          break;
        default:
          /* We should not reach here */
          g_printerr ("Unexpected message received.\n");
          break;
      }
      gst_message_unref (msg);
    }
  } while (!terminate);

  /* Free resources */
  gst_object_unref (bus);
  gst_element_set_state (data.pipeline, GST_STATE_NULL);
  gst_object_unref (data.pipeline);
  return 0;
}

/* This function will be called by the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
  GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink");
  GstPadLinkReturn ret;
  GstCaps *new_pad_caps = NULL;
  GstStructure *new_pad_struct = NULL;
  const gchar *new_pad_type = NULL;

  g_print ("Received new pad '%s' from '%s':\n", GST_PAD_NAME (new_pad), GST_ELEMENT_NAME (src));

  /* If our converter is already linked, we have nothing to do here */
  if (gst_pad_is_linked (sink_pad)) {
    g_print ("  We are already linked. Ignoring.\n");
    goto exit;
  }

  /* Check the new pad's type */
  new_pad_caps = gst_pad_get_caps (new_pad);
  new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
  new_pad_type = gst_structure_get_name (new_pad_struct);
  if (!g_str_has_prefix (new_pad_type, "audio/x-raw")) {
    g_print ("  It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type);
    goto exit;
  }

  /* Attempt the link */
  ret = gst_pad_link (new_pad, sink_pad);
  if (GST_PAD_LINK_FAILED (ret)) {
    g_print ("  Type is '%s' but link failed.\n", new_pad_type);
  } else {
    g_print ("  Link succeeded (type '%s').\n", new_pad_type);
  }

exit:
  /* Unreference the new pad's caps, if we got them */
  if (new_pad_caps != NULL)
    gst_caps_unref (new_pad_caps);

  /* Unreference the sink pad */
  gst_object_unref (sink_pad);
}

编写插件

编写新的 GStreamer 插件是一项艰巨的任务。位于 https://gstreamer.freedesktop.org/data/doc/gstreamer/head/pwg/html/index.html 的文档“GStreamer 作者指南”对此给出了广泛的建议。

结论

本章从命令行和一个示例 C 程序两个方面介绍了 GStreamer 的使用。有一个庞大的可用插件列表,可以满足音频/视频开发人员的许多需求。我只是触及了 GStreamer 的皮毛,它还有许多其他特性,包括与 GTK 工具包的集成。

十一、libao

根据 libao 文档( www.xiph.org/ao/doc/overview.html ),“libao 旨在使使用各种音频设备和库进行简单的音频输出变得容易。由于这个原因,复杂的音频控制功能丢失了,并且可能永远不会被添加。然而,如果你只是想打开任何可用的音频设备并播放声音,libao 应该没问题。”

资源

查看以下内容:

libao

libao 是一个极小的图书馆;它基本上只是播放音频数据。它不能解码任何标准的文件格式:不支持 WAV、MP3、Vorbis 等等。您必须配置位、通道、速率和字节格式的格式参数,然后将适当的数据发送到设备。它的主要用途是输出 PCM 数据,可以在编解码器解码后使用,或者播放正弦波等简单声音。

下面是一个来自 libao 网站的简单例子,播放一秒钟的正弦音调:

/*
 *
 * ao_example.c
 *
 *     Written by Stan Seibert - July 2001
 *
 * Legal Terms:
 *
 *     This source file is released into the public domain.  It is
 *     distributed without any warranty; without even the implied
 *     warranty * of merchantability or fitness for a particular
 *     purpose.
 *
 * Function:
 *
 *     This program opens the default driver and plays a 440 Hz tone for
 *     one second.
 *
 * Compilation command line (for Linux systems):
 *
 *     gcc -lao -ldl -lm -o ao_example ao_example.c
 *
 */

#include <stdio.h>
#include <ao/ao.h>
#include <math.h>

#define BUF_SIZE 4096

int main(int argc, char **argv)
{
        ao_device *device;
        ao_sample_format format;
        int default_driver;
        char *buffer;
        int buf_size;
        int sample;
        float freq = 440.0;
        int i;

        /* -- Initialize -- */

        fprintf(stderr, "libao example program\n");

        ao_initialize();

        /* -- Setup for default driver -- */

        default_driver = ao_default_driver_id();

        memset(&format, 0, sizeof(format));
        format.bits = 16;
        format.channels = 2;
        format.rate = 44100;
        format.byte_format = AO_FMT_LITTLE;

        /* -- Open driver -- */
        device = ao_open_live(default_driver, &format, NULL /* no options */);
        if (device == NULL) {
                fprintf(stderr, "Error opening device.\n");
                return 1;
        }

        /* -- Play some stuff -- */
        buf_size = format.bits/8 * format.channels * format.rate;
        buffer = calloc(buf_size,
                        sizeof(char));

        for (i = 0; i < format.rate; i++) {
                sample = (int)(0.75 * 32768.0 *
                        sin(2 * M_PI * freq * ((float) i/format.rate)));

                /* Put the same stuff in left and right channel */
                buffer[4*i] = buffer[4*i+2] = sample & 0xff;
                buffer[4*i+1] = buffer[4*i+3] = (sample >> 8) & 0xff;
        }
        ao_play(device, buffer, buf_size);

        /* -- Close and shutdown -- */
        ao_close(device);

        ao_shutdown();

  return (0);
}

结论

libao 并不复杂;这是一个基本的库,可以在任何可用的设备上播放声音。它将适合的情况下,你有一个已知的 PCM 格式的声音。

十二、 FFmpeg/Libav

根据“FFmpeg 初学者教程”( http://keycorner.org/pub/text/doc/ffmpegtutorial.htm ),FFmpeg 是一个完整的、跨平台的命令行工具,能够记录、转换和流式传输各种格式的数字音频和视频。它可以用来快速轻松地完成大多数多媒体任务,如音频压缩、音频/视频格式转换、从视频中提取图像等。

FFmpeg 由一组命令行工具和一组库组成,可用于将音频(和视频)文件从一种格式转换为另一种格式。它既可以在容器上工作,也可以在编解码器上工作。它不是为播放或录制音频而设计的;它更像是一个通用的转换工具。

资源

FFmpeg/Libav 之争

FFmpeg 开始于 2000 年,为处理多媒体数据提供库和程序。然而,在过去的几年里,开发人员之间发生了一些纠纷,导致了 2011 年 Libav 项目的分叉。从那以后,这两个项目一直在进行,几乎是并行的,并且经常互相借鉴。然而,形势依然严峻,似乎没有解决的可能。

这对开发者来说是不幸的。虽然程序通常可以在这两个系统之间移植,但有时在 API 和行为上存在差异。还有发行版支持的问题。多年来,Debian 及其衍生产品只支持 Libav,忽略了 FFmpeg。这已经改变了,现在两者都支持。参见“为什么 Debian 回到 FFmpeg”(https://lwn.net/Articles/650816/)对其中一些问题的讨论。

FFmpeg 命令行工具

主要的 FFmpeg 工具是ffmpeg本身。最简单的用途是作为从一种格式到另一种格式的转换器,如下所示:

        ffmpeg -i file.ogg file.mp3

这将把 Vorbis 编解码器数据的 Ogg 容器转换成 MP2 编解码器数据的 MPEG 容器。

Libav 的等价物是avconv,运行方式类似。

      avconv -i file.ogg file.mp3

在内部,ffmpeg使用模块流水线,如图 12-1 所示。

A435426_1_En_12_Fig1_HTML.gif

图 12-1。

FFmpeg/Libav pipeline (Source: http://ffmpeg.org/ffmpeg.html )

如果默认值不合适,可以使用选项设置多路复用器/多路分解器和解码器/编码器。

以下是其他命令:

  • ffprobe给出关于文件的信息。
  • 是一个简单的媒体播放器。
  • ffserver是媒体服务器。

设计

有许多库可用于 FFmpeg/Libav 编程。Libav 构建了以下库:

  • libavcodec 公司
  • libavdevice
  • libavfilter
  • libavformat
  • libavresample
  • 滑鹌

FFmepg 构建以下内容:

  • libavcodec 公司
  • libavdevice
  • libavfilter
  • libavformat
  • libavresample
  • 滑鹌
  • libpostproc
  • libswresample
  • libswscale

FFmpeg 中的额外库用于视频后处理和缩放。

使用这些系统都不是一个简单的过程。Libav 网站声明,“Libav 一直是一个非常实验性的、由开发者驱动的项目。它是许多多媒体项目中的关键组件,并且不断添加新功能。为了提供一个稳定的基础,主要版本每四到六个月削减一次,并至少维持两年。”

FFmpeg 网站声明,“FFmpeg 一直是一个非常实验性和开发者驱动的项目。它是许多多媒体项目中的关键组件,并且不断添加新功能。开发分支快照在 99%的时间里都工作得很好,所以人们不怕使用它们。”

我的经验是,这两个项目的“实验”性质导致了不稳定的核心 API,定期废弃和替换关键功能。比如libavcodec版本 56 中的函数avcodec_decode_audio现在升级到版本 4: avcodec_decode_audio4。甚至那个版本现在也在 FFmpeg 和 Libav 的上游版本(版本 57)中被弃用,取而代之的是在版本 56 中不存在的函数,比如avcodec_send_packet。除此之外,还有两个项目具有相同的目标和大体相同的 API,但并不总是如此。比如 FFmpeg 有swr_alloc_set_opts,而 Libav 用的是av_opt_set_int。此外,视听编解码器和容器本身也在不断发展。

这样做的结果是,互联网上的许多示例程序不再编译,不再使用废弃的 API,或者属于“其他”系统。这并不是要贬低两个成就高超的系统,只是希望不要这么乱。

解码 MP3 文件

以下程序将 MP3 文件解码为原始 PCM 文件。这是使用 FFmpeg/Libav 所能完成的最简单的任务,但不幸的是这并不简单。首先,你要注意你要处理的是一个编解码器,而不是一个包含编解码器的文件。这不是一个 FFmpeg/Libav 问题,而是一个一般性问题。

扩展名为.mpg.mp3的文件可能包含许多不同的格式。如果我对我拥有的一些文件运行命令file,我会得到不同的结果。

BST.mp3: MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
Beethoven_Fr_Elise.mp3: MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
Angel-no-vocal.mp3: Audio file with ID3 version 2.3.0
01DooWackaDoo.mp3: Audio file with ID3 version 2.3.0, \
    contains: MPEG ADTS, layer III, v1, 224 kbps, 44.1 kHz, JntStereo

前两个文件只包含一个编解码器,可以由下面的程序管理。第三和第四个文件是容器文件,包含 MPEG+ID3 数据。这些需要使用avformat函数来管理,例如av_read_frame 1

该程序基本上是 FFmpeg/Libav 源代码发行版中的一个标准示例。它基于 FFmpeg 源中的ffmpeg-3.2/doc/examples/decoding_encoding.c和 Libav 源中的libav-12/doc/examples/avcodec.c。顺便提一下,两个程序都使用了avcodec_decode_audio4,这在这两个上游版本中都被否决了,也没有替换函数avcodec_send_packet的例子。

更严重的问题是,MP3 文件越来越多地使用平面格式。在这种情况下,不同的通道位于不同的平面。FFmpeg/Libav 函数avcodec_decode_audio4通过将每个平面放置在单独的数据阵列中来正确处理这一问题,但当它作为 PCM 数据输出时,平面必须交错。示例中没有这样做,这可能会导致 PCM 数据不正确(大量咔嗒声,然后是半速音频)。

相关的 FFmpeg 功能如下:

  • 注册所有可能的多路复用器、多路分解器和协议。
  • avformat_open_input:打开输入流。
  • av_find_stream_info:提取流信息。
  • av_init_packet:设定数据包中的默认值。
  • avcodec_find_decoder:找到合适的解码器。
  • avcodec_alloc_context3:设置主数据结构的默认值。
  • avcodec_open2:打开解码器。
  • fread:FFmpeg 处理循环从数据流中一次读取一个缓冲区。
  • avcodec_decode_audio4:将音频帧解码成原始音频数据。

其余的代码交错数据流以输出到 PCM 文件。生成的文件可以通过以下方式播放:

      aplay -c 2 -r 44100 /tmp/test.sw -f S16_LE

该计划如下:

/*
 * copyright (c) 2001 Fabrice Bellard
 *
 * This file is part of Libav.
 *
 * Libav is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * Libav 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with Libav; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

// From http://code.haskell.org/∼thielema/audiovideo-example/cbits/
// Adapted to version version 2.8.6-1ubuntu2 by Jan Newmarch

/**
 * @file
 * libavcodec API use example.
 *
 * @example libavcodec/api-example.c
 * Note that this library only handles codecs (mpeg, mpeg4, etc...),
 * not file formats (avi, vob, etc...). See library 'libavformat' for the
 * format handling
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#ifdef HAVE_AV_CONFIG_H
#undef HAVE_AV_CONFIG_H
#endif

#include "libavcodec/avcodec.h"
#include <libavformat/avformat.h>

#define INBUF_SIZE 4096
#define AUDIO_INBUF_SIZE 20480
#define AUDIO_REFILL_THRESH 4096

void die(char *s) {
    fputs(s, stderr);
    exit(1);
}

/*
 * Audio decoding.
 */
static void audio_decode_example(AVFormatContext* container,
                                 const char *outfilename, const char *filename)
{
    AVCodec *codec;
    AVCodecContext *context = NULL;
    int len;
    FILE *f, *outfile;
    uint8_t inbuf[AUDIO_INBUF_SIZE + FF_INPUT_BUFFER_PADDING_SIZE];
    AVPacket avpkt;
    AVFrame *decoded_frame = NULL;
    int num_streams = 0;
    int sample_size = 0;

    av_init_packet(&avpkt);

    printf("Audio decoding\n");

    int stream_id = -1;

    // To find the first audio stream. This process may not be necessary
    // if you can gurarantee that the container contains only the desired
    // audio stream
    int i;
    for (i = 0; i < container->nb_streams; i++) {
        if (container->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
            stream_id = i;
            break;
        }
    }

    /* find the appropriate audio decoder */
    AVCodecContext* codec_context = container->streams[stream_id]->codec;
    codec = avcodec_find_decoder(codec_context->codec_id);
    if (!codec) {
        fprintf(stderr, "codec not found\n");
        exit(1);
    }

    context = avcodec_alloc_context3(codec);;

    /* open it */
    if (avcodec_open2(context, codec, NULL) < 0) {
        fprintf(stderr, "could not open codec\n");
        exit(1);
    }

    f = fopen(filename, "rb");
    if (!f) {
        fprintf(stderr, "could not open %s\n", filename);
        exit(1);
    }
    outfile = fopen(outfilename, "wb");
    if (!outfile) {
        av_free(context);
        exit(1);
    }

    /* decode until eof */
    avpkt.data = inbuf;
    avpkt.size = fread(inbuf, 1, AUDIO_INBUF_SIZE, f);

    while (avpkt.size > 0) {
        int got_frame = 0;

        if (!decoded_frame) {
            if (!(decoded_frame = av_frame_alloc())) {
                fprintf(stderr, "out of memory\n");
                exit(1);
            }
        } else {
            av_frame_unref(decoded_frame);
        }
        printf("Stream idx %d\n", avpkt.stream_index);

        len = avcodec_decode_audio4(context, decoded_frame, &got_frame, &avpkt);
        if (len < 0) {
            fprintf(stderr, "Error while decoding\n");
            exit(1);
        }
        if (got_frame) {
            printf("Decoded frame nb_samples %d, format %d\n",
                   decoded_frame->nb_samples,
                   decoded_frame->format);
            if (decoded_frame->data[1] != NULL)
                printf("Data[1] not null\n");
            else
                printf("Data[1] is null\n");
            /* if a frame has been decoded, output it */
            int data_size = av_samples_get_buffer_size(NULL, context->channels,
                                                       decoded_frame->nb_samples,
                                                       context->sample_fmt, 1);
            // first time: count the number of  planar streams
            if (num_streams == 0) {
                while (num_streams < AV_NUM_DATA_POINTERS &&
                       decoded_frame->data[num_streams] != NULL)
                    num_streams++;
                printf("Number of streams %d\n", num_streams);
            }

            // first time: set sample_size from 0 to e.g 2 for 16-bit data
            if (sample_size == 0) {
                sample_size =
                    data_size / (num_streams * decoded_frame->nb_samples);
            }

            int m, n;
            for (n = 0; n < decoded_frame->nb_samples; n++) {
                // interleave the samples from the planar streams
                for (m = 0; m < num_streams; m++) {
                    fwrite(&decoded_frame->data[m][n*sample_size],
                           1, sample_size, outfile);
                }
            }
        }
        avpkt.size -= len;
        avpkt.data += len;
        if (avpkt.size < AUDIO_REFILL_THRESH) {
            /* Refill the input buffer, to avoid trying to decode
             * incomplete frames. Instead of this, one could also use
             * a parser, or use a proper container format through
             * libavformat. */
            memmove(inbuf, avpkt.data, avpkt.size);
            avpkt.data = inbuf;
            len = fread(avpkt.data + avpkt.size, 1,
                        AUDIO_INBUF_SIZE - avpkt.size, f);
            if (len > 0)
                avpkt.size += len;
        }
    }

    fclose(outfile);
    fclose(f);

    avcodec_close(context);
    av_free(context);
    av_free(decoded_frame);
}

int main(int argc, char **argv)
{
    const char *filename = "Beethoven_Fr_Elise.mp3";
    AVFormatContext *pFormatCtx = NULL;

    if (argc == 2) {
        filename = argv[1];
    }

    // Register all formats and codecs
    av_register_all();
    if(avformat_open_input(&pFormatCtx, filename, NULL, NULL)!=0) {
        fprintf(stderr, "Can't get format of file %s\n", filename);
        return -1; // Couldn't open file
    }
    // Retrieve stream information
    if(avformat_find_stream_info(pFormatCtx, NULL)<0)
        return -1; // Couldn't find stream information
    av_dump_format(pFormatCtx, 0, filename, 0);
    printf("Num streams %d\n", pFormatCtx->nb_streams);
    printf("Bit rate %d\n", pFormatCtx->bit_rate);
    audio_decode_example(pFormatCtx, "/tmp/test.sw", filename);

    return 0;
}

结论

本章简要介绍了 FFmpeg/Libav,查看了 libavcodec 库。FFmpeg 和 Libav 要复杂得多,它们可以进行复杂得多的转换。此外,他们还可以进行视频处理,这在第十五章中有说明。

Footnotes 1

第十五章和第二十一章中给出了 av_read_frame 的示例。

十三、OpenMAXIL

OpenMAX 是 Khronos Group 为低性能设备设计的音频和视频开放标准。卡的供应商被期望生产实现。一般的 Linux 实现方式很少,但是 Broadcom 已经实现了其中一个规范(OpenMAX IL),它的芯片被用于 Raspberry Pi。其他 Khronos 规范(OpenMAX AL 和 OpenSL ES)已经在 Android 设备中实现,可通过原生开发套件(NDK)访问,但这些并不打算直接使用;它们只能通过 Java APIs 使用。本书不讨论它们。本章仅讨论 OpenMAX IL。

资源

以下是一些资源:

引用

以下是一些引述:

OpenMAX 概念

OpenMAX IL API 与 OpenMAX AL 的 API 截然不同。基本概念是组件,即某种类型的音频/视频(或其他)处理单元,如音量控制、混音器或输出设备。每个组件有零个或多个输入和输出端口,每个端口可以有一个或多个携带数据的缓冲器。

OpenMAX IL 通常由某种 A/V 框架使用,如 OpenMAX AL。除了 OpenMAX AL,目前还有一个 GStreamer 插件在底层使用 OpenMAX IL。但是也可以构建独立的应用,直接调用 OpenMAX IL API。总的来说,这些都被称为 IL 客户端。

OpenMAX IL API 很难直接使用。错误消息经常是无用的,线程会毫无解释地阻塞,直到一切都完全正确,静默阻塞不会给你任何关于什么是不正确的线索。此外,我必须处理的例子并没有完全正确地遵循规范,这会导致大量的时间浪费。

OpenMAX IL 组件使用缓冲区来传送数据。组件通常会处理来自输入缓冲区的数据,并将其放在输出缓冲区。这种处理对 API 是不可见的,因此它允许供应商在硬件或软件中实现组件,构建在其他 A/V 组件之上,等等。OpenMAX IL 提供了设置和获取组件参数、调用组件上的标准函数或从组件中获取数据的机制。

虽然一些 OpenMAX IL 调用是同步的,但是那些可能需要大量处理的调用是异步的,通过回调函数传递结果。这自然会导致多线程处理模型,尽管 OpenMAX IL 并不明显使用任何线程库,并且应该不知道 IL 客户端如何使用线程。Bellagio 示例使用 pthreads,而 Broadcom 的 Raspberry Pi 示例使用 Broadcom 的 video core OS(VCO)线程( https://github.com/raspberrypi/userland/blob/master/interface/vcos/vcos_semaphore.h )。

有两种机制可以让数据进出组件。第一个是 IL 客户端调用组件的地方。所有组件都需要支持此机制。第二种是在两个组件之间建立一个隧道,让数据沿着共享缓冲区流动。支持这种机制不需要组件。

OpenMAX IL 组件

OpenMAX IL in 1.1.2 lists 中列出了许多标准组件,包括(对于音频)解码器、编码器、混合器、读取器、渲染器、写入器、捕获器和处理器。一个 IL 客户端通过调用OMX_GetHandle()获得这样一个组件,并传入组件的名称。这是一个问题:组件没有标准的名称。

1.1.2 规范说,“由于组件是按名称请求的,因此定义了命名约定。OpenMAX IL 组件名是以零结尾的字符串,格式如下:OMX.<vendor_name>.<vendor_specified_convention>,例如OMX.CompanyABC.MP3Decoder.productXYZ。不同供应商的组件名称之间没有标准化。”

在这一点上,您必须查看当前可用的实现,因为这种标准化的缺乏会导致即使是最基本的程序也存在差异。

履行

以下是实现。

树莓派

Raspberry Pi 有一个 Broadcom 图形处理单元(GPU),Broadcom 支持 OpenMAX IL。构建应用所需的包含文件在/opt/vc/include/IL/opt/vc/include/opt/vc/include/interface/vcos/pthreads中。需要链接的库在/opt/vc/lib目录下,分别是openmaxilbcm_host

Broadcom 库需要调用额外的代码以及标准的 OpenMAX IL 函数。此外,OpenMAX IL 还有许多(合法的)扩展,这些扩展在规范或其他实现中是找不到的。这些在/opt/vc/include/IL/OMX_Broadcom.h中有描述。由于这些原因,我定义了RASPBERRY_PI来允许这些被处理。

例如,listcomponents.c的编译行如下:

cc -g -DRASPBERRY_PI -I /opt/vc/include/IL -I /opt/vc/include \
   -I /opt/vc/include/interface/vcos/pthreads \
   -o listcomponents listcomponents.c \
   -L /opt/vc/lib -l openmaxil -l bcm_host

Broadcom 实现是闭源的。它似乎是其 GPU API 的一个薄薄的包装,Broadcom 不会发布该 API 的任何细节。这意味着您不能扩展组件集或支持的编解码器,因为没有关于如何构建新组件的详细信息。虽然组件的设置是合理的,但目前除了 PCM 之外不支持编解码器,也不支持非 GPU 硬件,如 USB 声卡。

OtherCrashOverride ( www.raspberrypi.org/phpBB3/viewtopic.php?f=70&t=33101&p=287590#p287590 )说他已经设法让 Broadcom 组件在 LIM 实现下运行,但我还没有证实这一点。

就音频而言,Raspberry Pi 上的实现非常弱,因为所有音频解码都要在软件中完成,并且它只能播放 PCM 数据。视频更令人印象深刻,在我的书《Raspberry Pi GPU 音频视频编程》中有所论述。

百乐宫(美国酒店名)

Bellagio 库不需要额外的代码或任何扩展。有一些小错误,所以我定义BELLAGIO来处理它们。我从源代码构建但没有安装,所以 includes 和 libraries 在一个有趣的地方。我的编译代码如下:

cc  -g -DBELLAGIO -I ../libomxil-bellagio-0.9.3/include/ \
    -o listcomponents listcomponents.c \
    -L ../libomxil-bellagio-0.9.3/src/.libs -l omxil-bellagio

这是运行时的代码行:

export LD_LIBRARY_PATH=../libomxil-bellagio-0.9.3/src/.libs/
./listcomponents

Bellagio 代码是开源的。

潜象存储器(Latent Image Memory 的缩写)

下载 1.1 版本很麻烦,因为 1.1 下载使用了已经消失的 Git repo(截至 2016 年 11 月)。相反,您必须运行以下命令:

  git clone git://limoa.git.sourceforge.net/gitroot/limoa/limoi-components
  git clone git://limoa.git.sourceforge.net/gitroot/limoa/limoi-core
  git clone git://limoa.git.sourceforge.net/gitroot/limoa/limoi-plugins
  git clone git://limoa.git.sourceforge.net/gitroot/limoa/limutil
  git clone git://limoa.git.sourceforge.net/gitroot/limoa/manifest

您必须将构建中的root.mk文件复制到包含所有代码的顶层文件夹中,并将其重命名为Makefileroot.readme文件有构建指令。感谢 OtherCrashOverride ( www.raspberrypi.org/phpBB3/viewtopic.php?f=70&t=33101&p=286516#p286516 )的这些指令。

建造图书馆遇到了一些小问题。我不得不注释掉一个视频文件中的几行,因为它引用了不存在的结构字段,并且不得不从一个Makefile.am中移除-Werrors,否则关于未使用变量的警告将会中止编译。

库构建将文件放在我的HOME中的新目录中。到目前为止,我在实现中发现了一些小错误。我的编译代码如下:

cc -g -DLIM -I ../../lim-omx-1.1/LIM/limoi-core/include/ \
   -o listcomponents listcomponents.c \
   -L /home/newmarch/osm-build/lib/ -l limoa -l limoi-core

以下是运行时的代码行:

export LD_LIBRARY_PATH=/home/newmarch/osm-build/lib/
./listcomponents

LIM 代码是开源的。

硬件支持的版本

您可以在 open max IL Conformant Products(www.khronos.org/conformance/adopters/conformant-products#openmaxil)找到硬件支持的版本列表。

组件的实现

Bellagio 库(你需要源码包才能看到这些文件)在其README中只列出了两个音频组件。

  • OMX 音量控制
  • OMX 混音器组件

它们的名字(来自示例测试文件)分别是OMX.st.volume.componentOMX.st.audio.mixer。百乐宫背后的公司是意法半导体( www.st.com/internet/com/home/home.jsp ),这就解释了st

Raspberry Pi 上使用的 Broadcom OpenMAX IL 实现有更好的文档记录。如果您下载 Raspberry Pi 的固件主文件,它会在documentation/ilcomponents目录中列出 IL 组件。这列出了组件audio_captureaudio_decodeaudio_encodeaudio_lowpoweraudio_mixeraudio_processoraudio_renderaudio_splitter

Broadcom 示例中的许多 OpenMAX IL 函数调用都隐藏在 Broadcom 便利函数中,如下所示:

ilclient_create_component(st->client, &st->audio_render,
                         "audio_render",
                         ILCLIENT_ENABLE_INPUT_BUFFERS | ILCLIENT_DISABLE_ALL_PORTS);

这围绕着OMX_GetHandle()。但是至少ilclient.h声明,“在传递给 IL 核心之前,所提供的组件名称会自动加上前缀OMX.broadcom.”所以,你可以断定真名是,比如OMX.broadcom.audio_render,等等。

有一种简单的方法可以通过编程获得受支持的组件。首先用OMX_init()初始化 OpenMAX 系统,然后调用OMX_ComponentNameEnum()。对于连续的索引值,它每次都返回一个唯一的名称,直到最后返回一个错误值OMX_ErrorNoMore

每个组件可以支持多个角色。这些都是由OMX_GetRolesOfComponent给出的。1.1 规范在第 8.6 节“标准音频组件”中列出了音频组件的类别和相关角色 LIM 库匹配这些,而 Bellagio 和 Broadcom 不匹配。

下面的程序是listcomponents.c:

#include <stdio.h>
#include <stdlib.h>

#include <OMX_Core.h>

#ifdef RASPBERRY_PI
#include <bcm_host.h>
#endif

OMX_ERRORTYPE err;

//extern OMX_COMPONENTREGISTERTYPE OMX_ComponentRegistered[];

void listroles(char *name) {
    int n;
    OMX_U32 numRoles;
    OMX_U8 *roles[32];

    /* get the number of roles by passing in a NULL roles param */
    err = OMX_GetRolesOfComponent(name, &numRoles, NULL);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Getting roles failed\n", 0);
        exit(1);
    }
    printf("  Num roles is %d\n", numRoles);
    if (numRoles > 32) {
        printf("Too many roles to list\n");
        return;
    }

    /* now get the roles */
    for (n = 0; n < numRoles; n++) {
        roles[n] = malloc(OMX_MAX_STRINGNAME_SIZE);
    }
    err = OMX_GetRolesOfComponent(name, &numRoles, roles);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Getting roles failed\n", 0);
        exit(1);
    }
    for (n = 0; n < numRoles; n++) {
        printf("    role: %s\n", roles[n]);
        free(roles[n]);
    }

    /* This is in version 1.2
    for (i = 0; OMX_ErrorNoMore != err; i++) {
        err = OMX_RoleOfComponentEnum(role, name, i);
        if (OMX_ErrorNone == err) {
            printf("   Role of omponent is %s\n", role);
        }
    }
    */
}

int main(int argc, char** argv) {

    int i;
    unsigned char name[OMX_MAX_STRINGNAME_SIZE];

# ifdef RASPBERRY_PI
    bcm_host_init();
# endif

    err = OMX_Init();
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "OMX_Init() failed\n", 0);
        exit(1);
    }

    err = OMX_ErrorNone;
    for (i = 0; OMX_ErrorNoMore != err; i++) {
        err = OMX_ComponentNameEnum(name, OMX_MAX_STRINGNAME_SIZE, i);
        if (OMX_ErrorNone == err) {
            printf("Component is %s\n", name);
            listroles(name);
        }
    }
    printf("No more components\n");

    /*
    i= 0 ;
    while (1) {
        printf("Component %s\n", OMX_ComponentRegistered[i++]);
    }
    */
    exit(0);
}

Bellagio 库的输出如下:

Component is OMX.st.clocksrc
  Num roles is 1
    role: clocksrc
Component is OMX.st.clocksrc
  Num roles is 1
    role: clocksrc
Component is OMX.st.video.scheduler
  Num roles is 1
    role: video.scheduler
Component is OMX.st.video.scheduler
  Num roles is 1
    role: video.scheduler
Component is OMX.st.volume.component
  Num roles is 1
    role: volume.component
Component is OMX.st.volume.component
  Num roles is 1
    role: volume.component
Component is OMX.st.audio.mixer
  Num roles is 1
    role: audio.mixer
Component is OMX.st.audio.mixer
  Num roles is 1
    role: audio.mixer
Component is OMX.st.clocksrc
  Num roles is 1
    role: clocksrc
Component is OMX.st.clocksrc
  Num roles is 1
    role: clocksrc
Component is OMX.st.video.scheduler
  Num roles is 1
    role: video.scheduler
Component is OMX.st.video.schedu

ler
  Num roles is 1
    role: video.scheduler
Component is OMX.st.volume.component
  Num roles is 1
    role: volume.component
Component is OMX.st.volume.component
  Num roles is 1
    role: volume.component
Component is OMX.st.audio.mixer
  Num roles is 1
    role: audio.mixer
Component is OMX.st.audio.mixer
  Num roles is 1
    role: audio.mixer
No more components

这不太正确。OpenMAX IL 规范规定每个组件只能出现一次,不能重复。

Raspberry Pi 报告了大量的组件,但是没有为它们中的任何一个定义角色。

Component is OMX.broadcom.audio_capture
  Num roles is 0
Component is OMX.broadcom.audio_decode
  Num roles is 0
Component is OMX.broadcom.audio_encode
  Num roles is 0
Component is OMX.broadcom.audio_render
  Num roles is 0
Component is OMX.broadcom.audio_mixer
  Num roles is 0
Component is OMX.broadcom.audio_splitter
  Num roles is 0
Component is OMX.broadcom.audio_processor
  Num roles is 0
Component is OMX.broadcom.camera
  Num roles is 0
Component is OMX.broadcom.clock
  Num roles is 0
Component is OMX.broadcom.coverage
  Num roles is 0
Component is OMX.broadcom.egl_render
  Num roles is 0
Component is OMX.broadcom.image_fx
  Num roles is 0
Component is OMX.broadcom.image_decode
  Num roles is 0
Component is OMX.broadcom.image_encode
  Num roles is 0
Component is OMX.broadcom.image_read
  Num roles is 0
Component is OMX.broadcom.image_write
  Num roles is 0
Component is OMX.broadcom.read_media
  Num roles is 0
Component is OMX.broadcom.resize
  Num roles is 0
Component is OMX.broadcom.source
  Num roles is 0
Component is OMX.broadcom.text_scheduler
  Num roles is 0
Component is OMX.broadcom.transition
  Num roles is 0
Component is OMX.broadcom.video_decode
  Num roles is 0
Component is OMX.broadcom.video_encode
  Num roles is 0
Component is OMX.broadcom.video_render
  Num roles is 0
Component is OMX.broadcom.video_scheduler
  Num roles is 0
Component is OMX.broadcom.video_splitter
  Num roles is 0
Component is OMX.broadcom.visualisation
  Num roles is 0
Component is OMX.broadcom.write_media
  Num roles is 0
Component is OMX.broadcom.write_still
  Num roles is 0
No more components

LIM 的输出如下:

Component is OMX.limoi.alsa_sink
  Num roles is 1
    role: audio_renderer.pcm
Component is OMX.limoi.clock
  Num roles is 1
    role: clock.binary
Component is OMX.limoi.ffmpeg.decode.audio
  Num roles is 8
    role: audio_decoder.aac
    role: audio_decoder.adpcm
    role: audio_decoder.amr
    role: audio_decoder.mp3
    role: audio_decoder.ogg
    role: audio_decoder.pcm
    role: audio_decoder.ra
    role: audio_decoder.wma
Component is OMX.limoi.ffmpeg.decode.video
  Num roles is 7
    role: video_decoder.avc
    role: video_decoder.h263
    role: video_decoder.mjpeg
    role: video_decoder.mpeg2
    role: video_decoder.mpeg4
    role: video_decoder.rv
    role: video_decoder.wmv
Component is OMX.limoi.ffmpeg.demux
  Num roles is 1
    role: container_demuxer.all
Component is OMX.limoi.ffmpeg.encode.audio
  Num roles is 2
    role: audio_encoder.aac
    role: audio_encoder.mp3
Component is OMX.limoi.ffmpeg.encode.video
  Num roles is 2
    role: video_encoder.h263
    role: video_encoder.mpeg4
Component is OMX.limoi.ffmpeg.mux
  Num roles is 1
    role: container_muxer.all
Component is OMX.limoi.ogg_dec
  Num roles is 1
    role: audio_decoder_with_framing.ogg
Component is OMX.limoi.sdl.renderer.video
  Num roles is 1
    role: iv_renderer.yuv.overlay
Component is OMX.limoi.vid

eo_scheduler
  Num roles is 1
    role: video_scheduler.binary
No more components

获取关于 IL 组件的信息

接下来,您将了解如何获取有关 OpenMAX IL 系统和您使用的任何组件的信息。所有 IL 客户端必须通过调用OMX_Init()来初始化 OpenMAX IL。几乎所有函数都返回错误值,Bellagio 使用的风格如下:

  err = OMX_Init();
  if(err != OMX_ErrorNone) {
      fprintf(stderr, "OMX_Init() failed\n", 0);
      exit(1);
  }

这在我看来是一种合理的风格,所以我在续集中遵循了它。

下一个需求是获得组件的句柄。这需要组件的供应商名称,可以使用前面显示的listcomponents.c程序找到。函数OMX_GetHandle接受一些参数,包括一组回调函数。这些是跟踪应用的行为所需要的,但对于本节中的示例并不需要。这段代码显示了如何获得 Bellagio 音量组件的句柄:

  OMX_HANDLETYPE handle;
  OMX_CALLBACKTYPE callbacks;
  OMX_ERRORTYPE err;

  err = OMX_GetHandle(&handle, "OMX.st.volume.component", NULL /*appPriv */, &callbacks);
  if(err != OMX_ErrorNone) {
      fprintf(stderr, "OMX_GetHandle failed\n", 0);
      exit(1);
  }

组件有端口,端口有通道。这些信息的获取和设置由函数OMX_GetParameter()OMX_SetParameter()OMX_GetConfig()OMX_GetConfig()完成。在组件被“加载”之前进行…Parameter调用,在组件被加载之后进行…Config调用。

c 不是 OO 语言,这是一个普通的函数调用(嗯,实际上是一个宏)。在 OO 语言中,它是一个对象将另一个对象作为参数的方法,如component.method(object)。在 OpenMAX IL 中,Get/Set 函数将调用“对象”作为第一个参数(组件,该方法的参数是什么类型的“对象”的指示符),可能的“对象”类型的索引,以及参数对象的结构。索引值与 1.1 规范表 4-2 中的结构相关。

这些调用采用一个(指向的)结构来填充或提取值。这些结构都是规范化的,因此它们共享公共字段,如结构的大小。在 Bellagio 示例中,这是通过宏setHeader()完成的。传入以获取端口信息的结构通常是类型为OMX_PORT_PARAM_TYPE的通用结构。有些字段可以直接访问,有些需要转换为更特殊的类型,有些隐藏在联合中,必须提取出来。

端口由整数索引标记。不同的功能有不同的端口,如音频、图像、视频等。要获取有关音频端口起始值的信息,请使用以下命令:

  setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));
  err = OMX_GetParameter(handle, OMX_IndexParamAudioInit, &param);
  if(err != OMX_ErrorNone){
      fprintf(stderr, "Error in getting OMX_PORT_PARAM_TYPE parameter\n", 0);
    exit(1);
  }
  printf("Audio ports start on %d\n",
         ((OMX_PORT_PARAM_TYPE)param).nStartPortNumber);
  printf("There are %d open ports\n",
         ((OMX_PORT_PARAM_TYPE)param).nPorts);

setHeader只是填充头部信息,比如版本号和数据结构的大小。

现在可以询问特定端口的能力。您可以查询端口类型(音频或其他)、方向(输入或输出)以及有关支持的 MIME 类型的信息。

  OMX_PARAM_PORTDEFINITIONTYPE sPortDef;

  setHeader(&sPortDef, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
  sPortDef.nPortIndex = 0;
  err = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
  if(err != OMX_ErrorNone){
      fprintf(stderr, "Error in getting OMX_PORT_PARAM_TYPE parameter\n", 0);
    exit(1);
  }
  if (sPortDef.eDomain == OMX_PortDomainAudio) {
      printf("Is an audio port\n");
  } else {
      printf("Is other device port\n");
  }

  if (sPortDef.eDir == OMX_DirInput) {
      printf("Port is an input port\n");
  } else {
      printf("Port is an output port\n");
  }

  /* the Audio Port info is buried in a union format.audio within the struct */
  printf("Port min buffers %d,  mimetype %s, encoding %d\n",
         sPortDef.nBufferCountMin,
         sPortDef.format.audio.cMIMEType,
         sPortDef.format.audio.eEncoding);

Bellagio 库为其音量控制组件支持的 MIME 类型返回“raw/audio”。但是,这不是 IANA MIME 媒体类型( www.iana.org/assignments/media-types )列出的有效 MIME 类型。编码返回的值是零,对应OMX_AUDIO_CodingUnused,这个好像也不正确。

如果您在 Raspberry Pi 组件audio_render和 LIM 组件OMX.limoi.alsa_sink上尝试相同的程序,您会得到 MIME 类型的NULL,但是编码值为 2,也就是OMX_AUDIO_CodingPCM。PCM 有一个哑剧类型的audio/L16,所以NULL似乎不合适。

OpenMAX IL 库允许向端口查询其支持的数据类型。这是通过使用索引OMX_IndexParamAudioPortFormat查询OMX_AUDIO_PARAM_PORTFORMATTYPE对象来完成的。根据规范,对于从零开始的每个索引,对GetParameter()的调用应该返回一个编码,比如OMX_AUDIO_CodingPCMOMX_AUDIO_CodingMp3,直到不再有支持的格式,在这种情况下,调用将返回OMX_ErrorNoMore

Bellagio 代码返回值OMX_AUDIO_CodingUnused,这是不正确的。LIM 代码根本没有设置值,所以您得到的只是垃圾。Broadcom 实现工作正常,但正如将要讨论的那样,它会返回实际上不受支持的值。所以,这种呼吁的价值有限。

以下代码对此进行了测试:

void getSupportedAudioFormats(int indentLevel, int portNumber) {
    OMX_AUDIO_PARAM_PORTFORMATTYPE sAudioPortFormat;

    setHeader(&sAudioPortFormat, sizeof(OMX_AUDIO_PARAM_PORTFORMATTYPE));
    sAudioPortFormat.nIndex = 0;
    sAudioPortFormat.nPortIndex = portNumber;

    printf("Supported audio formats are:\n");
    for(;;) {
        err = OMX_GetParameter(handle, OMX_IndexParamAudioPortFormat, &sAudioPortFormat);
        if (err == OMX_ErrorNoMore) {
            printf("No more formats supported\n");
            return;
        }

        /* This shouldn't occur, but does with Broadcom library */
        if (sAudioPortFormat.eEncoding == OMX_AUDIO_CodingUnused) {
             printf("No coding format returned\n");
             return;
        }

        switch (sAudioPortFormat.eEncoding) {
        case OMX_AUDIO_CodingPCM:
            printf("Supported encoding is PCM\n");
            break;
        case OMX_AUDIO_CodingVORBIS:
            printf("Supported encoding is Ogg Vorbis\n");
            break;
        case OMX_AUDIO_CodingMP3:
            printf("Supported encoding is MP3\n");
            break;
#ifdef RASPBERRY_PI
        case OMX_AUDIO_CodingFLAC:
            printf("Supported encoding is FLAC\n");
            break;
        case OMX_AUDIO_CodingDDP:
            printf("Supported encoding is DDP\n");
            break;
        case OMX_AUDIO_CodingDTS:
            printf("Supported encoding is DTS\n");
            break;
        case OMX_AUDIO_CodingWMAPRO:
            printf("Supported encoding is WMAPRO\n");
            break;
#endif
        case OMX_AUDIO_CodingAAC:
            printf("Supported encoding is AAC\n");
            break;
        case OMX_AUDIO_CodingWMA:
            printf("Supported encoding is WMA\n");
            break;
        case OMX_AUDIO_CodingRA:
            printf("Supported encoding is RA\n");
            break;
        case OMX_AUDIO_CodingAMR:
            printf("Supported encoding is AMR\n");
            break;
        case OMX_AUDIO_CodingEVRC:
            printf("Supported encoding is EVRC\n");
            break;
        case OMX_AUDIO_CodingG726:
            printf("Supported encoding is G726\n");
            break;
        case OMX_AUDIO_CodingMIDI:
            printf("Supported encoding is MIDI\n");
            break;
        case OMX_AUDIO_CodingATRAC3:
            printf("Supported encoding is ATRAC3\n");
            break;
        case OMX_AUDIO_CodingATRACX:
            printf("Supported encoding is ATRACX\n");
            break;
        case OMX_AUDIO_CodingATRACAAL:
            printf("Supported encoding is ATRACAAL\n");
            break;
        default:
            printf("Supported encoding is %d\n",
                  sAudioPortFormat.eEncoding);
        }
        sAudioPortFormat.nIndex++;
    }
}

请注意,该代码包含特定于 Broadcom 库的枚举值,如OMX_AUDIO_CodingATRAC3。根据 OpenMAX IL 扩展机制,这些是合法的值,但当然不是可移植的值。

Bellagio 库错误地为每个索引值返回OMX_AUDIO_CodingUnused

Broadcom 库可以返回许多值。例如,对于audio_decode组件,它返回以下内容:

      Supported audio formats are:
      Supported encoding is MP3
      Supported encoding is PCM
      Supported encoding is AAC
      Supported encoding is WMA
      Supported encoding is Ogg Vorbis
      Supported encoding is RA
      Supported encoding is AMR
      Supported encoding is EVRC
      Supported encoding is G726
      Supported encoding is FLAC
      Supported encoding is DDP
      Supported encoding is DTS
      Supported encoding is WMAPRO
      Supported encoding is ATRAC3
      Supported encoding is ATRACX
      Supported encoding is ATRACAAL
      Supported encoding is MIDI
      No more formats supported

遗憾的是,除了 PCM 之外,这些都不被支持。以下是根据 jamesh 在“音频解码器组件的 OMX _ 分配缓冲区失败”中的说法:

The way it works is that the component returns success for all codecs it may support (that is, all codecs we once owned). This is limited by the actual installed codec. It is best to detect which codecs exist at runtime, but these codes have never been written because they are never needed. This is also unlikely to happen, because Broadcom no longer supports audio codecs in this way, they have moved from the video core to the host CPU, because they are now strong enough to handle any audio decoding task.

这真的有点可悲。

将所有的位放在一起就产生了程序info.c,如下所示:

/**
   Based on code
   Copyright (C) 2007-2009 STMicroelectronics
   Copyright (C) 2007-2009 Nokia Corporation and/or its subsidiary(-ies).
   under the LGPL
*/

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>

#include <OMX_Core.h>
#include <OMX_Component.h>
#include <OMX_Types.h>
#include <OMX_Audio.h>

#ifdef RASPBERRY_PI
#include <bcm_host.h>
#endif

OMX_ERRORTYPE err;
OMX_HANDLETYPE handle;
OMX_VERSIONTYPE specVersion, compVersion;

OMX_CALLBACKTYPE callbacks;

#define indent {int n = 0; while (n++ < indentLevel*2) putchar(' ');}

static void setHeader(OMX_PTR header, OMX_U32 size) {
    /* header->nVersion */
    OMX_VERSIONTYPE* ver = (OMX_VERSIONTYPE*)(header + sizeof(OMX_U32));
    /* header->nSize */
    *((OMX_U32*)header) = size;

    /* for 1.2
       ver->s.nVersionMajor = OMX_VERSION_MAJOR;
       ver->s.nVersionMinor = OMX_VERSION_MINOR;
       ver->s.nRevision = OMX_VERSION_REVISION;
       ver->s.nStep = OMX_VERSION_STEP;
    */
    ver->s.nVersionMajor = specVersion.s.nVersionMajor;
    ver->s.nVersionMinor = specVersion.s.nVersionMinor;
    ver->s.nRevision = specVersion.s.nRevision;
    ver->s.nStep = specVersion.s.nStep;
}

void printState() {
    OMX_STATETYPE state;
    err = OMX_GetState(handle, &state);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Error on getting state\n");
        exit(1);
    }
    switch (state) {
    case OMX_StateLoaded: fprintf(stderr, "StateLoaded\n"); break;
    case OMX_StateIdle: fprintf(stderr, "StateIdle\n"); break;
    case OMX_StateExecuting: fprintf(stderr, "StateExecuting\n"); break;
    case OMX_StatePause: fprintf(stderr, "StatePause\n"); break;
    case OMX_StateWaitForResources: fprintf(stderr, "StateWiat\n"); break;
    default:  fprintf(stderr, "State unknown\n"); break;
    }
}

OMX_ERRORTYPE setEncoding(int portNumber, OMX_AUDIO_CODINGTYPE encoding) {
    OMX_PARAM_PORTDEFINITIONTYPE sPortDef;

    setHeader(&sPortDef, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
    sPortDef.nPortIndex = portNumber;
    sPortDef.nPortIndex = portNumber;
    err = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting OMX_PORT_DEFINITION_TYPE parameter\n",
 0);
        exit(1);
    }

    sPortDef.format.audio.eEncoding = encoding;
    sPortDef.nBufferCountActual = sPortDef.nBufferCountMin;

    err = OMX_SetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
    return err;
}

void getPCMInformation(int indentLevel, int portNumber) {
    /* assert: PCM is a supported mode */
    OMX_AUDIO_PARAM_PCMMODETYPE sPCMMode;

    /* set it into PCM format before asking for PCM info */
    if (setEncoding(portNumber, OMX_AUDIO_CodingPCM) != OMX_ErrorNone) {
        fprintf(stderr, "Error in setting coding to PCM\n");
        return;
    }

    setHeader(&sPCMMode, sizeof(OMX_AUDIO_PARAM_PCMMODETYPE));
    sPCMMode.nPortIndex = portNumber;
    err = OMX_GetParameter(handle, OMX_IndexParamAudioPcm, &sPCMMode);
    if(err != OMX_ErrorNone){
        indent printf("PCM mode unsupported\n");
    } else {
        indent printf("  PCM default sampling rate %d\n", sPCMMode.nSamplingRate);
        indent printf("  PCM default bits per sample %d\n", sPCMMode.nBitPerSample);
        indent printf("  PCM default number of channels %d\n", sPCMMode.nChannels);
    }

    /*
    setHeader(&sAudioPortFormat, sizeof(OMX_AUDIO_PARAM_PORTFORMATTYPE));
    sAudioPortFormat.nIndex = 0;
    sAudioPortFormat.nPortIndex = portNumber;
    */

}
void getMP3Information(int indentLevel, int portNumber) {
    /* assert: MP3 is a supported mode */
    OMX_AUDIO_PARAM_MP3TYPE sMP3Mode;

    /* set it into MP3 format before asking for MP3 info */
    if (setEncoding(portNumber, OMX_AUDIO_CodingMP3) != OMX_ErrorNone) {
        fprintf(stderr, "Error in setting coding to MP3\n");
        return;
    }

    setHeader(&sMP3Mode, sizeof(OMX_AUDIO_PARAM_MP3TYPE));
    sMP3Mode.nPortIndex = portNumber;
    err = OMX_GetParameter(handle, OMX_IndexParamAudioMp3, &sMP3Mode);
    if(err != OMX_ErrorNone){
        indent printf("MP3 mode unsupported\n");
    } else {
        indent printf("  MP3 default sampling rate %d\n", sMP3Mode.nSampleRate);
        indent printf("  MP3 default bits per sample %d\n", sMP3Mode.nBitRate);
        indent printf("  MP3 default number of channels %d\n", sMP3Mode.nChannels);
    }
}

void getSupportedAudioFormats(int indentLevel, int portNumber) {
    OMX_AUDIO_PARAM_PORTFORMATTYPE sAudioPortFormat;

    setHeader(&sAudioPortFormat, sizeof(OMX_AUDIO_PARAM_PORTFORMATTYPE));
    sAudioPortFormat.nIndex = 0;
    sAudioPortFormat.nPortIndex = portNumber;

#ifdef LIM
    printf("LIM doesn't set audio formats properly\n");
    return;
#endif

    indent printf("Supported audio formats are:\n");
    for(;;) {
        err = OMX_GetParameter(handle, OMX_IndexParamAudioPortFormat, &sAudioPortFormat);
        if (err == OMX_ErrorNoMore) {
            indent printf("No more formats supported\n");
            return;
        }

        /* This shouldn't occur, but does with Broadcom library */
        if (sAudioPortFormat.eEncoding == OMX_AUDIO_CodingUnused) {
             indent printf("No coding format returned\n");
             return;
        }

        switch (sAudioPortFormat.eEncoding) {
        case OMX_AUDIO_CodingPCM:
            indent printf("Supported encoding is PCM\n");
            getPCMInformation(indentLevel+1, portNumber);
            break;
        case OMX_AUDIO_CodingVORBIS:
            indent printf("Supported encoding is Ogg Vorbis\n");
            break;
        case OMX_AUDIO_CodingMP3:
            indent printf("Supported encoding is MP3\n");
            getMP3Information(indentLevel+1, portNumber);
            break;
#ifdef RASPBERRY_PI
        case OMX_AUDIO_CodingFLAC:
            indent printf("Supported encoding is FLAC\n");
            break;
        case OMX_AUDIO_CodingDDP:
            indent printf("Supported encoding is DDP\n");
            break;
        case OMX_AUDIO_CodingDTS:
            indent printf("Supported encoding is DTS\n");
            break;
        case OMX_AUDIO_CodingWMAPRO:
            indent printf("Supported encoding is WMAPRO\n");
            break;
        case OMX_AUDIO_CodingATRAC3:
            indent printf("Supported encoding is ATRAC3\n");
            break;
        case OMX_AUDIO_CodingATRACX:
            indent printf("Supported encoding is ATRACX\n");
            break;
        case OMX_AUDIO_CodingATRACAAL:
            indent printf("Supported encoding is ATRACAAL\n");
            break;
#endif
        case OMX_AUDIO_CodingAAC:
            indent printf("Supported encoding is AAC\n");
            break;
        case OMX_AUDIO_CodingWMA:
            indent printf("Supported encoding is WMA\n");
            break;
        case OMX_AUDIO_CodingRA:
            indent printf("Supported encoding is RA\n");
            break;
        case OMX_AUDIO_CodingAMR:
            indent printf("Supported encoding is AMR\n");
            break;
        case OMX_AUDIO_CodingEVRC:
            indent printf("Supported encoding is EVRC\n");
            break;
        case OMX_AUDIO_CodingG726:
            indent printf("Supported encoding is G726\n");
            break;
        case OMX_AUDIO_CodingMIDI:
            indent printf("Supported encoding is MIDI\n");
            break;

            /*
        case OMX_AUDIO_Coding:
            indent printf("Supported encoding is \n");
            break;
            */
        default:
            indent printf("Supported encoding is not PCM or MP3 or Vorbis, is 0x%X\n",
                  sAudioPortFormat.eEncoding);
        }
        sAudioPortFormat.nIndex++;
    }
}

void getAudioPortInformation(int indentLevel, int nPort, OMX_PARAM_PORTDEFINITIONTYPE sPortDef) {
    indent printf("Port %d requires %d buffers\n", nPort, sPortDef.nBufferCountMin);
    indent printf("Port %d has min buffer size %d bytes\n", nPort, sPortDef.nBufferSize);

    if (sPortDef.eDir == OMX_DirInput) {
        indent printf("Port %d is an input port\n", nPort);
    } else {
        indent printf("Port %d is an output port\n",  nPort);
    }
    switch (sPortDef.eDomain) {
    case OMX_PortDomainAudio:
        indent printf("Port %d is an audio port\n", nPort);
        indent printf("Port mimetype %s\n",
               sPortDef.format.audio.cMIMEType);

        switch (sPortDef.format.audio.eEncoding) {
        case OMX_AUDIO_CodingPCM:
            indent printf("Port encoding is PCM\n");
            break;
        case OMX_AUDIO_CodingVORBIS:
            indent printf("Port encoding is Ogg Vorbis\n");
            break;
        case OMX_AUDIO_CodingMP3:
            indent printf("Port encoding is MP3\n");
            break;
        default:
            indent printf("Port encoding is not PCM or MP3 or Vorbis, is %d\n",
                   sPortDef.format.audio.eEncoding);
        }
        getSupportedAudioFormats(indentLevel+1, nPort);

        break;
        /* could put other port types here */
    default:
        indent printf("Port %d is not an audio port\n",  nPort);
    }
}

void getAllAudioPortsInformation(int indentLevel) {
    OMX_PORT_PARAM_TYPE param;
    OMX_PARAM_PORTDEFINITIONTYPE sPortDef;

    int startPortNumber;
    int nPorts;
    int n;

    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));

    err = OMX_GetParameter(handle, OMX_IndexParamAudioInit, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting audio OMX_PORT_PARAM_TYPE parameter\n", 0);
        return;
    }
    indent printf("Audio ports:\n");
    indentLevel++;

    startPortNumber = param.nStartPortNumber;
    nPorts = param.nPorts;
    if (nPorts == 0) {
        indent printf("No ports of this type\n");
        return;
    }

    indent printf("Ports start on %d\n", startPortNumber);
    indent printf("There are %d open ports\n", nPorts);

    for (n = 0; n < nPorts; n++) {
        setHeader(&sPortDef, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
        sPortDef.nPortIndex = startPortNumber + n;
        err = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
        if(err != OMX_ErrorNone){
            fprintf(stderr, "Error in getting OMX_PORT_DEFINITION_TYPE parameter\n", 0);
            exit(1);
        }
        getAudioPortInformation(indentLevel+1, startPortNumber + n, sPortDef);
    }
}

void getAllVideoPortsInformation(int indentLevel) {
    OMX_PORT_PARAM_TYPE param;
    int startPortNumber;
    int nPorts;
    int n;

    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));

    err = OMX_GetParameter(handle, OMX_IndexParamVideoInit, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting video OMX_PORT_PARAM_TYPE parameter\n", 0);
        return;
    }
    printf("Video ports:\n");
    indentLevel++;

    startPortNumber = param.nStartPortNumber;
    nPorts = param.nPorts;
    if (nPorts == 0) {
        indent printf("No ports of this type\n");
        return;
    }

    indent printf("Ports start on %d\n", startPortNumber);
    indent printf("There are %d open ports\n", nPorts);
}

void getAllImagePortsInformation(int indentLevel) {
    OMX_PORT_PARAM_TYPE param;
    int startPortNumber;
    int nPorts;
    int n;

    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));

    err = OMX_GetParameter(handle, OMX_IndexParamVideoInit, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting image OMX_PORT_PARAM_TYPE parameter\n", 0);
        return;
    }
    printf("Image ports:\n");
    indentLevel++;

    startPortNumber = param.nStartPortNumber;
    nPorts = param.nPorts;
    if (nPorts == 0) {
        indent printf("No ports of this type\n");
        return;
    }

    indent printf("Ports start on %d\n", startPortNumber);
    indent printf("There are %d open ports\n", nPorts);
}

void getAllOtherPortsInformation(int indentLevel) {
    OMX_PORT_PARAM_TYPE param;
    int startPortNumber;
    int nPorts;
    int n;

    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));

    err = OMX_GetParameter(handle, OMX_IndexParamVideoInit, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting other OMX_PORT_PARAM_TYPE parameter\n", 0);
        exit(1);
    }
    printf("Other ports:\n");
    indentLevel++;

    startPortNumber = param.nStartPortNumber;
    nPorts = param.nPorts;
    if (nPorts == 0) {
        indent printf("No ports of this type\n");
        return;
    }

    indent printf("Ports start on %d\n", startPortNumber);
    indent printf("There are %d open ports\n", nPorts);
}

int main(int argc, char** argv) {

    OMX_PORT_PARAM_TYPE param;
    OMX_PARAM_PORTDEFINITIONTYPE sPortDef;
    OMX_AUDIO_PORTDEFINITIONTYPE sAudioPortDef;
    OMX_AUDIO_PARAM_PORTFORMATTYPE sAudioPortFormat;
    OMX_AUDIO_PARAM_PCMMODETYPE sPCMMode;

#ifdef RASPBERRY_PI
    char *componentName = "OMX.broadcom.audio_mixer";
#endif
#ifdef LIM
    char *componentName = "OMX.limoi.alsa_sink";
#else
    char *componentName = "OMX.st.volume.component";
#endif
    unsigned char name[128]; /* spec says 128 is max name length */
    OMX_UUIDTYPE uid;
    int startPortNumber;
    int nPorts;
    int n;

    /* ovveride component name by command line argument */
    if (argc == 2) {
        componentName = argv[1];
    }

# ifdef RASPBERRY_PI
    bcm_host_init();
# endif

    err = OMX_Init();
    if(err != OMX_ErrorNone) {
        fprintf(stderr, "OMX_Init() failed\n", 0);
        exit(1);
    }
    /** Ask the core for a handle to the volume control component
     */
    err = OMX_GetHandle(&handle, componentName, NULL /*app private data */, &callbacks);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "OMX_GetHandle failed\n", 0);
        exit(1);
    }
    err = OMX_GetComponentVersion(handle, name, &compVersion, &specVersion, &uid);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "OMX_GetComponentVersion failed\n", 0);
        exit(1);
    }
    printf("Component name: %s version %d.%d, Spec version %d.%d\n",
           name, compVersion.s.nVersionMajor,
           compVersion.s.nVersionMinor,
           specVersion.s.nVersionMajor,
           specVersion.s.nVersionMinor);

    /** Get  ports information */
    getAllAudioPortsInformation(0);
    getAllVideoPortsInformation(0);
    getAllImagePortsInformation(0);
    getAllOtherPortsInformation(0);

    exit(0);
}

Bellagio 版本的Makefile如下:

INCLUDES=-I ../libomxil-bellagio-0.9.3/include/
LIBS=-L ../libomxil-bellagio-0.9.3/src/.libs -l omxil-bellagio
CFLAGS = -g

info: info.c
        cc $(FLAGS) $(INCLUDES) -o info info.c $(LIBS)

使用 Bellagio 实现的输出如下:

Component name: OMX.st.volume.component version 1.1, Spec version 1.1
Audio ports:
  Ports start on 0
  There are 2 open ports
    Port 0 requires 2 buffers
    Port 0 is an input port
    Port 0 is an audio port
    Port mimetype raw/audio
    Port encoding is not PCM or MP3 or Vorbis, is 0
      Supported audio formats are:
      No coding format returned
    Port 1 requires 2 buffers
    Port 1 is an output port
    Port 1 is an audio port
    Port mimetype raw/audio
    Port encoding is not PCM or MP3 or Vorbis, is 0
      Supported audio formats are:
      No coding format returned
Video ports:
  No ports of this type
Image ports:
  No ports of this type
Other ports:
  No ports of this type

树莓派的Makefile如下:

INCLUDES=-I /opt/vc/include/IL -I /opt/vc/include -I /opt/vc/include/interface/vcos/pthreads
CFLAGS=-g -DRASPBERRY_PI
LIBS=-L /opt/vc/lib -l openmaxil -l bcm_host

info: info.c
        cc $(CFLAGS) $(INCLUDES) -o info info.c $(LIBS)

组件audio_render在 Raspberry Pi 上的输出如下:

Audio ports:
  Ports start on 100
  There are 1 open ports
    Port 100 requires 1 buffers
    Port 100 is an input port
    Port 100 is an audio port
    Port mimetype (null)
    Port encoding is PCM
      Supported audio formats are:
      Supported encoding is PCM
          PCM default sampling rate 44100
          PCM default bits per sample 16
          PCM default number of channels 2
      Supported encoding is DDP
      No more formats supported
Video ports:
  No ports of this type
Image ports:
  No ports of this type
Other ports:
  No ports of this type

直线电机的Makefile如下:

INCLUDES=-I ../../lim-omx-1.1/LIM/limoi-core/include/
#LIBS=-L ../../lim-omx-1.1/LIM/limoi-base/src/.libs -l limoi-base
LIBS = -L /home/newmarch/osm-build/lib/ -l limoa -l limoi-core
CFLAGS = -g -DLIM

info: info.c
        cc $(CFLAGS) $(INCLUDES) -o info info.c $(LIBS)

alsa_sink组件的 LIM 输出如下:

Component name: OMX.limoi.alsa_sink version 0.0, Spec version 1.1
Audio ports:
  Ports start on 0
  There are 1 open ports
    Port 0 requires 2 buffers
    Port 0 is an input port
    Port 0 is an audio port
    Port mimetype (null)
    Port encoding is PCM
LIM doesn't set audio formats properly
Error in getting video OMX_PORT_PARAM_TYPE parameter
Error in getting image OMX_PORT_PARAM_TYPE parameter
Error in getting other OMX_PORT_PARAM_TYPE parameter

当组件不支持某个模式时(这里的音频组件不支持视频、图像或其他模式),LIM 实现会抛出错误。这违反了 1.1 规范,该规范规定如下:

"All standard components shall support the following parameters:
  o OMX_IndexParamPortDefinition
  o OMX_IndexParamCompBufferSupplier
  o OMX_IndexParamAudioInit
  o OMX_IndexParamImageInit
  o OMX_IndexParamVideoInit
  o OMX_IndexParamOtherInit"

我想你可能会说alsa_sink组件不是标准组件,所以它是允许的。嗯,好吧…

播放 PCM 音频文件

向输出设备播放音频需要使用audio_render设备。这是 1.1 规范中的标准设备之一,包含在 Broadcom Raspberry Pi 库中,但不包含在 Bellagio 库中。LIM 有一个组件alsa_sink,起着同样的作用。

播放音频的程序结构如下:

  1. 初始化库和音频渲染组件。
  2. 不断填充输入缓冲区,并要求组件清空缓冲区。
  3. 从组件捕获事件,告知缓冲区已被清空,以便安排重新填充缓冲区并请求清空缓冲区。
  4. 完工后清理。

请注意,Raspberry Pi 音频渲染组件将只播放 PCM 数据,而 LIM alsa_sink组件只能以 44,100Hz 播放。

状态

初始化组件是一个多步骤的过程,具体取决于组件的状态。组件在Loaded状态下创建。它们通过OMX_SendCommand(handle, OMX_CommandStateSet, <next state>, <param>)从一种状态转换到另一种状态。从Loaded出发的下一个州应该是Idle,从那里到Executing。还有其他一些你不需要关心的状态。

改变状态的请求是异步的。send 命令立即返回(嗯,在 5 毫秒内)。当状态发生实际变化时,会调用事件处理程序回调函数。

线

一些命令要求组件处于特定状态。将组件置于某种状态的请求是异步的。因此,客户端可以发出请求,但是客户端可能必须等待,直到状态发生变化。这最好通过客户端暂停其线程的操作来完成,直到被事件处理程序中发生的状态变化唤醒。

Linux/Unix 已经在管理多线程的 Posix pthreads 库上实现了标准化。出于我们的目的,您使用了这个库中的两个部分:在关键部分放置互斥体的能力和基于条件挂起/唤醒线程的能力。Pthreads 在很多地方都有涉及,Blaise Barney 有一个很短很好的教程叫做“POSIX Threads 编程”( https://computing.llnl.gov/tutorials/pthreads/#Misc )。

您使用的函数和数据如下:

pthread_mutex_t mutex;
OMX_STATETYPE currentState = OMX_StateLoaded;
pthread_cond_t stateCond;

void waitFor(OMX_STATETYPE state) {
    pthread_mutex_lock(&mutex);
    while (currentState != state)
        pthread_cond_wait(&stateCond, &mutex);
    fprintf(stderr, "Wait successfully completed\n");
    pthread_mutex_unlock(&mutex);
}

void wakeUp(OMX_STATETYPE newState) {
    pthread_mutex_lock(&mutex);
    currentState = newState;
    pthread_cond_signal(&stateCond);
    pthread_mutex_unlock(&mutex);
}
pthread_mutex_t empty_mutex;
int emptyState = 0;
OMX_BUFFERHEADERTYPE* pEmptyBuffer;
pthread_cond_t emptyStateCond;

void waitForEmpty() {
    pthread_mutex_lock(&empty_mutex);
    while (emptyState == 1)
        pthread_cond_wait(&emptyStateCond, &empty_mutex);
    emptyState = 1;
    pthread_mutex_unlock(&empty_mutex);
}

void wakeUpEmpty(OMX_BUFFERHEADERTYPE* pBuffer) {
    pthread_mutex_lock(&empty_mutex);
    emptyState = 0;
    pEmptyBuffer = pBuffer;
    pthread_cond_signal(&emptyStateCond);
    pthread_mutex_unlock(&empty_mutex);
}

void mutex_init() {
    int n = pthread_mutex_init(&mutex, NULL);
    if ( n != 0) {
        fprintf(stderr, "Can't init state mutex\n");
    }
    n = pthread_mutex_init(&empty_mutex, NULL);
    if ( n != 0) {
        fprintf(stderr, "Can't init empty mutex\n");
    }
}

OpenMAX IL 中的匈牙利符号

匈牙利符号是由查尔斯·西蒙尼发明的,用来给变量、结构和字段名添加类型或功能信息。Microsoft Windows SDK 中大量使用了一个窗体。在 OpenMAX IL 中,通过为变量、字段等添加前缀,使用了一种简化形式,如下所示:

  • 以某种数字为前缀。
  • p给指针加前缀。
  • 给结构或字符串加前缀。
  • 给回调函数加前缀。

这些公约的价值是很有争议的。

回收

两种类型的回调函数与这个例子相关:在状态和一些其他事件改变时发生的事件回调,以及当组件清空输入缓冲区时发生的空缓冲区回调。这些在以下机构注册:

OMX_CALLBACKTYPE callbacks  = { .EventHandler = cEventHandler,
            .EmptyBufferDone = cEmptyBufferDone,
};
err = OMX_GetHandle(&handle, componentName, NULL /*app private data */, &callbacks);

组件资源

每个组件都有许多需要配置的端口。端口是组件的一些资源。每个端口开始时是启用的,但可以用OMX_SendCommand(handle, OMX_CommandPortDisable, <port number>, NULL)设置为禁用。

启用的端口可以分配缓冲区,用于将数据传入和传出组件。这可以通过两种方式完成:OMX_AllocateBuffer要求组件为客户端执行分配,而使用OMX_UseBuffer客户端将一个缓冲区交给组件。由于可能存在缓冲区内存对齐问题,我更喜欢让组件进行分配。

这是一个棘手的部分。要在组件上分配或使用缓冲区,必须请求从Loaded状态转换到Idle。因此,在分配缓冲区之前,必须调用OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateIdle, <param>)。但是直到每个端口都被禁用或者所有的缓冲区都被分配后,到Idle的转换才会发生。

这最后一步让我绞尽脑汁了将近一周。audio_render组件有两个端口:一个输入音频端口和一个时间更新端口。虽然我已经正确配置了音频端口,但我没有禁用时间端口,因为我不知道它有时间端口。因此,到Idle的转换从未发生。下面是处理这种情况的代码:

    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));
    err = OMX_GetParameter(handle, OMX_IndexParamOtherInit, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting OMX_PORT_PARAM_TYPE parameter\n", 0);
        exit(1);
    }
    startPortNumber = ((OMX_PORT_PARAM_TYPE)param).nStartPortNumber;
    nPorts = ((OMX_PORT_PARAM_TYPE)param).nPorts;
    printf("Other has %d ports\n", nPorts);
    /* and disable it */
    err = OMX_SendCommand(handle, OMX_CommandPortDisable, startPortNumber, NULL);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Error on setting port to disabled\n");
        exit(1);
    }

以下是如何设置音频端口的参数:

    /** Get audio port information */
    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));
    err = OMX_GetParameter(handle, OMX_IndexParamAudioInit, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting OMX_PORT_PARAM_TYPE parameter\n", 0);
        exit(1);
    }
    startPortNumber = ((OMX_PORT_PARAM_TYPE)param).nStartPortNumber;
    nPorts = ((OMX_PORT_PARAM_TYPE)param).nPorts;
    if (nPorts > 1) {
        fprintf(stderr, "Render device has more than one port\n");
        exit(1);
    }

    setHeader(&sPortDef, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
    sPortDef.nPortIndex = startPortNumber;
    err = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting OMX_PORT_DEFINITION_TYPE parameter\n", 0);
        exit(1);
    }
    if (sPortDef.eDomain != OMX_PortDomainAudio) {
        fprintf(stderr, "Port %d is not an audio port\n", startPortNumber);
        exit(1);
    }

    if (sPortDef.eDir != OMX_DirInput) {
        fprintf(stderr, "Port is not an input port\n");
        exit(1);
    }
    if (sPortDef.format.audio.eEncoding == OMX_AUDIO_CodingPCM) {
        printf("Port encoding is PCM\n");
    }    else {
        printf("Port has unknown encoding\n");
    }

    /* create minimum number of buffers for the port */
    nBuffers = sPortDef.nBufferCountActual = sPortDef.nBufferCountMin;
    printf("Number of bufers is %d\n", nBuffers);
    err = OMX_SetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in setting OMX_PORT_PARAM_TYPE parameter\n", 0);
        exit(1);
    }

    /* call to put state into idle before allocating buffers */
    err = OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateIdle, NULL);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Error on setting state to idle\n");
        exit(1);
    }

    err = OMX_SendCommand(handle, OMX_CommandPortEnable, startPortNumber, NULL);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Error on setting port to enabled\n");
        exit(1);
    }

    nBufferSize = sPortDef.nBufferSize;
    printf("%d buffers of size is %d\n", nBuffers, nBufferSize);

    inBuffers = malloc(nBuffers * sizeof(OMX_BUFFERHEADERTYPE *));
    if (inBuffers == NULL) {
        fprintf(stderr, "Can't allocate buffers\n");
        exit(1);
    }
    for (n = 0; n < nBuffers; n++) {
        err = OMX_AllocateBuffer(handle, inBuffers+n, startPortNumber, NULL,
                                 nBufferSize);
        if (err != OMX_ErrorNone) {
            fprintf(stderr, "Error on AllocateBuffer in 1%i\n", err);
            exit(1);
        }

    }

    waitFor(OMX_StateIdle);
    /* try setting the encoding to PCM mode */
    setHeader(&sPCMMode, sizeof(OMX_AUDIO_PARAM_PCMMODETYPE));
    sPCMMode.nPortIndex = startPortNumber;
    err = OMX_GetParameter(handle, OMX_IndexParamAudioPcm, &sPCMMode);
    if(err != OMX_ErrorNone){
        printf("PCM mode unsupported\n");
        exit(1);
    } else {
        printf("PCM mode supported\n");
        printf("PCM sampling rate %d\n", sPCMMode.nSamplingRate);
        printf("PCM nChannels %d\n", sPCMMode.nChannels);
    }

设置输出设备

OpenMAX 有一个标准的音频渲染组件。但是它渲染到什么设备上呢?内置声卡?USB 声卡?这不是 OpenMAX IL 的一部分;甚至没有办法列出音频设备,只有音频组件。

OpenMAX 有一个扩展机制,OpenMAX 实现者可以使用它来回答类似这样的问题。Broadcom 核心实现具有可用于设置音频目的(源)设备的扩展类型OMX_CONFIG_BRCMAUDIODESTINATIONTYPE(和OMX_CONFIG_BRCMAUDIOSOURCETYPE)。下面是执行此操作的代码:

void setOutputDevice(const char *name) {
   int32_t success = -1;
   OMX_CONFIG_BRCMAUDIODESTINATIONTYPE arDest;

   if (name && strlen(name) < sizeof(arDest.sName)) {
       setHeader(&arDest, sizeof(OMX_CONFIG_BRCMAUDIODESTINATIONTYPE));
       strcpy((char *)arDest.sName, name);

       err = OMX_SetParameter(handle, OMX_IndexConfigBrcmAudioDestination, &arDest);
       if (err != OMX_ErrorNone) {
           fprintf(stderr, "Error on setting audio destination\n");
           exit(1);
       }
   }
}

这是它再次陷入黑暗的地方。头文件<IL/OMX_Broadcom.h>声明sName的默认值是“local ”,但没有给出任何其他值。Raspberry Pi 论坛表示,这是指 3.5 毫米模拟音频输出,hdmi 是通过使用值“HDMI”来选择的没有记录其他值,并且 Broadcom OpenMAX IL 似乎不支持任何其他音频设备。特别是,当前的 Broadcom OpenMAX IL 组件不支持 USB 音频设备的输入或输出。因此,你不能使用 OpenMAX IL 在 Raspberry Pi 上进行音频捕获,因为它没有 Broadcom 支持的音频输入。

主循环

一旦所有端口都设置好,播放音频文件包括填充缓冲区,等待它们变空,然后再填充它们,直到数据结束。有两种可能的样式。

  • 在主循环中填充缓冲区一次,然后在空缓冲区回调中继续填充和清空缓冲区。
  • 在主循环中,不断地填充和清空缓冲区,在每次填充之间等待缓冲区清空。

Bellagio 示例使用了第一种技术。然而,1.2 规范说“…IL 客户端不应该从 IL 回调上下文中调用 IL 核心或组件函数”,所以这不是一个好的技术。Raspberry Pi 示例使用了第二种技术,但是使用了一个非标准调用来查找当时的等待时间和延迟。最好只设置更多的 pthreads 条件,并在这些条件上进行阻塞。

这将导致一个如下所示的主循环:

    emptyState = 1;
    for (;;) {
        int data_read = read(fd, inBuffers[0]->pBuffer, nBufferSize);
        inBuffers[0]->nFilledLen = data_read;
        inBuffers[0]->nOffset = 0;
        filesize -= data_read;
        if (data_read <= 0) {
            fprintf(stderr, "In the %s no more input data available\n", __func__);
            inBuffers[0]->nFilledLen=0;
            inBuffers[0]->nFlags = OMX_BUFFERFLAG_EOS;
            bEOS=OMX_TRUE;
            err = OMX_EmptyThisBuffer(handle, inBuffers[0]);
            break;
        }
        if(!bEOS) {
            fprintf(stderr, "Emptying again buffer %p %d bytes, %d to go\n", inBuffers[0], data_read, filesize);
            err = OMX_EmptyThisBuffer(handle, inBuffers[0]);
        }else {
            fprintf(stderr, "In %s Dropping Empty This buffer to Audio Dec\n", __func__);
        }
        waitForEmpty();
        printf("Waited for empty\n");
    }

    printf("Buffers emptied\n");

完整程序

完整的程序如下:

/**
   Based on code
   Copyright (C) 2007-2009 STMicroelectronics
   Copyright (C) 2007-2009 Nokia Corporation and/or its subsidiary(-ies).
   under the LGPL
*/

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pthread.h>

#include <OMX_Core.h>
#include <OMX_Component.h>
#include <OMX_Types.h>
#include <OMX_Audio.h>

#ifdef RASPBERRY_PI
#include <bcm_host.h>
#include <IL/OMX_Broadcom.h>
#endif

OMX_ERRORTYPE err;
OMX_HANDLETYPE handle;
OMX_VERSIONTYPE specVersion, compVersion;

int fd = 0;
unsigned int filesize;
static OMX_BOOL bEOS=OMX_FALSE;

OMX_U32 nBufferSize;
int nBuffers;

pthread_mutex_t mutex;
OMX_STATETYPE currentState = OMX_StateLoaded;
pthread_cond_t stateCond;

void waitFor(OMX_STATETYPE state) {
    pthread_mutex_lock(&mutex);
    while (currentState != state)
        pthread_cond_wait(&stateCond, &mutex);
    pthread_mutex_unlock(&mutex);
}

void wakeUp(OMX_STATETYPE newState) {
    pthread_mutex_lock(&mutex);
    currentState = newState;
    pthread_cond_signal(&stateCond);
    pthread_mutex_unlock(&mutex);
}

pthread_mutex_t empty_mutex;
int emptyState = 0;
OMX_BUFFERHEADERTYPE* pEmptyBuffer;
pthread_cond_t emptyStateCond;

void waitForEmpty() {
    pthread_mutex_lock(&empty_mutex);
    while (emptyState == 1)
        pthread_cond_wait(&emptyStateCond, &empty_mutex);
    emptyState = 1;
    pthread_mutex_unlock(&empty_mutex);
}

void wakeUpEmpty(OMX_BUFFERHEADERTYPE* pBuffer) {
    pthread_mutex_lock(&empty_mutex);
    emptyState = 0;
    pEmptyBuffer = pBuffer;
    pthread_cond_signal(&emptyStateCond);
    pthread_mutex_unlock(&empty_mutex);
}

void mutex_init() {
    int n = pthread_mutex_init(&mutex, NULL);
    if ( n != 0) {
        fprintf(stderr, "Can't init state mutex\n");
    }
    n = pthread_mutex_init(&empty_mutex, NULL);
    if ( n != 0) {
        fprintf(stderr, "Can't init empty mutex\n");
    }
}

static void display_help() {
    fprintf(stderr, "Usage: render input_file");
}

/** Gets the file descriptor's size
 * @return the size of the file. If size cannot be computed
 * (i.e. stdin, zero is returned)
 */
static int getFileSize(int fd) {

    struct stat input_file_stat;
    int err;

    /* Obtain input file length */
    err = fstat(fd, &input_file_stat);
    if(err){
        fprintf(stderr, "fstat failed",0);
        exit(-1);
    }
    return input_file_stat.st_size;
}

OMX_ERRORTYPE cEventHandler(
                            OMX_HANDLETYPE hComponent,
                            OMX_PTR pAppData,
                            OMX_EVENTTYPE eEvent,
                            OMX_U32 Data1,
                            OMX_U32 Data2,
                            OMX_PTR pEventData) {

    fprintf(stderr, "Hi there, I am in the %s callback\n", __func__);
    if(eEvent == OMX_EventCmdComplete) {
        if (Data1 == OMX_CommandStateSet) {
            fprintf(stderr, "Component State changed in ", 0);
            switch ((int)Data2) {
            case OMX_StateInvalid:
                fprintf(stderr, "OMX_StateInvalid\n", 0);
                break;
            case OMX_StateLoaded:
                fprintf(stderr, "OMX_StateLoaded\n", 0);
                break;
            case OMX_StateIdle:
                fprintf(stderr, "OMX_StateIdle\n",0);
                break;
            case OMX_StateExecuting:
                fprintf(stderr, "OMX_StateExecuting\n",0);
                break;
            case OMX_StatePause:
                fprintf(stderr, "OMX_StatePause\n",0);
                break;
            case OMX_StateWaitForResources:
                fprintf(stderr, "OMX_StateWaitForResources\n",0);
                break;
            }
            wakeUp((int) Data2);
        } else  if (Data1 == OMX_CommandPortEnable){

        } else if (Data1 == OMX_CommandPortDisable){

        }
    } else if(eEvent == OMX_EventBufferFlag) {
        if((int)Data2 == OMX_BUFFERFLAG_EOS) {

        }
    } else {
        fprintf(stderr, "Param1 is %i\n", (int)Data1);
        fprintf(stderr, "Param2 is %i\n", (int)Data2);
    }

    return OMX_ErrorNone;
}

OMX_ERRORTYPE cEmptyBufferDone(
                               OMX_HANDLETYPE hComponent,
                               OMX_PTR pAppData,
                               OMX_BUFFERHEADERTYPE* pBuffer) {

    fprintf(stderr, "Hi there, I am in the %s callback.\n", __func__);
    if (bEOS) {
        fprintf(stderr, "Buffers emptied, exiting\n");
    }
    wakeUpEmpty(pBuffer);
    fprintf(stderr, "Exiting callback\n");

    return OMX_ErrorNone;
}

OMX_CALLBACKTYPE callbacks  = { .EventHandler = cEventHandler,
                                .EmptyBufferDone = cEmptyBufferDone,
};

void printState() {
    OMX_STATETYPE state;
    err = OMX_GetState(handle, &state);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Error on getting state\n");
        exit(1);
    }
    switch (state) {
    case OMX_StateLoaded: fprintf(stderr, "StateLoaded\n"); break;
    case OMX_StateIdle: fprintf(stderr, "StateIdle\n"); break;
    case OMX_StateExecuting: fprintf(stderr, "StateExecuting\n"); break;
    case OMX_StatePause: fprintf(stderr, "StatePause\n"); break;
    case OMX_StateWaitForResources: fprintf(stderr, "StateWiat\n"); break;
    default:  fprintf(stderr, "State unknown\n"); break;
    }
}

static void setHeader(OMX_PTR header, OMX_U32 size) {
    /* header->nVersion */
    OMX_VERSIONTYPE* ver = (OMX_VERSIONTYPE*)(header + sizeof(OMX_U32));
    /* header->nSize */
    *((OMX_U32*)header) = size;

    /* for 1.2
       ver->s.nVersionMajor = OMX_VERSION_MAJOR;
       ver->s.nVersionMinor = OMX_VERSION_MINOR;
       ver->s.nRevision = OMX_VERSION_REVISION;
       ver->s.nStep = OMX_VERSION_STEP;
    */
    ver->s.nVersionMajor = specVersion.s.nVersionMajor;
    ver->s.nVersionMinor = specVersion.s.nVersionMinor;
    ver->s.nRevision = specVersion.s.nRevision;
    ver->s.nStep = specVersion.s.nStep;
}

/**
 * Disable unwanted ports, or we can't transition to Idle state
 */
void disablePort(OMX_INDEXTYPE paramType) {
    OMX_PORT_PARAM_TYPE param;
    int nPorts;
    int startPortNumber;
    int n;

    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));
    err = OMX_GetParameter(handle, paramType, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting OMX_PORT_PARAM_TYPE parameter\n", 0);
        exit(1);
    }
    startPortNumber = ((OMX_PORT_PARAM_TYPE)param).nStartPortNumber;
    nPorts = ((OMX_PORT_PARAM_TYPE)param).nPorts;
    if (nPorts > 0) {
        fprintf(stderr, "Other has %d ports\n", nPorts);
        /* and disable it */
        for (n = 0; n < nPorts; n++) {
            err = OMX_SendCommand(handle, OMX_CommandPortDisable, n + startPortNumber, NULL);
            if (err != OMX_ErrorNone) {
                fprintf(stderr, "Error on setting port to disabled\n");
                exit(1);
            }
        }
    }
}

#ifdef RASPBERRY_PI
/* For the RPi name can be "hdmi" or "local" */
void setOutputDevice(const char *name) {
   int32_t success = -1;
   OMX_CONFIG_BRCMAUDIODESTINATIONTYPE arDest;

   if (name && strlen(name) < sizeof(arDest.sName)) {
       setHeader(&arDest, sizeof(OMX_CONFIG_BRCMAUDIODESTINATIONTYPE));
       strcpy((char *)arDest.sName, name);

       err = OMX_SetParameter(handle, OMX_IndexConfigBrcmAudioDestination, &arDest);
       if (err != OMX_ErrorNone) {
           fprintf(stderr, "Error on setting audio destination\n");
           exit(1);
       }
   }
}
#endif

void setPCMMode(int startPortNumber) {
    OMX_AUDIO_PARAM_PCMMODETYPE sPCMMode;

    setHeader(&sPCMMode, sizeof(OMX_AUDIO_PARAM_PCMMODETYPE));
    sPCMMode.nPortIndex = startPortNumber;
    sPCMMode.nSamplingRate = 48000;
    sPCMMode.nChannels;

    err = OMX_SetParameter(handle, OMX_IndexParamAudioPcm, &sPCMMode);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "PCM mode unsupported\n");
        return;
    } else {
        fprintf(stderr, "PCM mode supported\n");
        fprintf(stderr, "PCM sampling rate %d\n", sPCMMode.nSamplingRate);
        fprintf(stderr, "PCM nChannels %d\n", sPCMMode.nChannels);
    }
}

int main(int argc, char** argv) {

    OMX_PORT_PARAM_TYPE param;
    OMX_PARAM_PORTDEFINITIONTYPE sPortDef;
    OMX_AUDIO_PORTDEFINITIONTYPE sAudioPortDef;
    OMX_AUDIO_PARAM_PORTFORMATTYPE sAudioPortFormat;
    OMX_AUDIO_PARAM_PCMMODETYPE sPCMMode;
    OMX_BUFFERHEADERTYPE **inBuffers;

#ifdef RASPBERRY_PI
    char *componentName = "OMX.broadcom.audio_render";
#endif
#ifdef LIM
    char *componentName = "OMX.limoi.alsa_sink";
#endif
    unsigned char name[OMX_MAX_STRINGNAME_SIZE];
    OMX_UUIDTYPE uid;
    int startPortNumber;
    int nPorts;
    int n;

# ifdef RASPBERRY_PI
    bcm_host_init();
# endif

    fprintf(stderr, "Thread id is %p\n", pthread_self());
    if(argc < 2){
        display_help();
        exit(1);
    }

    fd = open(argv[1], O_RDONLY);
    if(fd < 0){
        perror("Error opening input file\n");
        exit(1);
    }
    filesize = getFileSize(fd);

    err = OMX_Init();
    if(err != OMX_ErrorNone) {
        fprintf(stderr, "OMX_Init() failed\n", 0);
        exit(1);
    }
    /** Ask the core for a handle to the audio render component
     */
    err = OMX_GetHandle(&handle, componentName, NULL /*app private data */, &callbacks);
    if(err != OMX_ErrorNone) {
        fprintf(stderr, "OMX_GetHandle failed\n", 0);
        exit(1);
    }
    err = OMX_GetComponentVersion(handle, name, &compVersion, &specVersion, &uid);
    if(err != OMX_ErrorNone) {
        fprintf(stderr, "OMX_GetComponentVersion failed\n", 0);
        exit(1);
    }

    /** disable other ports */
    disablePort(OMX_IndexParamOtherInit);

    /** Get audio port information */
    setHeader(&param, sizeof(OMX_PORT_PARAM_TYPE));
    err = OMX_GetParameter(handle, OMX_IndexParamAudioInit, &param);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting OMX_PORT_PARAM_TYPE parameter\n", 0);
        exit(1);
    }
    startPortNumber = ((OMX_PORT_PARAM_TYPE)param).nStartPortNumber;
    nPorts = ((OMX_PORT_PARAM_TYPE)param).nPorts;
    if (nPorts > 1) {
        fprintf(stderr, "Render device has more than one port\n");
        exit(1);
    }

    /* Get and check port information */
    setHeader(&sPortDef, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
    sPortDef.nPortIndex = startPortNumber;
    err = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in getting OMX_PORT_DEFINITION_TYPE parameter\n", 0);
        exit(1);
    }
    if (sPortDef.eDomain != OMX_PortDomainAudio) {
        fprintf(stderr, "Port %d is not an audio port\n", startPortNumber);
        exit(1);
    }

    if (sPortDef.eDir != OMX_DirInput) {
        fprintf(stderr, "Port is not an input port\n");
        exit(1);
    }
    if (sPortDef.format.audio.eEncoding == OMX_AUDIO_CodingPCM) {
        fprintf(stderr, "Port encoding is PCM\n");
    }    else {
        fprintf(stderr, "Port has unknown encoding\n");
    }

    /* Create minimum number of buffers for the port */
    nBuffers = sPortDef.nBufferCountActual = sPortDef.nBufferCountMin;
    fprintf(stderr, "Number of bufers is %d\n", nBuffers);
    err = OMX_SetParameter(handle, OMX_IndexParamPortDefinition, &sPortDef);
    if(err != OMX_ErrorNone){
        fprintf(stderr, "Error in setting OMX_PORT_PARAM_TYPE parameter\n", 0);
        exit(1);
    }
    if (sPortDef.bEnabled) {
        fprintf(stderr, "Port is enabled\n");
    } else {
        fprintf(stderr, "Port is not enabled\n");
    }

    /* call to put state into idle before allocating buffers */
    err = OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateIdle, NULL);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Error on setting state to idle\n");
        exit(1);
    }

    err = OMX_SendCommand(handle, OMX_CommandPortEnable, startPortNumber, NULL);
    if (err != OMX_ErrorNone) {
        fprintf(stderr, "Error on setting port to enabled\n");
        exit(1);
    }

    /* Configure buffers for the port */
    nBufferSize = sPortDef.nBufferSize;
    fprintf(stderr, "%d buffers of size is %d\n", nBuffers, nBufferSize);

    inBuffers = malloc(nBuffers * sizeof(OMX_BUFFERHEADERTYPE *));
    if (inBuffers == NULL) {
        fprintf(stderr, "Can't allocate buffers\n");
        exit(1);
    }

    for (n = 0; n < nBuffers; n++) {
        err = OMX_AllocateBuffer(handle, inBuffers+n, startPortNumber, NULL,
                                 nBufferSize);
        if (err != OMX_ErrorNone) {
            fprintf(stderr, "Error on AllocateBuffer in 1%i\n", err);
            exit(1);
        }
    }
    /* Make sure we've reached Idle state */
    waitFor(OMX_StateIdle);

    /* Now try to switch to Executing state */
    err = OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateExecuting, NULL);
    if(err != OMX_ErrorNone){
        exit(1);
    }

    /* One buffer is the minimum for Broadcom component, so use that */
    pEmptyBuffer = inBuffers[0];
    emptyState = 1;
    /* Fill and empty buffer */
    for (;;) {
        int data_read = read(fd, pEmptyBuffer->pBuffer, nBufferSize);
        pEmptyBuffer->nFilledLen = data_read;
        pEmptyBuffer->nOffset = 0;
        filesize -= data_read;
        if (data_read <= 0) {
            fprintf(stderr, "In the %s no more input data available\n", __func__);
            pEmptyBuffer->nFilledLen=0;
            pEmptyBuffer->nFlags = OMX_BUFFERFLAG_EOS;
            bEOS=OMX_TRUE;
        }
        fprintf(stderr, "Emptying again buffer %p %d bytes, %d to go\n", pEmptyBuffer, data_read, filesize);
        err = OMX_EmptyThisBuffer(handle, pEmptyBuffer);
        waitForEmpty();
        fprintf(stderr, "Waited for empty\n");
        if (bEOS) {
            fprintf(stderr, "Exiting loop\n");
            break;
        }
    }
    fprintf(stderr, "Buffers emptied\n");
    exit(0);
}

结论

Khronos 集团已经为低性能系统中的音频和视频制定了规范。这些目前被 Android 和 Raspberry Pi 使用。本章已经给出了这些规范和一些示例程序的介绍性概述。LIM 包自 2012 年以来就没有更新过,而 Bellagio 包自 2011 年以来就没有更新过,所以它们似乎没有得到积极的维护。另一方面,RPi 正在蓬勃发展,使用 GPU 的 OpenMAX 编程在我的书《Raspberry Pi GPU 音频视频编程》中有详细介绍。

十四、LADSPA

Linux Audio Plug-Ins (LADSPA)是一组插件,应用可以使用它们来添加延迟和滤镜等效果。它的设计考虑到了简单性,所以只能产生有限的效果。然而,这些可以是相当广泛的,并足以用于各种各样的应用。

资源

以下是一些资源:

用户级工具

LADSPA 插件位于默认为/usr/lib/ladspa的目录中。这可以通过环境变量LADSPA_PATH来控制。这个目录将包含一组作为 LADSPA 插件的.so文件。

每个插件都包含关于自身的信息,您可以通过运行命令行工具listplugins来检查插件集。通过只安装 LADPSA,缺省插件如下:

/usr/lib/ladspa/amp.so:
        Mono Amplifier (1048/amp_mono)
        Stereo Amplifier (1049/amp_stereo)
/usr/lib/ladspa/delay.so:
        Simple Delay Line (1043/delay_5s)
/usr/lib/ladspa/filter.so:
        Simple Low Pass Filter (1041/lpf)
        Simple High Pass Filter (1042/hpf)
/usr/lib/ladspa/sine.so:
        Sine Oscillator (Freq:audio, Amp:audio) (1044/sine_faaa)
        Sine Oscillator (Freq:audio, Amp:control) (1045/sine_faac)
        Sine Oscillator (Freq:control, Amp:audio) (1046/sine_fcaa)
        Sine Oscillator (Freq:control, Amp:control) (1047/sine_fcac)
/usr/lib/ladspa/noise.so:
        White Noise Source (1050/noise_white)

您可以从工具analyseplugin中找到关于每个插件的更多详细信息。例如,下面是amp插件的信息:

$analyseplugin amp

Plugin Name: "Mono Amplifier"
Plugin Label: "amp_mono"
Plugin Unique ID: 1048
Maker: "Richard Furse (LADSPA example plugins)"
Copyright: "None"
Must Run Real-Time: No
Has activate() Function: No
Has deactivate() Function: No
Has run_adding() Function: No
Environment: Normal or Hard Real-Time
Ports:  "Gain" input, control, 0 to ..., default 1, logarithmic
        "Input" input, audio
        "Output" output, audio

Plugin Name: "Stereo Amplifier"
Plugin Label: "amp_stereo"
Plugin Unique ID: 1049
Maker: "Richard Furse (LADSPA example plugins)"
Copyright: "None"
Must Run Real-Time: No
Has activate() Function: No
Has deactivate() Function: No
Has run_adding() Function: No
Environment: Normal or Hard Real-Time
Ports:  "Gain" input, control, 0 to ..., default 1, logarithmic
        "Input (Left)" input, audio
        "Output (Left)" output, audio
        "Input (Right)" input, audio
        "Output (Right)" output, audio

可以使用applyplugin对每个插件进行简单的测试。当不带参数运行时,它会给出一条用法消息。

$applyplugin
Usage:  applyplugin [flags] <input Wave file> <output Wave file>
        <LADSPA plugin file name> <plugin label> <Control1> <Control2>...
        [<LADSPA plugin file name> <plugin label> <Control1> <Control2>...]...
Flags:  -s<seconds>  Add seconds of silence after end of input file.

这将输入和输出 WAV 文件作为第一个和第二个参数。接下来是.so文件的名称和选择的插件标签。接下来是控件的值。对于amp插件,文件名为amp.so,立体声插件为amp_stereo,增益只有一个控制,取值范围为 0-1。要将包含立体声 WAV 数据的文件的音量减半,请使用:

applyplugin 54154.wav tmp.wav amp.so amp_stereo 0.5

LADSPA_Descriptor 类型

应用和 LADSPA 插件之间的通信通过类型为LADSPA_Descriptor的数据结构进行。它的字段包含了由listpluginsanalyseplugins显示的所有信息。此外,它还包含控制内存布局、是否支持硬实时等等的字段。

unsigned long UniqueID

  • 每个插件在 LADSPA 系统中必须有一个唯一的 ID。

const char * Label

  • 这是用于指代 LADSPA 系统内插件的标签。

const char * Name

  • 这是插件的“用户友好”名称。例如,amp文件(稍后显示)包含两个插件。单声道放大器的 ID 为 1048,标签为amp_mono,命名为单声道放大器,而立体声放大器的 ID 为 1049,标签为amp_stereo,命名为立体声放大器。

const char * Maker, * Copyright

  • 这应该很明显。

unsigned long PortCount

  • 这表示插件上存在的端口(输入和输出)的数量。

const``LADSPA_PortDescriptor

  • 这个成员表示一个端口描述符数组。有效索引从0PortCount-1不等。

const char * const * PortNames

  • 此成员指示描述端口的空终止字符串数组。例如,单声道放大器有两个输入端口和一个输出端口,分别标记为增益、输入和输出。输入端口有端口描述符(LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO),而输出端口有端口描述符(LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO)

LADSPA_PortRangeHint * PortRangeHints

  • 这是一个类型为LADSPA_PortRangeHint的数组,每个端口一个元素。这允许插件传递信息,比如它是否有一个上界或下界的值,如果有,这个上界是什么,它是否应该被当作一个布尔值,等等。比方说,GUI 可以使用这些提示为插件提供可视化的控制显示。

此外,它还包含作为函数指针的字段,LADSPA 运行时调用这些字段来初始化插件、处理数据和清理。这些字段如下:

instantiate

  • 这将采样速率作为一个参数。它负责插件的一般实例化、设置本地参数、分配内存等等。它返回一个指向特定于插件的数据结构的指针,该数据结构包含与该插件相关的所有信息。这个指针将作为第一个参数传递给其他函数,以便它们可以检索这个插件的信息。

connect_port

  • 这需要三个参数,第二个和第三个分别是端口号和数据可读/可写的地址。对于每个端口,插件只能使用该地址从 LADSPA 运行时读取/写入数据。它将在runrun_adding之前被调用。

activate/deactivate

  • 可以调用这些函数来重新初始化插件状态。他们可能是NULL

run

  • 这个函数是插件完成所有实际工作的地方。它的第二个参数是准备好读/写的样本数。

cleanup

  • 这是显而易见的。

其他功能字段通常设置为NULL

加载插件

应用可以通过用一个参数调用loadLADSPAPluginLibrary来加载插件,这个参数是插件文件的名称。请注意,没有 LADSPA 库。LADPSA 提供了一个名为ladspa.h的头文件,发行版可能包含一个文件load.c,它实现了loadLADSPAPluginLibrary(它搜索LADSPA_PATH中的目录)。

当插件被dlopen加载时,函数_init被无参数调用。这可能用于设置插件和构建,例如,LADSPA_Descriptor

DLL 必须有一个可以挂接的入口点。对于 LADSPA,每个插件必须定义一个函数LADSPA_Descriptor * ladspa_descriptor(unsigned long Index)。索引 0、1…的值是文件中包含的每个插件的LADSPA_Descriptor值。

单声道放大器客户端

analyseplugin amp命令显示amp插件包含两个插件模块:一个单声道和一个立体声插件。单声道插件有一个插件标签amp_mono,对应一个LADSPA_DescriptorLabel字段。

使用这个插件意味着您必须加载插件文件,获得一个ladspa_descriptor结构的句柄,然后浏览描述符,检查标签,直到找到amp_mono插件。

加载插件文件是通过 LADSPA 包中的load.c程序中的函数来完成的。相关代码如下:

    char *pcPluginFilename = "amp.so";
    void *pvPluginHandle = loadLADSPAPluginLibrary(pcPluginFilename);
    dlerror();

    pfDescriptorFunction
        = (LADSPA_Descriptor_Function)dlsym(pvPluginHandle, "ladspa_descriptor");
    if (!pfDescriptorFunction) {
        const char * pcError = dlerror();
        if (pcError)
            fprintf(stderr,
                    "Unable to find ladspa_descriptor() function in plugin file "
                    "\"%s\": %s.\n"
                    "Are you sure this is a LADSPA plugin file?\n",
                    pcPluginFilename,
                    pcError);
        return 1;
    }

加载后,搜索amp_mono插件:

    char *pcPluginLabel = "amp_mono";
    for (lPluginIndex = 0;; lPluginIndex++) {
        psDescriptor = pfDescriptorFunction(lPluginIndex);
        if (!psDescriptor)
            break;
        if (pcPluginLabel != NULL) {
            if (strcmp(pcPluginLabel, psDescriptor->Label) != 0)
                continue;
        }
        // got mono_amp

您知道有三个端口——控制、输入和输出——所以您查看端口列表来分配索引并将相关数组连接到插件描述符。

隐藏在这里的是一个关键部分:您不仅要设置插件的输入和输出,还要设置控制机制。analyseplugin报告显示有一个带控制的增益端口。这需要输入。控制端口只需要一个浮点值的地址,这是将要发生的放大量。这是通过以下代码完成的:

        handle = psDescriptor->instantiate(psDescriptor, SAMPLE_RATE);
        if (handle == NULL) {
            fprintf(stderr, "Can't instantiate plugin %s\n", pcPluginLabel);
            exit(1);
        }

        // get ports
        int lPortIndex;
        printf("Num ports %lu\n", psDescriptor->PortCount);
        for (lPortIndex = 0;
             lPortIndex < psDescriptor->PortCount;
             lPortIndex++) {
            if (LADSPA_IS_PORT_INPUT
                (psDescriptor->PortDescriptors[lPortIndex])
                && LADSPA_IS_PORT_AUDIO
                (psDescriptor->PortDescriptors[lPortIndex])) {
                printf("input %d\n", lPortIndex);
                lInputPortIndex = lPortIndex;

                psDescriptor->connect_port(handle,
                                           lInputPortIndex, pInBuffer);
            } else if (LADSPA_IS_PORT_OUTPUT
                       (psDescriptor->PortDescriptors[lPortIndex])
                       && LADSPA_IS_PORT_AUDIO
                       (psDescriptor->PortDescriptors[lPortIndex])) {
                printf("output %d\n", lPortIndex);
                lOutputPortIndex = lPortIndex;

                psDescriptor->connect_port(handle,
                                           lOutputPortIndex, pOutBuffer);
            }

            if (LADSPA_IS_PORT_CONTROL
                (psDescriptor->PortDescriptors[lPortIndex])) {
                printf("control %d\n", lPortIndex);
                LADSPA_Data control = 0.5f; // here is where we say to halve the volume
                psDescriptor->connect_port(handle,
                                           lPortIndex, &control);
            }
        }
        // we've got what we wanted

然后,run_plugin函数开始循环,从输入文件中读取样本,应用插件的run函数,并写入输出文件。

void run_plugin() {
    sf_count_t numread;

    open_files();

    // it's NULL for the amp plugin
    if (psDescriptor->activate != NULL)
        psDescriptor->activate(handle);

    while ((numread = fill_input_buffer()) > 0) {
        printf("Num read %d\n", numread);
        psDescriptor->run(handle, numread);
        empty_output_buffer(numread);
    }
}

我已经使用了 libsndfile 库,通过使用fill_input_bufferempty_output_buffer来简化任何格式的文件的读写。

完整的程序称为mono_amp.c,如下所示:

#include <stdlib.h>
#include <stdio.h>
#include <ladspa.h>
#include <dlfcn.h>
#include <sndfile.h>

#include "utils.h"

const LADSPA_Descriptor * psDescriptor;
LADSPA_Descriptor_Function pfDescriptorFunction;
LADSPA_Handle handle;

// choose the mono plugin from the amp file
char *pcPluginFilename = "amp.so";
char *pcPluginLabel = "amp_mono";

long lInputPortIndex = -1;
long lOutputPortIndex = -1;

SNDFILE* pInFile;
SNDFILE* pOutFile;

// for the amplifier, the sample rate doesn't really matter
#define SAMPLE_RATE 44100

// the buffer size isn't really important either
#define BUF_SIZE 2048
LADSPA_Data pInBuffer[BUF_SIZE];
LADSPA_Data pOutBuffer[BUF_SIZE];

// How much we are amplifying the sound by
LADSPA_Data control = 0.5f;

char *pInFilePath = "/home/local/antialize-wkhtmltopdf-7cb5810/scripts/static-build/linux-local/qts/demos/mobile/quickhit/plugins/LevelTemplate/sound/enableship.wav";
char *pOutFilePath = "tmp.wav";

void open_files() {
    // using libsndfile functions for easy read/write
    SF_INFO sfinfo;

    sfinfo.format = 0;
    pInFile = sf_open(pInFilePath, SFM_READ, &sfinfo);
    if (pInFile == NULL) {
        perror("can't open input file");
        exit(1);
    }

    pOutFile = sf_open(pOutFilePath, SFM_WRITE, &sfinfo);
    if (pOutFile == NULL) {
        perror("can't open output file");
        exit(1);
    }
}

sf_count_t fill_input_buffer() {
    return sf_read_float(pInFile, pInBuffer, BUF_SIZE);
}

void empty_output_buffer(sf_count_t numread) {
    sf_write_float(pOutFile, pOutBuff
er, numread);
}

void run_plugin() {
    sf_count_t numread;

    open_files();

    // it's NULL for the amp plugin
    if (psDescriptor->activate != NULL)
        psDescriptor->activate(handle);

    while ((numread = fill_input_buffer()) > 0) {
        printf("Num read %d\n", numread);
        psDescriptor->run(handle, numread);
        empty_output_buffer(numread);
    }
}

int main(int argc, char *argv[]) {
    int lPluginIndex;

    void *pvPluginHandle = loadLADSPAPluginLibrary(pcPluginFilename);
    dlerror();

    pfDescriptorFunction
        = (LADSPA_Descriptor_Function)dlsym(pvPluginHandle, "ladspa_descriptor");
    if (!pfDescriptorFunction) {
        const char * pcError = dlerror();
        if (pcError)
            fprintf(stderr,
                    "Unable to find ladspa_descriptor() function in plugin file "
                    "\"%s\": %s.\n"
                    "Are you sure this is a LADSPA plugin file?\n",
                    pcPluginFilename,
                    pcError);
        return 1;
    }

    for (lPluginIndex = 0;; lPluginIndex++) {
        psDescriptor = pfDescriptorFunction(lPluginIndex);
        if (!psDescriptor)
            break;
        if (pcPluginLabel != NULL) {
            if (strcmp(pcPluginLabel, psDescriptor->Label) != 0)
                continue;
        }
        // got mono_amp

        handle = psDescriptor->instantiate(psDescriptor, SAMPLE_RATE);
        if (handle == NULL) {
            fprintf(stderr, "Can't instantiate plugin %s\n", pcPluginLabel);
            exit(1);

        }

        // get ports
        int lPortIndex;
        printf("Num ports %lu\n", psDescriptor->PortCount);
        for (lPortIndex = 0;
             lPortIndex < psDescriptor->PortCount;
             lPortIndex++) {
            if (LADSPA_IS_PORT_INPUT
                (psDescriptor->PortDescriptors[lPortIndex])
                && LADSPA_IS_PORT_AUDIO
                (psDescriptor->PortDescriptors[lPortIndex])) {
                printf("input %d\n", lPortIndex);
                lInputPortIndex = lPortIndex;

                psDescriptor->connect_port(handle,
                                           lInputPortIndex, pInBuffer);
            } else if (LADSPA_IS_PORT_OUTPUT
                       (psDescriptor->PortDescriptors[lPortIndex])
                       && LADSPA_IS_PORT_AUDIO
                       (psDescriptor->PortDescriptors[lPortIndex])) {
                printf("output %d\n", lPortIndex);
                lOutputPortIndex = lPortIndex;

                psDescriptor->connect_port(handle,
                                           lOutputPortIndex, pOutBuffer);
            }

            if (LADSPA_IS_PORT_CONTROL
                (psDescriptor->PortDescriptors[lPortIndex])) {
                printf("control %d\n", lPortIndex);
                psDescriptor->connect_port(handle,
                                           lPortIndex, &control);
            }
        }
        // we've got what we wanted, get out of this loop
        break;
    }

    if ((psDescriptor == NULL) ||
        (lInputPortIndex == -1) ||
        (lOutputPortIndex == -1)) {
        fprintf(stderr, "Can't find plugin information\n");
        exit(1);
    }

    run_plugin();

    exit(0);
}

它只是通过调用mono_amp来运行,没有参数,因为输入和输出文件是硬编码到程序中的。

具有图形用户界面的立体声放大器

amp文件包含一个立体声放大器和一个单声道放大器。这导致了管理插件的几个差异。现在有两个输入端口和两个输出端口,但是仍然只有一个用于放大系数的控制端口。您需要一组输入端口和一组输出端口。这只是增加了一点复杂性。

主要区别在于处理流:libsndfile返回声音帧,立体声信号的两个声道交错。对于每个输入端口,这些必须被分离到单独的通道中,然后两个输出端口必须交错在一起。

添加像 GTK 这样的 GUI 相当简单。下面的代码只是显示了一个滑块来控制音量。GUI 代码和 LADSPA 代码显然必须在不同的(POSIX)线程中运行。实际上只有一个棘手的问题:在执行run函数的过程中,控制值不应该改变。这可以用锁来保护,但是在这种情况下,这太重了:只需保存一份被滑块修改过的控件的副本,并在每次调用run之前把它带过来。

代码是使用 GTK v3 编写的,如下所示:

#include <gtk/gtk.h>

#include <stdlib.h>
#include <stdio.h>
#include <ladspa.h>
#include <dlfcn.h>
#include <sndfile.h>

#include "utils.h"

gint count = 0;
char buf[5];

pthread_t ladspa_thread;

const LADSPA_Descriptor * psDescriptor;
LADSPA_Descriptor_Function pfDescriptorFunction;
LADSPA_Handle handle;

// choose the mono plugin from the amp file
char *pcPluginFilename = "amp.so";
char *pcPluginLabel = "amp_stereo";

long lInputPortIndex = -1;
long lOutputPortIndex = -1;

int inBufferIndex = 0;
int outBufferIndex = 0;

SNDFILE* pInFile;
SNDFILE* pOutFile;

// for the amplifier, the sample rate doesn't really matter
#define SAMPLE_RATE 44100

// the buffer size isn't really important either
#define BUF_SIZE 2048
LADSPA_Data pInStereoBuffer[2*BUF_SIZE];
LADSPA_Data pOutStereoBuffer[2*BUF_SIZE];
LADSPA_Data pInBuffer[2][BUF_SIZE];
LADSPA_Data pOutBuffer[2][BUF_SIZE];

// How much we are amplifying the sound by
// We aren't allowed to change the control values
// during execution of run(). We could put a lock
// around run() or simpler, change the value of
// control only outside of run()
LADSPA_Data control;
LADSPA_Data pre_control = 0.2f;

char *pInFilePath = "/home/newmarch/Music/karaoke/nights/nightsinwhite-0.wav";
char *pOutFilePath = "tmp.wav";

void open_files() {
    // using libsndfile functions for easy read/write
    SF_INFO sfinfo;

    sfinfo.format = 0;
    pInFile = sf_open(pInFilePath, SFM_READ, &sfinfo);
    if (pInFile == NULL) {
        perror("can't open input file");
        exit(1);
    }

    pOutFile = sf_open(pOutFilePath, SFM_WRITE, &sfinfo);
    if (pOutFile == NULL) {
        perror("can't open output file");
        exit(1);
    }
}

sf_count_t fill_input_buffer() {
    int numread = sf_read_float(pInFile, pInStereoBuffer, 2*BUF_SIZE);

    // split frames into samples for each channel
    int n;
    for (n = 0; n < numread; n += 2) {
        pInBuffer[0][n/2] = pInStereoBuffer[n];
        pInBuffer[1][n/2] = pInStereoBuffer[n+1];
    }
    return numread/2;
}

void empty_output_buffer(sf_count_t numread) {
    // combine output samples back into frames
    int n;
    for (n = 0; n < 2*numread; n += 2) {
        pOutStereoBuffer[n] = pOutBuffer[0][n/2];
        pOutStereoBuffer[n+1] = pOutBuffer[1][n/2];
    }

    sf_write_float(pOutFile, pOutStereoBuffer, 2*numread);
}

gpointer run_plugin(gpointer args) {
    sf_count_t numread;

    // it's NULL for the amp plugin
    if (psDescriptor->activate != NULL)
        psDescriptor->activate(handle);

    while ((numread = fill_input_buffer()) > 0) {
        // reset control outside of run()
        control = pre_control;

        psDescriptor->run(handle, numread);
        empty_output_buffer(numread);
        usleep(1000);
    }
    printf("Plugin finished!\n");
}

void setup_ladspa() {
    int lPluginIndex;

    void *pvPluginHandle = loadLADSPAPluginLibrary(pcPluginFilename);
    dlerror();

    pfDescriptorFunction
        = (LADSPA_Descriptor_Function)dlsym(pvPluginHandle, "ladspa_descriptor");
    if (!pfDescriptorFunction) {
        const char * pcError = dlerror();
        if (pcError)
            fprintf(stderr,
                    "Unable to find ladspa_descriptor() function in plugin file "
                    "\"%s\": %s.\n"
                    "Are you sure this is a LADSPA plugin file?\n",
                    pcPluginFilename,
                    pcError);
        exit(1);
    }

    for (lPluginIndex = 0;; lPluginIndex++) {
        psDescriptor = pfDescriptorFunction(lPluginIndex);
        if (!psDescriptor)
            break;
        if (pcPluginLabel != NULL) {
            if (strcmp(pcPluginLabel, psDescriptor->Label) != 0)
                continue;
        }
        // got stero_amp

        handle = psDescriptor->instantiate(psDescriptor, SAMPLE_RATE);
        if (handle == NULL) {
            fprintf(stderr, "Can't instantiate plugin %s\n", pcPluginLabel);
            exit(1);
        }

        // get ports
        int lPortIndex;
        printf("Num ports %lu\n", psDescriptor->PortCount);
        for (lPortIndex = 0;
             lPortIndex < psDescriptor->PortCount;
             lPortIndex++) {
            if (LADSPA_IS_PORT_AUDIO
                (psDescriptor->PortDescriptors[lPortIndex])) {
                if (LADSPA_IS_PORT_INPUT
                    (psDescriptor->PortDescriptors[lPortIndex])) {
                    printf("input %d\n", lPortIndex);
                    lInputPortIndex = lPortIndex;

                    psDescriptor->connect_port(handle,
                                               lInputPortIndex, pInBuffer[inBufferIndex++]);
                } else if (LADSPA_IS_PORT_OUTPUT
                           (psDescriptor->PortDescriptors[lPortIndex])) {
                    printf("output %d\n", lPortIndex);
                    lOutputPortIndex = lPortIndex;

                    psDescriptor->connect_port(handle,
                                               lOutputPortIndex, pOutBuffer[outBufferIndex++]);
                }
            }

            if (LADSPA_IS_PORT_CONTROL
                (psDescriptor->PortDescriptors[lPortIndex])) {
                printf("control %d\n", lPortIndex);
                psDescriptor->connect_port(handle,
                                           lPortIndex, &control);
            }
        }
        // we've got what we wanted, get out of this loop
        break;
    }

    if ((psDescriptor == NULL) ||
        (lInputPortIndex == -1) ||
        (lOutputPortIndex == -1)) {
        fprintf(stderr, "Can't find plugin information\n");
        exit(1);
    }

    open_files();

    pthread_create(&ladspa_thread, NULL, run_plugin, NULL);
}

void slider_change(GtkAdjustment *adj,  gpointer data)
{
    count++;

    pre_control = gtk_adjustment_get_value(adj);
    //gtk_label_set_text(GTK_LABEL(label), buf);
}

int main(int argc, char** argv) {

    //GtkWidget *label;
    GtkWidget *window;
    GtkWidget *frame;
    GtkWidget *slider;
    GtkAdjustment *adjustment;

    setup_ladspa();

    gtk_init(&argc, &argv);

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
    gtk_window_set_default_size(GTK_WINDOW(window), 250, 80);
    gtk_window_set_title(GTK_WINDOW(window), "Volume");

    frame = gtk_fixed_new();
    gtk_container_add(GTK_CONTAINER(window), frame);

    adjustment = gtk_adjustment_new(1.0,
                               0.0,
                               2.0,
                               0.1,
                               1.0,
                               0.0);
    slider = gtk_scale_new(GTK_ORIENTATION_HORIZONTAL,
                         adjustment);
    gtk_widget_set_size_request(slider, 240, 5);
    gtk_fixed_put(GTK_FIXED(frame), slider, 5, 20);

    //label = gtk_label_new("0");
    //gtk_fixed_put(GTK_FIXED(frame), label, 190, 58);

    gtk_widget_show_all(window);

    g_signal_connect(window, "destroy",
                     G_CALLBACK (gtk_main_quit), NULL);

    g_signal_connect(adjustment, "value-changed",
                     G_CALLBACK(slider_change), NULL);

    gtk_main();

    return 0;
}

它只是通过调用stereo_amp来运行,没有参数。

amp 计划

您在最后两节中调用的程序是amp程序,它在 LADSPA 源代码的文件ladspa_sdk/src/plugins/amp.c中。如果您想自己编写一个 LADSPA 插件,或者想了解其中涉及的内容,这是值得研究的。有几个关键功能。

  • DLL 加载程序调用函数_init()。它的作用主要是为每个插件组件设置一个LADSPA_Descriptor。这是长篇大论。它包括所有可由analyseplugin打印的信息,例如:

    g_psMonoDescriptor->Name = strdup("Mono Amplifier");
    
    

    它还包含内部函数指针,例如当单声道放大器需要做一些工作时运行的函数。

    g_psMonoDescriptor->run = runMonoAmplifier;
    
    
  • 卸载插件时,调用函数_fini()来清理所有数据。

插件的核心是在处理样本时对样本做什么。输入样本包含在一个缓冲器中,输出样本包含在另一个缓冲器中,对于单声道放大器,每个输入样本都需要乘以增益系数才能得到输出样本。代码如下:

void
runMonoAmplifier(LADSPA_Handle Instance,
                 unsigned long SampleCount) {

  LADSPA_Data * pfInput;
  LADSPA_Data * pfOutput;
  LADSPA_Data fGain;
  Amplifier * psAmplifier;
  unsigned long lSampleIndex;

  psAmplifier = (Amplifier *)Instance;

  pfInput = psAmplifier->m_pfInputBuffer1;
  pfOutput = psAmplifier->m_pfOutputBuffer1;
  fGain = *(psAmplifier->m_pfControlValue);

  for (lSampleIndex = 0; lSampleIndex *lt; SampleCount; lSampleIndex++)
    *(pfOutput++) = *(pfInput++) * fGain;
}

结论

LADSPA 是音效插件的常用框架。本章介绍了一些命令行工具和编程模型。

十五、使用 Gtk 和 FFmpeg 以叠加方式显示视频

这一章与声音无关。视频通常伴随着音频。Karaoke 经常在视频上覆盖歌词。构建一个包含视频和音频的应用会将您带入图形用户界面(GUI)的领域。这本身就是一个复杂的领域,值得(而且已经!)很多书,包括我自己多年前写的关于 X Window 系统和 Motif 的书。这一章是关于编程的视频方面,使用 FFmpeg,Gtk,Cairo 和 Pango。我假设您熟悉窗口小部件、事件、事件处理程序等概念,它们是当前所有 GUI 框架的基础。

Motif 很久以前就失去了作为 Linux/Unix 系统主要 GUI 的地位。现在有很多替代方案,包括 Gtk(Gimp 工具包)、tcl/Tk、Java Swing、KDE、XFCE 等等。每一种都有自己的追随者、使用领域、怪癖、特质等等。没有一个单一的 GUI 能满足所有人。

在这一章中,我处理 Gtk。原因有三。

  • 它有一个 C 库。它也有一个 Python 库,这很好,我可能有一天会用到它。最重要的是,它不是基于 C++的。C++是我最不喜欢的语言之一。我曾经碰到过一句名言(source lost)“c++是一个逃脱的实验室实验”,我完全同意那个评价。
  • 对 i18n(国际化)有很好的支持。我希望能够播放中文 Karaoke 文件,所以这对我很重要。
  • 它不是基于 Java 的。不要误解我,我真的很喜欢 Java,并且已经用它编程很多年了。MIDI API 相当不错,当然其他东西比如 i18n 也很棒。但是对于 MIDI 来说,它是一个 CPU 占用率很高的设备,不能在低功耗设备上使用,比如 Raspberry Pi,而且通常音频/视频 API 已经多年没有进步了。

然而,当我努力理解 Gtk 版本 2.0 与 3.0、Cairo、Pango、Glib 等等的区别时,我认为修复 Java MIDI 引擎可能更容易!这不是一次愉快的经历,续集将会展示这一点。

FFmpeg

要播放 MPEG 文件、OGV 文件或类似文件,您需要一个解码器。主要竞争者似乎是 GStreamer 和 FFmpeg。没有特别的原因,我选择了 FFmpeg。

下面的程序读取视频文件并将前五帧存储到磁盘。直接摘自斯蒂芬·德朗格的《一个 FFmpeg 和 SDL 教程》( http://dranger.com/ffmpeg/ )。程序是play_video.c,如下图:

// tutorial01.c
// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101
// on GCC 4.7.2 in Debian February 2015

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

/* Requires
   libavcodec-dev
   libavformat-dev
   libswscale
*/

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
    FILE *pFile;
    char szFilename[32];
    int  y;

    // Open file
    sprintf(szFilename, "frame%d.ppm", iFrame);
    pFile=fopen(szFilename, "wb");
    if(pFile==NULL)
        return;

    // Write header
    fprintf(pFile, "P6\n%d %d\n255\n", width, height);

    // Write pixel data
    for(y=0; y<height; y++)
        fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);

    // Close file
    fclose(pFile);
}

main(int argc, cha
r **argv) {
    AVFormatContext *pFormatCtx = NULL;
    int i, videoStream;
    AVCodecContext *pCodecCtx = NULL;
    AVCodec *pCodec = NULL;
    AVFrame *pFrame = NULL;
    AVFrame *pFrameRGB = NULL;
    AVPacket packet;
    int frameFinished;
    int numBytes;
    uint8_t *buffer = NULL;

    AVDictionary *optionsDict = NULL;
    struct SwsContext *sws_ctx = NULL;

    if(argc < 2) {
        printf("Please provide a movie file\n");
        return -1;
    }
    // Register all formats and codecs
    av_register_all();

    // Open video file
    if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
        return -1; // Couldn't open file

    // Retrieve stream information
    if(avformat_find_stream_info(pFormatCtx, NULL)<0)
        return -1; // Couldn't find stream information

    // Dump information about file onto standard error
    av_dump_format(pFormatCtx, 0, argv[1], 0);

    // Find the first video stream
    videoStream=-1;
    for(i=0; i<pFormatCtx->nb_streams; i++)
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
            videoStream=i;
            break;
        }
    if(videoStream==-1)
        return -1; // Didn't find a video stream

    // Get a pointer to the code
c context for the video stream
    pCodecCtx=pFormatCtx->streams[videoStream]->codec;

    // Find the decoder for the video stream
    pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
    if(pCodec==NULL) {
        fprintf(stderr, "Unsupported codec!\n");
        return -1; // Codec not found
    }
    // Open codec
    if(avcodec_open2(pCodecCtx, pCodec, &optionsDict)<0)
        return -1; // Could not open codec

    // Allocate video frame
    pFrame=avcodec_alloc_frame();

    // Allocate an AVFrame structure
    pFrameRGB=avcodec_alloc_frame();
    if(pFrameRGB==NULL)
        return -1;

    // Determine required buffer size and allocate buffer
    numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                                pCodecCtx->height);
    buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

    sws_ctx =
        sws_getContext
        (
         pCodecCtx->width,
         pCodecCtx->height,
         pCodecCtx->pix_fmt,
         pCodecCtx->width,
         pCodecCtx->height,
         PIX_FMT_RGB24,
         SWS_BILINEAR,
         NULL,
         NULL,
         NULL
         );

    // Assign appropriat
e parts of buffer to image planes in pFrameRGB
    // Note that pFrameRGB is an AVFrame, but AVFrame is a superset
    // of AVPicture
    avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
                   pCodecCtx->width, pCodecCtx->height);

    // Read frames and save first five frames to disk
    i=0;
    while(av_read_frame(pFormatCtx, &packet)>=0) {
        // Is this a packet from the video stream?
        if(packet.stream_index==videoStream) {
            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
                                  &packet);

            // Did we get a video frame?
            if(frameFinished) {
                // Convert the image from its native format to RGB
                sws_scale
                    (
                     sws_ctx,
                     (uint8_t const * const *)pFrame->data,
                     pFrame->linesize,
                     0,
                     pCodecCtx->height,
                     pFrameRGB->data,
                     pFrameRGB->linesize
                     );

                printf("Read frame\n");
                // Save the frame to disk
                if(++i<=5)
                    SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height,
                              i);
                else
                    break;
            }
        }

        // Free the packet that was allocated by av_read_frame
        av_free_packet(&packet);
    }

    // Free the RGB image
    av_free(buffer);
    av_free(pFrameRGB);

    // Free the YUV frame
    av_free(pFrame);

    // Close the codec
    avcodec_close(pCodecCtx);

    // Close the video file
    avformat_close_input(&pFormatCtx);

    return 0;

}

基本 Gtk

Gtk 是一个相当标准的 GUI 工具包。简单的程序在《GTK+》(http://zetcode.com/tutorials/gtktutorial/firstprograms/)等很多教程中都有描述。有关 Gtk 编程的基础知识,请参考此类教程。

我在没有解释的情况下包括了下面的例子;它使用了三个子部件、两个按钮和一个标签。标签将保存一个整数。按钮将增加或减少这个数字。

        #include <gtk/gtk.h>

        gint count = 0;
        char buf[5];

        void increase(GtkWidget *widget, gpointer label)
        {
            count++;

            sprintf(buf, "%d", count);
            gtk_label_set_text(GTK_LABEL(label), buf);
        }

        void decrease(GtkWidget *widget, gpointer label)
        {
            count--;

            sprintf(buf, "%d", count);
            gtk_label_set_text(GTK_LABEL(label), buf);
        }

        int main(int argc, char** argv) {

            GtkWidget *label;
            GtkWidget *window;
            GtkWidget *frame;
            GtkWidget *plus;
            GtkWidget *minus;

            gtk_init(&argc, &argv);

            window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
            gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
            gtk_window_set_default_size(GTK_WINDOW(window), 250, 180);
            gtk_window_set_title(GTK_WINDOW(window), "+-");

            frame = gtk_fixed_new();
            gtk_container_add(GTK_CONTAINER(window), frame);

            plus = gtk_button_new_with_label("+");
            gtk_widget_set_size_request(plus, 80, 35);
            gtk_fixed_put(GTK_FIXED(frame), plus, 50, 20);

            minus = gtk_button_new_with_label("-");
            gtk_widget_set_size_request(minus, 80, 35);
            gtk_fixed_put(GTK_FIXED(frame), minus, 50, 80);

            label = gtk_label_new("0");
            gtk_fixed_put(GTK_FIXED(frame), label, 190, 58);

            gtk_widget_show_all(window);

            g_signal_connect(window, "destroy",
            G_CALLBACK (gtk_main_quit), NULL);

            g_signal_connect(plus, "clicked",
            G_CALLBACK(increase), label);

            g_signal_connect(minus, "clicked",
            G_CALLBACK(decrease), label);

            gtk_main();

            return 0;
        }

Gtk 和其他 GUI 工具包一样,有大量的小部件。这些都列在 GTK+ 3 参考手册中( https://developer.gnome.org/gtk3/3.0/ )。这包括小部件 GtkImage ( https://developer.gnome.org/gtk3/3.0/GtkImage.html )。顾名思义,它可以从某个地方获取一组像素,并将它们构建成可以显示的图像。

以下示例显示了从文件加载的图像:

#include <gtk/gtk.h>

int main( int argc, char *argv[])
{
    GtkWidget *window, *image;

    gtk_init(&argc, &argv);

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    image = gtk_image_new_from_file("jan-small.png");
    gtk_container_add(GTK_CONTAINER(window), image);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    gtk_widget_show(image);
    gtk_widget_show(window);
    gtk_main();

    return 0;
}

Gtk 的版本

Gtk 目前(截至 2016 年 11 月)有主要版本 2 和 3。宏GTK_MAJOR_VERSION可用于检测版本 2 或 3。然而,Gtk 还依赖于许多其他的库,并且很难确定应该查看哪些文档页面。以下是主要库及其主要 API 页面的列表:

Gtk 3 ( https://developer.gnome.org/gtk3/3.0/

Gtk 2

使用 Gtk 显示视频

比如你想把 FFmpeg 产生的图像作为AVFrame s,显示在一个GtkImage里。你不希望使用从文件中读取的代码,因为以每秒 30 帧的速度读写文件是荒谬的。相反,您希望将一些内存中的帧表示加载到GtkImage中。

这里是您遇到第一个障碍的地方:在 Gtk 2.0 和 Gtk 3.0 之间,合适的内存表示发生了不兼容的变化。我只打算用 X Window 系统的语言来说,因为我不了解其他底层系统,比如微软的 Windows。

参见“从 GTK+ 2.x 迁移到 GTK+3”(https://developer.gnome.org/gtk3/3.5/gtk-migrating-2-to-3.html)了解这些版本之间的一些变化。

像素地图

X Window 系统架构模型是一个客户机-服务器模型,它让客户机(应用)与服务器(带有图形显示和输入设备的设备)进行对话。在最低层(Xlib),客户端将向服务器发送基本请求,如“从这里到那里画一条线”。服务器将使用服务器端的信息绘制线条,例如当前线条的粗细、颜色等。

如果你想用一个像素数组来表示一个图像,那么这个数组通常保存在 X Window 服务器的一个 pixmap 中。应用可以通过从客户机向服务器发送消息来创建和修改位图。即使是简单的修改,如更改单个像素的值,也需要网络往返,如果经常进行,这显然会变得非常昂贵。

Pixbufs

Pixbufs 是客户端的 pixmaps 的等价物。客户端可以操纵它们,而无需往返于 X Window 服务器。这减少了操作它们的时间和网络开销。然而,这意味着原本保存在服务器上的信息现在必须在客户端应用端构建和维护。

x、韦兰和和平号

X Window 系统已经有将近 30 年的历史了。在此期间,它已经发展到满足硬件和软件需求的变化,同时仍然保持向后兼容性。

在这 30 年中,硬件发生了重大变化:多核系统现在很普遍,GPU 带来了视频处理的变化。通常,内存量(缓存和 RAM)意味着内存不再是一个问题。

与此同时,软件方面也发生了变化。现在普遍使用 Compiz 这样的“合成窗口管理器”,这样你就可以制作出像摇晃的窗口这样的效果。这对于 X 窗口模型来说并不好:来自应用的请求发送到 X 服务器,但是请求的图像必须传递到合成窗口管理器,它将执行它的效果,然后将图像发送回 X 服务器。这是网络流量的巨大增长,X 服务器现在只是扮演显示者的角色,而不是合成器。

应用库现在已经得到了发展,以前由 X 服务器完成的许多工作现在可以由 Cairo、Pixman、Freetype、Fontconfig 和 Pango 等库在应用端完成。

所有这些变化导致了对新的后端服务器的建议,它们在这个不断发展的世界中协同工作。这是由 Wayland ( http://wayland.freedesktop.org/ )的开发引发的,但被 Ubuntu 分叉这个来开发 Mir ( https://wiki.ubuntu.com/Mir/ )有点搞砸了。不要相信这些争论。谷歌一下米尔和韦兰就知道了。

从简单的意义上来说,这意味着在未来,当 pixbufs 出现时,pixmaps 将会退出。

Gtk 3.0

随着 Gtk 3.0 的出现,像素贴图不再存在。数据结构GdkPixbuf中只有 pixbufs。要显示 FFmpeg 解码的视频,您需要在图像被转码为picture_RGB后,将其转换为GdkPixbuf,并创建GtkImage

        pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
                                          0, 8, width, height,
                                          picture_RGB->linesize[0],
                                          pixmap_destroy_notify,
                                          NULL);
        gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf);

Gtk 2.0

Gtk 2.0 在结构GdkPixmap中仍然有 pixmaps。理论上,应该可以使用函数GdkPixmap *gdk_pixmap_create_from_data(GdkDrawable *drawable, const gchar *data, gint width, gint height, gint depth, const GdkColor *fg, const GdkColor *bg)编写类似于 Gtk 3.0 代码的代码,该函数在《GDK 2 参考手册》的“位图和像素图”( https://developer.gnome.org/gdk/unstable/gdk-Bitmaps-and-Pixmaps.html#gdk-pixmap-create-from-data ),然后调用void gtk_image_set_from_pixmap(GtkImage *image, GdkPixmap *pixmap, GdkBitmap *mask),该函数在 GtkImage ( www.gtk.org/api/2.6/gtk/GtkImage.html#gtk-image-set-from-pixmap )的 Gtk 2.6 参考手册中有记载。

唯一的问题是我无法让函数gdk_pixmap_create_from_data工作。无论我为 drawable 尝试什么参数,调用总是在类型或值上出错。例如,记录的值是NULL,但这总是会导致断言错误(“不应为空”)。

那么,什么有效呢?嗯,我能找到的只是 pixmap 和 pixbuf 的一点混乱:创建一个充满视频数据的 pixbuf,创建一个 pixmap,将 pix buf 数据写入 pixmap,然后用 pix map 数据填充图像。

        pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
                                          0, 8, width, height,
                                          picture_RGB->linesize[0],
                                          pixmap_destroy_notify,
                                          NULL);
        pixmap = gdk_pixmap_new(window->window, width, height, -1);
        gdk_draw_pixbuf((GdkDrawable *) pixmap, NULL,
                        pixbuf,
                        0, 0, 0, 0, wifth, height,
                        GDK_RGB_DITHER_NORMAL, 0, 0);

        gtk_image_set_from_pixmap((GtkImage*) image, pixmap, NULL);

螺纹和 Gtk

视频将需要在自己的线程中播放。Gtk 将在其线程中建立一个 GUI 处理循环。既然这是 Linux,你就用 Posix pthreads。视频线程需要通过以下方式明确启动:

        pthread_t tid;
        pthread_create(&tid, NULL, play_background, NULL);

这里函数play_background调用 FFmpeg 代码来解码视频文件。请注意,在应用实现之前,不应该启动线程,否则它会试图在不存在的窗口中绘图。

Gtk 线程将通过调用以下内容来启动:

        gtk_main();

这很简单。但是现在您必须处理调用 GUI 线程的视频线程,以便绘制图像。我在这方面找到的最好的文档是“GTK+线程安全吗?我如何编写多线程 GTK+应用?”( https://developer.gnome.org/gtk-faq/stable/x481.html )。基本上,它声明影响 Gtk 线程的代码应该用一个gdk_threads_enter() … gdk_threads_leave()对括起来。

对于 Gtk 2.0 来说还可以。Gtk 3.0 呢?呜!这些调用现在已被否决。那么,你该怎么办?到目前为止(截至 2013 年 7 月),所有似乎存在的都是开发者对话,例如在 https://mail.gnome.org/archives/gtk-devel-list/2012-August/msg00020.html ,其中写道:

"We never seem to explain when gdk_threads_enter/leave is needed. Therefore, during the checkout process of my jhbuild, many key parts I saw were unnecessary. If your application does not call gdk_threads_init or gdk_threads_set_lock_functions, there is no need to use enter/leave. Of course, the library is another matter. "

实际的解决方法是不同的方向,解决方法如 https://developer.gnome.org/gdk3/stable/gdk3-Threads.html 所示:Gtk 不是线程安全的。Gtk 线程内的调用是安全的,但是大多数来自不同线程的 Gtk 调用并不安全。如果您需要从另一个线程调用 Gtk,那么调用gdk_threads_add_idle()来调用将在 Gtk 线程中运行的函数。与该延迟呼叫相关的数据可能会作为另一个参数传递给gdk_threads_add_idle()

在本章的剩余部分,你将只考虑 Gtk 3 而不考虑 Gtk 2。

《守则》

最后,是时候看看在与 Gtk 3.0 兼容的 Gtk 应用中播放视频的代码了。我会把它打碎。

播放视频的函数作为后台线程运行。它使用 Gtk 3 读取帧并创建一个 pixbuf。内容如下:

  static gboolean draw_image(gpointer user_data) {
      GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;

      gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
      gtk_widget_queue_draw(image);
      g_object_unref(pixbuf);

      return G_SOURCE_REMOVE;
  }
   static gpointer play_background(gpointer args) {

    int i;
    AVPacket packet;
    int frameFinished;
    AVFrame *pFrame = NULL;

    /* initialize packet, set data to NULL, let the demuxer fill it */
    /* http://ffmpeg.org/doxygen/trunk/doc_2examples_2demuxing_8c-example.html#a80 */
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;

    int bytesDecoded;
    GdkPixbuf *pixbuf;
    AVFrame *picture_RGB;
    char *buffer;

    pFrame=avcodec_alloc_frame();

    i=0;
    picture_RGB = avcodec_alloc_frame();
    buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, WIDTH, HEIGHT));
    avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, WIDTH, HEIGHT);

    while(av_read_frame(pFormatCtx, &packet)>=0) {
        if(packet.stream_index==videoStream) {
            usleep(33670);  // 29.7 frames per second
            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
                                  &packet);

            int width = pCodecCtx->width;
            int height = pCodecCtx->height;

            sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt, width, height,
                                     PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);

            if (frameFinished) {

                sws_scale(sws_ctx,  (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, height,
                                picture_RGB->data, picture_RGB->linesize);

                printf("old width %d new width %d\n",  pCodecCtx->width, picture_RGB->width);
                pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
                                                  0, 8, width, height,
                                                  picture_RGB->linesize[0], pixmap_destroy_notify,
                                                  NULL);
                gdk_threads_add_idle(draw_image, pixbuf);

                gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf);

            }
            sws_freeContext(sws_ctx);
        }
        av_free_packet(&packet);
        g_thread_yield();
    }

    printf("Video over!\n");
    exit(0);
}

这个函数被设置为在它自己的线程中运行,并且有一个窗口供它绘制。

/* Called when the windows

are realized
 */
static void realize_cb (GtkWidget *widget, gpointer data) {
    /* start the video playing in its own thread */
    GThread *tid;
    tid = g_thread_new("video",
                       play_background,
                       NULL);
}

main 函数负责初始化 FFmpeg 环境以读取视频,然后设置 Gtk 窗口以供其绘制。内容如下:

int main(int argc, char** argv)
{

    int i;

    /* FFMpeg stuff */

    AVFrame *pFrame = NULL;
    AVPacket packet;

    AVDictionary *optionsDict = NULL;

    av_register_all();

    if(avformat_open_input(&pFormatCtx, "/home/httpd/html/ComputersComputing/simpson.mpg", NULL, NULL)!=0)
        return -1; // Couldn't open file

    // Retrieve stream information
    if(avformat_find_stream_info(pFormatCtx, NULL)<0)
        return -1; // Couldn't find stream information

    // Dump information about file onto standard error
    av_dump_format(pFormatCtx, 0, argv[1], 0);

    // Find the first video stream
    videoStream=-1;
    for(i=0; i<pFormatCtx->nb_streams; i++)
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
            videoStream=i;
            break;
        }
    if(videoStream==-1)
        return -1; // Didn't find a video stream

    for(i=0; i<pFormatCtx->nb_streams; i++)
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) {
            printf("Found an audio stream too\n");
            break;
        }

    // Get a pointer to the codec context for the video stream
    pCodecCtx=pFormatCtx->streams[videoStream]->codec;

    // Find the decoder for the video stream
    pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
    if(pCodec==NULL) {
        fprintf(stderr, "Unsupported codec!\n");
        return -1; // Codec not found
    }

    // Open codec
    if(avcodec_open2(pCodecCtx, pCodec, &optionsDict)<0)
        return -1; // Could not open codec

    width =  pCodecCtx->width;
    height =  pCodecCtx->height;

    sws_ctx =
        sws_getContext
        (
         pCodecCtx->width,
         pCodecCtx->height,
         pCodecCtx->pix_fmt,
         pCodecCtx->width,
         pCodecCtx->height,
         PIX_FMT_YUV420P,
         SWS_BILINEAR,
         NULL,
         NULL,
         NULL
         );

    /* GTK stuff now */

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

    /* When the window is given the "delete-event" signal (this is given
     * by the window manager, usually by the "close" option, or on the
     * titlebar), we ask it to call the delete_event () function
     * as defined above. The data passed to the callback
     * function is NULL and is ignored in the callback function. */
    g_signal_connect (window, "delete-event",
                      G_CALLBACK (delete_event), NULL);

    /* Here we connect the "destroy" event to a signal handler.
     * This event occurs when we call gtk_widget_destroy() on the window,
     * or if we return FALSE in the "delete-event" callback. */
    g_signal_connect (window, "destroy",
                      G_CALLBACK (destroy), NULL);

    g_signal_connect (window, "realize", G_CALLBACK (realize_cb), NULL);

    /* Sets the border width of the window. */
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);

    image = gtk_image_new();
    gtk_widget_show (image);

    /* This packs the button into the window (a gtk container). */
    gtk_container_add (GTK_CONTAINER (window), image);

    /* and the window */
    gtk_widget_show (window);

    /* All GTK applications
must have a gtk_main(). Control ends here
     * and waits for an event to occur (like a key press or
     * mouse event). */
    gtk_main ();

    return 0;
}

在图像上覆盖图像

在电视电影中,通常会看到一个固定的图像叠加在视频之上。字幕可以是动态图像的一个例子,但也可以是文本覆盖。本节只考虑一个图像在另一个图像之上。

在 Gtk 2.0 中,这非常简单:将一个 pixbuf 绘制到一个 pixmap 中,然后将叠加的 pixbuf 绘制到同一个 pixmap 中。

pixmap = gdk_pixmap_new(window->window, 720, 480, -1);

gdk_draw_pixbuf((GdkDrawable *) pixmap, NULL,
                pixbuf,
                0, 0, 0, 0, 720, 480,
                GDK_RGB_DITHER_NORMAL, 0, 0);

// overlay another pixbuf
gdk_draw_pixbuf((GdkDrawable *) pixmap, NULL,
                overlay_pixbuf,
                0, 0, 0, 0, overlay_width, overlay_height,
                GDK_RGB_DITHER_NORMAL, 0, 0);

gtk_image_set_from_pixmap((GtkImage*) image, pixmap, NULL);

gtk_widget_queue_draw(image);

Gtk 3.0 看起来并不那么简单,因为像素地图已经消失了。许多页面建议使用 Cairo 曲面,后面的章节将会介绍这一点。但是“GdkPixbuf 结构”( https://developer.gnome.org/gdk-pixbuf/unstable/gdk-pixbuf-The-GdkPixbuf-Structure.html )建议,只要把数据类型对齐,就可以把第二个图像的像素写入第一个图像的 Pixbuf 数据中。名为“Gdk-pix buf”(http://openbooks.sourceforge.net/books/wga/graphics-gdk-pixbuf.html)的页面(虽然很老)是一个关于 Gdk pixbufs 的有用教程。您必须正确处理的一个细节是每个图像的 rowstride:二维图像存储为字节的线性数组,rowstride 告诉您一行由多少个字节组成。通常每个像素有 3 或 4 个字节(对于 RGB 或 RGB+alpha),并且这些也需要在图像之间匹配。

Gtk 3 叠加功能如下:

static void overlay(GdkPixbuf *pixbuf, GdkPixbuf *overlay_pixbuf,
                         int height_offset, int width_offset) {
    int overlay_width, overlay_height, overlay_rowstride, overlay_n_channels;
    guchar *overlay_pixels, *overlay_p;
    guchar red, green, blue, alpha;
    int m, n;
    int rowstride, n_channels, width, height;
    guchar *pixels, *p;

    if (overlay_pixbuf == NULL) {
        return;
    }

    /* get stuff out of overlay pixbuf */
    overlay_n_channels = gdk_pixbuf_get_n_channels (overlay_pixbuf);
    n_channels =  gdk_pixbuf_get_n_channels(pixbuf);
    printf("Overlay has %d channels, destination has %d channels\n",
           overlay_n_channels, n_channels);
    overlay_width = gdk_pixbuf_get_width (overlay_pixbuf);
    overlay_height = gdk_pixbuf_get_height (overlay_pixbuf);

    overlay_rowstride = gdk_pixbuf_get_rowstride (overlay_pixbuf);
    overlay_pixels = gdk_pixbuf_get_pixels (overlay_pixbuf);

    rowstride = gdk_pixbuf_get_rowstride (pixbuf);
    width = gdk_pixbuf_get_width (pixbuf);
    pixels = gdk_pixbuf_get_pixels (pixbuf);

    printf("Overlay: width %d str8ide %d\n", overlay_width, overlay_rowstride);
    printf("Dest: width  str8ide %d\n", rowstride);

    for (m = 0; m < overlay_width; m++) {
        for (n = 0; n < overlay_height; n++) {
            overlay_p = ove
rlay_pixels + n * overlay_rowstride + m * overlay_n_channels;
            red = overlay_p[0];
            green = overlay_p[1];
            blue = overlay_p[2];
            if (overlay_n_channels == 4)
                alpha = overlay_p[3];
            else
                alpha = 0;

            p = pixels + (n+height_offset) * rowstride + (m+width_offset) * n_channels;
            p[0] = red;
            p[1] = green;
            p[2] = blue;
            if (n_channels == 4)
                p[3] = alpha;
        }
    }
}

阿尔法通道

叠加图像中可能有一些“透明”部分。你不希望这样的部分被覆盖到下面的图像。但是这些部分需要在像素阵列中有一个值。连零都是一个值:黑!一些图像会为每个像素分配另一个字节作为 alpha 通道。这有一个显示像素透明度的值。值 255 表示完全不透明,值 0 表示完全透明。

将透明像素与底层像素合并的最简单方法就是不要这样做:不要动底层像素。维基百科“阿尔法合成”( http://en.wikipedia.org/wiki/Alpha_compositing )页面指出了更复杂的算法。

使用函数gdk_pixbuf_add_alpha可以将没有 alpha 通道的图像转换成有 alpha 通道的图像。这也可以用于通过匹配颜色来设置 alpha 通道的值。例如,下面的代码应该将任何白色像素的 alpha 值设置为 0,将所有其他像素的 alpha 值设置为 255:

pixbuf = gdk_pixbuf_add_alpha(pixbuf, TRUE, 255, 255, 255);

不幸的是,它似乎想留下一个像素的“边缘”,应该标记为透明。

有了 alpha 标记,可以在覆盖功能中使用一个简单的测试来决定是否执行覆盖。

if (alpha < 128) {
    continue;
 }

仅仅为了几行改动就给出一个完整的程序是不值得的。是gtk_play_video_overlay_alpha.c

使用 Cairo 绘制图像

随着 Gtk 3.0 中 pixmaps 的消失,Cairo 现在是将多个组件组装成一个图像的唯一真正的方法。您可以在 http://cairographics.org/documentation/ 找到开罗的一般信息,在 http://zetcode.com/gfx/cairo/ 找到教程,在 http://zetcode.com/gfx/cairo/cairoimg/ 找到叠加到图像上的信息。

开罗需要来源和目的地。源可以改变,通常是从图像源到颜色源,等等。目的地是画出来的东西的最终目的地。

目的地可以在内存中,也可以在各种后端。您需要一个内存中的目的地,以便可以从中提取 pixbuf,所有操作都在客户端完成。您创建一个目的地作为类型为cairo_surface_t的表面,并使用以下内容将其设置到类型为cairo_t的 Cairo 上下文中:

cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                       width, height);
cairo_t *cr = cairo_create(surface);

Cairo 上下文cr随后用于设置源、执行绘制等等。最后,您将从surface中提取一个位图。

第一步是将视频的每一帧的源设置为 pixbuf,并使用以下代码将其绘制到目标:

gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
cairo_paint (cr);

您可以在此基础上叠加另一个图像,方法是将源更改为叠加图像,并绘制:

gdk_cairo_set_source_pixbuf(cr, overlay_pixbuf, 300, 200);
cairo_paint (cr);

请注意,如果覆盖图有“透明”像素,Cairo 将进行任何所需的 alpha 混合。

要绘制文本,您需要将源重置为 RGB 表面,设置文本的所有参数,并将文本绘制到目标中。这是通过以下方式完成的:

// white text
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
// this is a standard font for Cairo
cairo_select_font_face (cr, "cairo:serif",
                        CAIRO_FONT_SLANT_NORMAL,
                        CAIRO_FONT_WEIGHT_BOLD);
cairo_set_font_size (cr, 20);
cairo_move_to(cr, 10.0, 50.0);
cairo_show_text (cr, "hello");

最后,您要从目的地提取最终图像,并将其设置到GdkImage中进行显示。这里还有一个 Gtk 2.0 和 Gtk 3.0 的区别:Gtk 3.0 有一个函数gdk_pixbuf_get_from_surface,会返回一个GdKPixbuf;Gtk 2.0 没有这个功能。你会在这里看到 Gtk 3.0 版本。

pixbuf = gdk_pixbuf_get_from_surface(surface,
                                     0,
                                     0,
                                     width,
                                     height);

gdk_threads_add_idle(draw_image, pixbuf);

使用 Cairo 播放视频的修改函数如下:

static gboolean draw_image(gpointer user_data) {
    GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;

    gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
    gtk_widget_queue_draw(image);
    g_object_unref(pixbuf);

    return G_SOURCE_REMOVE;
}

static void *play_background(void *args) {

    int i;
    AVPacket packet;
    int frameFinished;
    AVFrame *pFrame = NULL;

    int bytesDecoded;
    GdkPixbuf *pixbuf;
    GdkPixbuf *overlay_pixbuf;
    AVFrame *picture_RGB;
    char *buffer;

    GError *error = NULL;
    overlay_pixbuf = gdk_pixbuf_new_from_file(OVERLAY_IMAGE, &error);
    if (!overlay_pixbuf) {
        fprintf(stderr, "%s\n", error->message);
        g_error_free(error);
        exit(1);
    }

    // add an alpha layer for a white background
    overlay_pixbuf = gdk_pixbuf_add_alpha(overlay_pixbuf, TRUE, 255, 255, 255);

    int overlay_width = gdk_pixbuf_get_width(overlay_pixbuf);
    int overlay_height =  gdk_pixbuf_get_height(overlay_pixbuf);

    pFrame=avcodec_alloc_frame();

    i=0;
    picture_RGB = avcodec_alloc_frame();
    buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, 720, 576));
    avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, 720, 576);

    while(av_read_frame(pFormatCtx, &packet)>=0) {
        if(packet.stream_index==videoStream) {
            usleep(33670);  // 29.7 frames per second
            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
                                  &packet);

            int width = pCodecCtx->width;
            int height = pCodecCtx->height;

            sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt, width, height,
                                     PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);

            if (frameFinished) {
                printf("Frame %d\n", i++);

                sws_scale(sws_ctx,  (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, height, picture_RGB->data, picture_RGB->linesize);

                printf("old width %d new width %d\n",  pCodecCtx->width, picture_RGB->width);
                pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
                                                  0, 8, width, height,
                                                  picture_RGB->linesize[0], pixmap_destroy_notify,
                                                  NULL);

                // Create the destination surface
                cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                                       width, height);
                cairo_t *cr = cairo_create(surface);

                // draw the background image
                gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
                cairo_paint (cr);

                // overlay an image on top
                // alpha blending will be done by Cairo
                gdk_cairo_set_source_pixbuf(cr, overlay_pixbuf, 300, 200);
                cairo_paint (cr);

                // draw some white text on top
                cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
                // this is a standard font for Cairo
                cairo_select_font_face (cr, "cairo:serif",
                                        CAIRO_FONT_SLANT_NORMAL,
                                        CAIRO_FONT_WEIGHT_BOLD);
                cairo_set_font_size (cr, 20);
                cairo_move_to(cr, 10.0, 50.0);
                cairo_show_text (cr, "hello");

                pixbuf = gdk_pixbuf_get_from_surface(surface,
                                                     0,
                                                     0,
                                                     width,
                                                     height);

                gdk_threads_add_idle(draw_image, pixbuf);

                sws_freeContext(sws_ctx);
                cairo_surface_destroy(surface);
                cairo_destroy(cr);

            }
        }
        av_free_packet(&packet);
    }

    printf("Video over!\n");
    exit(0);
}

使用 Pango 绘制文本

虽然 Cairo 可以绘制任何形式的文本,但像cairo_show_text这样的函数没有太大的灵活性。比如说,画多种颜色会涉及很多工作。Pango 是一个处理文本所有方面的库。在 https://developer.gnome.org/pango/stable/ 有一本盘古参考手册。好的教程在 www.ibm.com/developerworks/library/l-u-pango2/

给文本着色(和一些其他效果)的最简单方法是创建用 HTML 标记的文本,如下所示:

gchar *markup_text = "<span foreground=\"red\">hello </span><span foreground=\"black\">world</span>";

红色的是“你好”,黑色的是“世界”。这然后被解析成文本“红黑”和一组属性标记。

gchar *markup_text = "<span foreground=\"red\">hello </span><span foreground=\"black\">world</span>";
PangoAttrList *attrs;
gchar *text;

pango_parse_markup (markup_text, -1,0, &attrs, &text, NULL, NULL);

这可以通过从 Cairo 上下文创建一个PangoLayout来呈现到 Cairo 上下文中,在 Pango 布局中布置文本及其属性,然后在 Cairo 上下文中显示这个布局。

PangoLayout *layout;
PangoFontDescription *desc;

cairo_move_to(cr, 300.0, 50.0);
layout = pango_cairo_create_layout (cr);
pango_layout_set_text (layout, text, -1);
pango_layout_set_attributes(layout, attrs);
pango_cairo_update_layout (cr, layout);
pango_cairo_show_layout (cr, layout);

(是的,在所有这些中,有很多在库之间跳来跳去!)

和前面一样,一旦所有内容都被绘制到 Cairo 上下文中,就可以从 Cairo 表面目的地提取出一个 pixbuf,设置到GtkImage中,并添加到 Gtk 事件队列中。

使用 Pango 绘制视频的修改函数如下:

static gboolean draw_image(gpointer user_data) {
    GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;

    gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
    gtk_widget_queue_draw(image);
    g_object_unref(pixbuf);

    return G_SOURCE_REMOVE;
}

static void *play_background(void *args) {

    int i;
    AVPacket packet;
    int frameFinished;
    AVFrame *pFrame = N
ULL;

    /* initialize packet, set data to NULL, let the demuxer fill it */
    /* http://ffmpeg.org/doxygen/trunk/doc_2examples_2demuxing_8c-example.html#a80 */
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;

    int bytesDecoded;
    GdkPixbuf *pixbuf;
    GdkPixbuf *overlay_pixbuf;
    AVFrame *picture_RGB;
    char *buffer;

    // Pango marked up text, half red, half black
    gchar *markup_text = "<span foreground=\"red\">hello</span><span foreground=\"black\">world</span>";
    PangoAttrList *attrs;
    gchar *text;

    pango_parse_markup (markup_text, -1,0, &attrs, &text, NULL, NULL);

    GError *error = NULL;
    overlay_pixbuf = gdk_pixbuf_new_from_file(OVERLAY_IMAGE, &error);
    if (!overlay_pixbuf) {
        fprintf(stderr, "%s\n", error->message);
        g_error_free(error);
        exit(1);
    }

    // add an alpha lay
er for a white background
    overlay_pixbuf = gdk_pixbuf_add_alpha(overlay_pixbuf, TRUE, 255, 255, 255);

    int overlay_width = gdk_pixbuf_get_width(overlay_pixbuf);
    int overlay_height =  gdk_pixbuf_get_height(overlay_pixbuf);

    pFrame=avcodec_alloc_frame();

    i=0;
    picture_RGB = avcodec_alloc_frame();
    buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, 720, 576));
    avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, 720, 576);

    while(av_read_frame(pFormatCtx, &packet)>=0) {
        if(packet.stream_index==videoStream) {
            usleep(33670);  // 29.7 frames per second
            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
                                  &packet);

            int width = pCodecCtx->width;
            int height = pCodecCtx->height;

            sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt, width, height,
                                     PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);

            if (frameFinished) {
                printf("Frame %d\n", i++);

                sws_scale(sws_ctx,  (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, height,
                                picture_RGB->data, picture_RGB->linesize);

                printf("old width %d new width %d\n",  pCodecCtx->width, picture_RGB->width);
                pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
                                                  0, 8, width, height,
                                                  picture_RGB->linesize[0], pixmap_destroy_notify,

                                                  NULL);

                // Create the destination surface
                cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                                       width, height);
                cairo_t *cr = cairo_create(surface);

                // draw the background image
                gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
                cairo_paint (cr);

                // overlay an image on top
                // alpha blending will be done by Cairo
                gdk_cairo_set_source_pixbuf(cr, overlay_pixbuf, 300, 200);
                cairo_paint (cr);

                // draw some white text on top
                cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
                // this is a standard font for Cairo
                cairo_select_font_face (cr, "cairo:serif",
                                        CAIRO_FONT_SLANT_NORMAL,
                                        CAIRO_FONT_WEIGHT_BOLD);
                cairo_set_font_size (cr, 20);
                cairo_move_to(cr, 10.0, 50.0);
                cairo_show_text (cr, "hello");

                // draw Pango text
                PangoLayout *layout;
                PangoFontDescription *desc;

                cairo_move_to(cr, 300.0, 50.0);
                layout = pango_cairo_create_layout (cr);
                pango_layout_set_text (layout, text, -1);
                pango_layout_set_attributes(layout, attrs);
                pango_cairo_update_layout (cr, layout);
                pango_cairo_show_layout (cr, layout);

                pixbuf = gdk_pixbuf_get_from_surface(surface,
                                                     0,
                                                     0,
                                                     width,
                                                     height);

                gdk_threads_add
_idle(draw_image, pixbuf);

                sws_freeContext(sws_ctx);
                g_object_unref(layout);
                cairo_surface_destroy(surface);
                cairo_destroy(cr);

            }
        }
        av_free_packet(&packet);
    }

    printf("Vi
deo over!\n");
    exit(0);
}

结论

掌握 Gtk 工具包的某些方面并不容易。你将在后面的章节中用到这些材料,这也是为什么它被从本书的声音章节中抽出,放在它自己的章节中。那些对 Linux sound 不感兴趣的人可能会发现它很有用。

十六、MIDI

MIDI 是电子版的乐谱。它基本上是一组指令,告诉 MIDI 演奏者演奏哪些音符,声音有多大,使用哪些乐器,使用什么效果,以及何时停止演奏音符。

MIDI 有两种形式:一种是“wire”格式,在这种格式中,MIDI 命令通过流发送,并在接收时进行处理;另一种是文件格式,在这种格式中,MIDI 命令存储在文件中,并从文件中读取和播放。

发明 MIDI 是为了让乐器能够交流,让一种乐器能够控制另一种乐器。它在电子音乐中被大量使用,但通常适用于任何电子乐器。当然,如果有合适的软件,计算机可以被认为是 MIDI 乐器。

资源

以下是一些资源:

MIDI 系统的组件

一个 MIDI 系统可以是一个单独的乐器,既产生又消耗 MIDI 事件。通常所说的合成器通常有一个键盘和多个控制来生成 MIDI 事件,也有硬件来从这些控制中产生声音。

合成器

抽象地说,合成器是 MIDI 事件的消费者,也是声音的生产者,通过扬声器或耳机。合成器可以在硬件中实现这一点,但也可以使用被称为声音字体的表格在软件中实现。声音字体有很多种,我们将在下一章讨论。

顺序

合成器对 MIDI 事件做出实时反应。其中最重要的是演奏音符的事件。现在,活页乐谱使用不同种类的音符(钩针、八分音符等等)来表示持续时间。MIDI 改为使用NOTE ONNOTE OFF事件。

这些音符事件不能一次全部发送到合成器,否则它会尝试一次播放它们。比方说,如果 MIDI 事件是由一个人在键盘上产生的,那么他们就控制它们如何被发送。

在这本书里,你将会看到 MIDI 事件存储在一个文件里的情况。因此,文件阅读器必须控制事件发送到合成器的时间。定序器的作用是在适当的时间向合成器发送事件。

其他组件

一个最小的系统将由一个音序器(一个人或一个组件)在正确的时间发送 MIDI 到合成器。不过,可能还有其他组件,包括架子鼓、产生混响或延迟等声音效果的设备,或者预先录制或数字化音频并可以回放的采样器。

MIDI 事件

MIDI 事件有几种类别。对我们来说,主要的事件是程序变更事件、注释事件和元事件。

程序变更事件

乐器或“声音”与通道相关联。声音和频道之间的建立通常在播放开始时完成,但是可以通过节目改变事件来改变。

记录事件

注意事件不是NOTE ON就是NOTE OFF。它们包括一个选择演奏乐器的频道。他们有一个代表音符的数字。从 0 到 127 共有 128 个,它们对应于音符 C0(8.175 赫兹)到 G10(12543.854 赫兹)。这些值是可以改变的,例如,对于微型音乐,但这超出了本书的范围。音符事件也包含力度,它给出音符的音量。

元事件

有一组元事件给出了关于 MIDI 系统演奏的信息。这些包括版权声明和序列或曲目名称,但对 Karaoke 最重要的是歌词和文本事件。

这些元事件不通过网络发送。比方说,合成器不知道如何处理版权声明。元事件包含在 MIDI 文件中,可以被从文件中读取的任何东西解释。

这导致了序列器的行为差异,当您查看 Karaoke 系统时会遇到这种差异:一些序列器提供元信息,如 Java 序列器。其他的没有,比如fluidsynth

结论

MIDI 系统早在 20 世纪 80 年代就已经出现了。从这个意义上说,它们是“旧的”技术,并且经常提出替换建议。然而,MIDI 仍然是一种持久的电子格式。本章介绍了 MIDI 系统的组成部分,并给出了 MIDI 信息的抽象视图。

十七、MIDI 的用户级工具

本章概述了用于播放 MIDI 文件的主要工具。它不包括 MIDI 编辑器、MIDI 制作人员等。

资源

查看以下资源:

声音字体

本章中描述的每个工具都包括一个软件合成器,它将输入的 MIDI 数据生成音频作为 PCM 数据。MIDI 数据包含关于演奏每个音符的乐器的信息,当然,每个乐器听起来都不一样。因此,合成器必须利用从 MIDI notes +乐器到 PCM 数据的映射信息。

映射通常使用声音字体文件来完成。这有各种格式。初级的是.sf2格式( http://connect.creativelabs.com/developer/SoundFont/Forms/AllItems.aspx/ )。有些合成器(如 TiMidity)也可以使用 Gravis 超声贴片,这是录制的真实乐器。

已经创建了许多声音字体文件。例如,请参见“SoundFonts 和其他类似文件的链接”( www.synthfont.com/links_to_soundfonts.html )(尽管许多链接已断开)。

可能令人惊讶的是,使用不同的声音字体似乎对 CPU 的使用没有太大的影响。对于 FluidSynth 来说,它们在一首歌曲上使用大约 60%到 70%的 CPU。当然,它们听起来确实不同。

TiMidity

TiMidity 是一个“软件声音渲染器(MIDI 音序器和 MOD 播放器)”。它的主页是 Maemo.org(http://maemo.org/packages/view/timidity/)。

TiMidity 可以用来播放 MIDI 文件,方法是在命令行上给出它们,就像这样:

          timidity rehab.mid

TiMidity 使用的默认声音字体是 Gravis 超声波补丁,来自/usr/share/midi/freepats/目录。这些声音字体是许多乐器所缺少的,因此应该被另一种字体所取代,例如 FluidSynth 字体。在配置文件/etc/timidity/timidity.cfg中进行设置。

作为服务器的 TiMidity

TiMidity 也可以作为监听端口的 ALSA 服务器运行(参见 http://wiki.winehq.org/MIDI “在 UNIX 上使用 MIDI”)。

          timidity -iAD -B2,8 -Os1l -s 44100

-iAD选项将它作为后台守护进程作为 ALSA 序列器客户端运行。-B2,8选项选择缓冲区碎片的数量。-Os1l选项选择 ALSA 输出作为 PCM。-s选项是样本大小。(对于树莓派,我发现-B0,12-B2,8好用。)

在这种模式下,ALSA 可以向它发送信息。命令

          aconnect -0

将显示如下输出:

          client 14: 'Midi Through' [type=kernel]
          0 'Midi Through Port-0'
          laptop:/home/httpd/html/LinuxSound/MIDI/Python/pyPortMidi-0.0.3$aconnect -o
          client 14: 'Midi Through' [type=kernel]
          0 'Midi Through Port-0'
          client 128: 'TiMidity' [type=user]
          0 'TiMidity port 0 '
          1 'TiMidity port 1 '
          2 'TiMidity port 2 '
          3 'TiMidity port 3 '

Midi 直通端口没有用,但是 TiMidity 端口有用。然后,MIDI 文件可以由 ALSA 音序器播放,如下所示:

          aplaymidi -p128:0 rehab.mid

设置 TiMidity 输出设备

您可以使用-O选项更改 TiMidity 的默认输出。TiMidity 帮助(timidity -h)显示如下内容:

          Available output modes (-O, --output-mode option):
          -Os          ALSA pcm device
          -Ow          RIFF WAVE file
          -Or          Raw waveform data
          -Ou          Sun audio file
          -Oa          AIFF file
          -Ol          List MIDI event
          -Om          Write MIDI file
          -OM          MOD -> MIDI file conversion

对于其中一些模式,也可以使用-o选项设置设备名称。例如,要使用hw:2 ALSA 设备播放文件,请使用:

          timidity -Os -o hw:2 ...

TiMidity 和 Jack

TiMidity 可通过使用-Oj选项的插孔输出运行。在 Ubuntu 等基于用户的环境中,可能需要停止或暂停 PulseAudio,启动 Jack 服务器,然后运行 Timothy。例如,在一个终端中,可以通过以下方式暂停 PulseAudio:

pasuspender cat

在另一个例子中,使用 ALSA 输入和输出启动 Jack 守护程序。

jackd -dalsa

在第三个终端,运行 TiMidity。

timidity -Oj 54154.mid

也可以通过运行qjackctl以图形方式显示链接。

GStreamer

GStreamer 允许您构建可以使用gst-launch播放的“管道”。它可以用这个播放 MIDI 文件,例如:

          gst-launch filesrc location="rehab.mid" ! decodebin ! alsasink

流体合成

fluidsynth是一个命令行 MIDI 播放器。它通过命令行在 ALSA 下运行,如下所示:

          fluidsynth -a alsa -l <sound font> <files...>

声音字体是在命令行上显式设置的,因此可以设置为另一种声音字体。

qsynthfluidsynth的 GUI 界面。

您可以使用fluidsynth将 MIDI 文件转换成 WAV 文件:

          fluidsynth -F out.wav /usr/share/sounds/sf2/FluidR3_GM.sf2 myfile.mid

作为服务器的 fluidsynth

fluidsynth可以像 TiMidity 一样作为服务器运行。用这个:

fluidsynth --server --audio-driver=alsa /usr/share/sounds/sf2/FluidR3_GM.sf2

然后a connect -o将显示端口,可以播放以下内容:

amidi -p 128:0 <midi-file>

玫瑰花园

Rosegarden 是一个全面的音频和 MIDI 音序器、乐谱编辑器和通用音乐创作和编辑环境。它的主页在 www.rosegardenmusic.com/ 。它不是一个独立的合成器;它用fluidsynth举例。

维尔德米迪

这个序列发生器/合成器的目标是体积小。它在这方面取得了成功。

比较

在不同的系统上播放同一首歌曲时,我观察到以下 CPU 模式:

TiMidity+脉冲音频(带有 GUS 或 SF2 声音字体)

  • 12%到 20%的 CPU

fluidsynth +脉冲

  • 65%到 72%的 CPU

维尔德米迪

  • 6%的 CPU

Java 声音

  • 52%到 60%

GStreamer

  • 15%到 20%的 CPU

可见光通讯

VLC 是一个通用的媒体播放器。有一个 VLC 模块( https://wiki.videolan.org/Midi )使用fluidsynth处理 MIDI 文件。为了在 Debian 系统上运行,你首先需要安装vlc-plugin-fluidsynth包。然后在 VLC 的高级选项中,选择编解码器-音频编解码器-FluidSynth。例如,您需要将声音字体设置为/usr/share/sounds/sf2/FluidR3_GM.sf2

结论

本章介绍了各种用于操纵 MIDI 的用户级工具。它主要包括播放器,但也有大量的 MIDI 编辑器,生产者,等等。

十八、MIDI Java 声音

Java Sound 有一个开发良好的 MIDI 系统,有干净分离的组件,如音序器和合成器,它允许元事件和普通 MIDI 事件挂钩。本章考虑使用 MIDI API 编程。

资源

许多资源可用于 Java Sound。

主要的 Java 声音 MIDI 类

Java Sound 依靠一组类来支持 MIDI。这些是 Java 中的标准。以下是主要类别:

设备信息

通过查询MidiSystemDeviceInfo对象列表,可以找到设备信息。每个信息对象包含诸如NameVendor的字段。您可以通过MidiSystem.getMidiDevice(info)使用该信息对象找到实际设备。然后,可以向该设备查询其接收器和发送器以及其类型,如序列发生器或合成器。

一个恼人的部分是,你无法获得所有设备的发射机和接收机的列表,只有那些开放的。您可以请求默认的发送器和接收器,这将隐式地打开它们。因此,您可以看到,在请求缺省值之前,列表可能是空的,但是如果有缺省值,那么之后它就不是空的了!如果没有默认值,将抛出一个M idiUnavailableException异常。

该计划如下:

import javax.sound.midi.*;
import java.util.*;

public class DeviceInfo {

    public static void main(String[] args) throws Exception {
        MidiDevice.Info[] devices;

        /*
        MidiDevice.Info[] info = p.getDeviceInfo();
        for (int m = 0; m < info.length; m++) {
            System.out.println(info[m].toString());
        }
        */

        System.out.println("MIDI devices:");
        devices = MidiSystem.getMidiDeviceInfo();
        for (MidiDevice.Info info: devices) {
            System.out.println("    Name: " + info.toString() +
                               ", Decription: " +
                               info.getDescription() +
                               ", Vendor: " +
                               info.getVendor());
            MidiDevice device = MidiSystem.getMidiDevice(info);
            if (! device.isOpen()) {
                device.open();
            }
            if (device instanceof Sequencer) {
                System.out.println("        Device is a sequencer");
            }
            if (device instanceof Synthesizer) {
                System.out.println("        Device is a synthesizer");
            }
            System.out.println("        Open receivers:");
            List<Receiver> receivers = device.getReceivers();
            for (Receiver r: receivers) {
                System.out.println("            " + r.toString());
            }
            try {
                System.out.println("\n        Default receiver: " +
                                   device.getReceiver().toString());

                System.out.println("\n        Open receivers now:");
                receivers = device.getReceivers();
                for (Receiver r: receivers) {
                    System.out.println("            " + r.toString());
                }
            } catch(MidiUnavailableException e) {
                System.out.println("        No default receiver");
            }

            System.out.println("\n        Open transmitters:");
            List<Transmitter> transmitters = device.getTransmitters();
            for (Transmitter t: transmitters) {
                System.out.println("            " + t.toString());
            }
            try {
                System.out.println("\n        Default transmitter: " +
                                   device.getTransmitter().toString());

                System.out.println("\n        Open transmitters now:");
                transmitters = device.getTransmitters();
                for (Transmitter t: transmitters) {
                    System.out.println("            " + t.toString());
                }
            } catch(MidiUnavailableException e) {
                System.out.println("        No default transmitter");
            }
            device.close();
        }

        Sequencer sequencer = MidiSystem.getSequencer();
        System.out.println("Default system sequencer is " +
                           sequencer.getDeviceInfo().toString() +
                           " (" + sequencer.getClass() + ")");

        Synthesizer synthesizer = MidiSystem.getSynthesizer();
        System.out.println("Default system synthesizer is " +
                           synthesizer.getDeviceInfo().toString() +
                           " (" + synthesizer.getClass() + ")");

    }
}

我的系统上的输出如下:

MIDI devices:
    Name: Gervill, Decription: Software MIDI Synthesizer, Vendor: OpenJDK
        Device is a synthesizer
        Open receivers:

        Default receiver: com.sun.media.sound.SoftReceiver@72f2a824

        Open receivers now:
            com.sun.media.sound.SoftReceiver@72f2a824

        Open transmitters:
        No default transmitter
    Name: Real Time Sequencer, Decription: Software sequencer, Vendor: Oracle Corporation
        Device is a sequencer
        Open receivers:

        Default receiver: com.sun.media.sound.RealTimeSequencer$SequencerReceiver@c23c5ff

        Open receivers now:
            com.sun.media.sound.RealTimeSequencer$SequencerReceiver@c23c5ff

        Open transmitters:
        Default transmitter: com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@4e13aa4e

        Open transmitters now:
            com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@4e13aa4e
Default system sequencer is Real Time Sequencer
Default system synthesizer is Gervill

转储 MIDI 文件

这两个程序从jsresources.org转储一个 MIDI 文件到控制台。MidiSystem从文件中创建一个Sequence。序列的每个轨道都循环播放,每个轨道中的每个事件都被检查。虽然可以就地打印,但是每个事件都被传递给一个Receiver对象,在本例中是DumpReceiver。该对象可以做任何事情,但在本例中只是将事件打印到stdout

The DumpSequence.java程序读取一个作为命令行参数给出的 MIDI 文件,并以可读形式将其内容列表转储到标准输出。它首先获取一个Sequence并打印出关于序列的信息,然后依次获取每个轨道,打印出轨道的内容。

 /*
 *      DumpSequence.java
 *
 *      This file is part of jsresources.org
 */

/*
 * Copyright (c) 1999, 2000 by Matthias Pfisterer
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.io.File;
import java.io.IOException;

import javax.sound.midi.MidiSystem;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Receiver;

public class DumpSequence
{
    private static String[]    sm_astrKeyNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};

    private static Receiver    sm_receiver = new DumpReceiver(System.out, true);

    public static void main(String[] args) {
        /*
         *      We check that there is exactely one command-line
         *      argument. If not, we display the usage message and
         *      exit.
         */
        if (args.length != 1) {
            out("DumpSequence: usage:");
            out("\tjava DumpSequence <midifile>");
            System.exit(1);
        }
        /*
         *      Now, that we're shure there is an argument, we take it as
         *      the filename of the soundfile we want to play.
         */
        String  strFilename = args[0];
        File    midiFile = new File(strFilename);

        /*
         *      We try to get a Sequence object, which the content
         *      of the MIDI file.
         */
        Sequence      sequence = null;
        try {
            sequence = MidiSystem.getSequence(midiFile);
        } catch (InvalidMidiDataException e) {
            e.printStackTrace();
            System.exit(1);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        /*
         *        And now, we output the data.
         */
        if (sequence == null) {
            out("Cannot retrieve Sequence.");
        } else {
            out("------------------------------------------------------------------------");
            out("File: " + strFilename);
            out("------------------------------------------------------------------------");
            out("Length: " + sequence.getTickLength() + " ticks");
            out("Duration: " + sequence.getMicrosecondLength() + " microseconds");
            out("------------------------------------------------------------------------");
            float      fDivisionType = sequence.getDivisionType();
            String     strDivisionType = null;
            if (fDivisionType == Sequence.PPQ) {
                strDivisionType = "PPQ";
            } else if (fDivisionType == Sequence.SMPTE_24) {
                strDivisionType = "SMPTE, 24 frames per second";
            } else if (fDivisionType == Sequence.SMPTE_25) {
                strDivisionType = "SMPTE, 25 frames per second";
            } else if (fDivisionType == Sequence.SMPTE_30DROP) {
                strDivisionType = "SMPTE, 29.97 frames per second";
            } else if (fDivisionType == Sequence.SMPTE_30) {
                strDivisionType = "SMPTE, 30 frames per second";
            }

            out("DivisionType: " + strDivisionType);

            String      strResolutionType = null;
            if (sequence.getDivisionType() == Sequence.PPQ) {
                strResolutionType = " ticks per beat";
            } else {
                strResolutionType = " ticks per frame";
            }
            out("Resolution: " + sequence.getResolution() + strResolutionType);
            out("------------------------------------------------------------------------");
            Track[]    tracks = sequence.getTracks();
            for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
                out("Track " + nTrack + ":");
                out("-----------------------");
                Track        track = tracks[nTrack];
                for (int nEvent = 0; nEvent < track.size(); nEvent++) {
                    MidiEvent  event = track.get(nEvent);
                    output(event);
                }
                out("--------------------------------------------------------------------");
            }
        }
    }

    public static void output(MidiEvent event) {
        MidiMessage     message = event.getMessage();
        long            lTicks = event.getTick();
        sm_receiver.send(message, lTicks);
    }

    private static void out(String strMessage) {
        System.out.println(strMessage);
    }
}
/*** DumpSequence.java ***/

有几个网站有合法的免费 MIDI 文件。文件 http://files.mididb.com/amy-winehouse/rehab.mid 给出了结果。

---------------------------------------------------------------------------
File: rehab.mid
---------------------------------------------------------------------------
Length: 251475 ticks
Duration: 216788738 microseconds
---------------------------------------------------------------------------
DivisionType: PPQ
Resolution: 480 ticks per beat
---------------------------------------------------------------------------
Track 0:
-----------------------
tick 0: Time Signature: 4/4, MIDI clocks per metronome tick: 24, 1/32 per 24 MIDI clocks: 8
tick 0: Key Signature: C major
tick 0: SMTPE Offset: 32:0:0.0.0
tick 0: Set Tempo: 145.0 bpm
tick 0: End of Track
---------------------------------------------------------------------------
Track 1:
-----------------------
tick 0: Sequence/Track Name: amy winehouse - rehab
tick 0: Instrument Name: GM Device
tick 40: Sysex message: F0 7E 7F 09 01 F7
tick 40: End of Track
---------------------------------------------------------------------------
Track 2:
-----------------------
tick 0: MIDI Channel Prefix: 1
tick 0: Sequence/Track Name: amy winehouse - rehab
tick 0: Instrument Name: GM Device  2
tick 480: [B1 79 00] channel 2: control change 121 value: 0
tick 485: [B1 0A 40] channel 2: control change 10 value: 64
tick 490: [B1 5D 14] channel 2: control change 93 value: 20
tick 495: [B1 5B 00] channel 2: control change 91 value: 0
tick 500: [B1 0B 7F] channel 2: control change 11 value: 127
tick 505: [B1 07 69] channel 2: control change 7 value: 105
tick 510: [E1 00 40] channel 2: pitch wheel change 8192
tick 515: [B1 00 00] channel 2: control change 0 value: 0
tick 520: [C1 22] channel 2: program change 34
...

播放 MIDI 文件

要播放 MIDI 文件,您可以使用MidiSystemFile创建一个Sequence。您还可以从MidiSystem中创建一个Sequencer,并将序列传递给它。音序器将通过其Transmitter输出 MIDI 信息。这就完成了系统的 MIDI 事件发生端的设置。

游戏面是通过从MidiSystem中获取一个Synthesizer来构建的。从合成器中找到Receiver,并将其提供给 MIDI 事件的发送器。通过调用音序器上的start()开始播放,音序器从文件中读取并将 MIDI 事件传递给它的发送器。这些被传递到合成器的接收器并播放。图 18-1 显示了相关类的 UML 类图。

A435426_1_En_18_Fig1_HTML.gif

图 18-1。

Class diagram for the SimpleMidiPlayer

这段代码来自播放一个音频文件(简单)。原文被大量评论,但我已经删除了它的大部分印刷书籍。逻辑是从文件中加载一个序列,获得默认的序列器,并将该序列设置到序列器中。音序器不一定是合成器,但默认音序器通常是合成器。如果没有,你得到默认的合成器,然后将音序器的发射器连接到合成器的接收器。然后通过调用音序器上的start()来播放 MIDI 文件。

/*
 *        SimpleMidiPlayer.java
 *
 *        This file is part of jsresources.org
 */

/*
 * Copyright (c) 1999 - 2001 by Matthias Pfisterer
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.io.File;
import java.io.IOException;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.Receiver;
import javax.sound.midi.Transmitter;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.ShortMessage;

public class SimpleMidiPlayer {

    private static Sequencer sm_sequencer = null;
    private static Synthesizer sm_synthesizer = null;

    public static void main(String[]args) {

        if (args.length == 0 || args[0].equals("-h")) {
            printUsageAndExit();
        }

        String strFilename = args[0];
        File midiFile = new File(strFilename);

        /*
         *      We read in the MIDI file to a Sequence object.
         */
        Sequence sequence = null;
        try {
            sequence = MidiSystem.getSequence(midiFile);
        }
        catch(InvalidMidiDataException e) {
            e.printStackTrace();
            System.exit(1);
        }
        catch(IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        /*
         *      Now, we need a Sequencer to play the sequence.
         *      Here, we simply request the default sequencer.
         *      With an argument of false, it does not create
         *      a default syntesizer.
         */
        try {
            sm_sequencer = MidiSystem.getSequencer(false);
        }
        catch(MidiUnavailableException e) {
            e.printStackTrace();
            System.exit(1);
        }
        if (sm_sequencer == null) {
            out("SimpleMidiPlayer.main(): can't get a Sequencer");
            System.exit(1);
        }

        try {
            sm_sequencer.open();
        }
        catch(MidiUnavailableException e) {
            e.printStackTrace();
            System.exit(1);
        }

        /*
         *      Next step is to tell the Sequencer which
         *      Sequence it has to play.
         */
        try {
            sm_sequencer.setSequence(sequence);
        }
        catch(InvalidMidiDataException e) {
            e.printStackTrace();
            System.exit(1);
        }

        Receiver synthReceiver = null;
        if (!(sm_sequencer instanceof Synthesizer)) {
            /*
             *      We try to get the default synthesizer, open()
             *      it and chain it to the sequencer with a
             *      Transmitter-Receiver pair.
             */
            try {
                sm_synthesizer = MidiSystem.getSynthesizer();
                sm_synthesizer.open();
                synthReceiver = sm_synthesizer.getReceiver();
                Transmitter seqTransmitter = sm_sequencer.getTransmitter();
                seqTransmitter.setReceiver(synthReceiver);
            }
            catch(MidiUnavailableException e) {
                e.printStackTrace();
            }
        }

        /*
         *      Now, we can start playing
         */
        sm_sequencer.start();

        try {
            Thread.sleep(5000);
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void printUsageAndExit() {
        out("SimpleMidiPlayer: usage:");
        out("\tjava SimpleMidiPlayer <midifile>");
        System.exit(1);
    }

    private static void out(String strMessage) {
        System.out.println(strMessage);
    }
}

Playing a file to an external MIDI synthesizer

我有一台 Edirol Studio Canvas SD-20 合成器,是花了几百澳币买的。它通过 USB 接口插入电脑。ALSA 通过以下几点认识到这一点:

 $ amidi -l
Dir Device    Name
IO  hw:2,0,0  SD-20 Part A
IO  hw:2,0,1  SD-20 Part B
I   hw:2,0,2  SD-20 MIDI

MidiDevice.Info设备信息列出hw:2,0,0两次,一次用于输入,一次用于输出,其他值类似。设备信息可以通过toString方法识别,该方法返回值如"SD20 [hw:2,0,0]"。从设备信息中,可以发现该设备与使用MidiSystem.getMidiDevice(info)前一样。输入和输出设备可以通过它支持的maxOutputReceivers的数量来区分:零表示无,而任何其他值(包括–1!)表示它有一个 MIDI 接收器。选择外部接收器是通过代码来完成的,用以下代码替换合成器的先前设置:

                Receiver        synthReceiver = null;
                MidiDevice.Info[] devices;
                devices = MidiSystem.getMidiDeviceInfo();

                for (MidiDevice.Info info: devices) {
                    System.out.println("    Name: " + info.toString() +
                                       ", Decription: " +
                                       info.getDescription() +
                                       ", Vendor: " +
                                       info.getVendor());
                    if (info.toString().equals("SD20 [hw:2,0,0]")) {
                        MidiDevice device = MidiSystem.getMidiDevice(info);
                        if (device.getMaxReceivers() != 0) {
                            try {
                                device.open();
                                System.out.println("  max receivers: " + device.getMaxReceivers());
                                receiver = device.getReceiver();
                                System.out.println("Found a receiver");
                                break;
                            } catch(Exception e) {}
                        }
                    }
                }

               if (receiver == null) {
            System.out.println("Receiver is null");
            System.exit(1);
              }
              try {
            Transmitter seqTransmitter = sm_sequencer.getTransmitter();
            seqTransmitter.setReceiver(receiver);
              }
        catch(MidiUnavailableException e) {
            e.printStackTrace();
             }

             /*
              *      Now, we can start playing as before
              */

改变音色库

音色库是一组以某种方式编码的“声音”,用于生成播放的音乐。Java 的默认声音合成器是 Gervill 合成器,它在$HOME/.gervill/soundbank-emg.sf2中寻找默认的声音库。这个默认的声音库很小;只有 1.9MB 大小。而且听起来,嗯,质量很差。

DaWicked1 在“Better Java-midi instrument sounds for Linux”(www.minecraftforum.net/forums/mapping-and-modding/mapping-and-modding-tutorials/1571330-better-java-midi-instrument-sounds-for-linux)中提供了两种方法来改善这一点:更简单的方法是用更好的字体(如 FluidSynth 字体)替换声音字体,使用默认名称。

第二种方法是编程式的,可能更好,因为它允许在运行时有更多的灵活性和选择。

改变音调和速度

更改 MIDI 文件的回放速度意味着更改从音序器发送 MIDI 信息的速率。Java 序列器有控制这种情况的方法,比如setTempoFactor。序列器将通过以不同的速率发送消息来响应此方法。

可以通过改变NOTE_ONNOTE_OFF消息的音调来改变音符的音调。这不仅要为将来的音符做,也要为当前正在播放的音符做。幸运的是,有一个名为弯音的 MIDI 命令,它可以被发送到合成器,以改变所有当前播放和未来音符的音高。弯音值 0x2000 对应于没有音高变化;高达 0x4000 的值表示音高增加,低于 0x 4000 的值表示音高减少。有许多网站给出了复杂的公式,但最简单的似乎是 MIDI 弯音范围( www.ultimatemetal.com/forum/threads/midi-pitch-bend-range.680677/ ),它指出 683 的音高变化大致是一个半音。因此,您更改音高值并向接收器发送一个新的弯音事件。

你寻找来自ʿ的用户的输入。然后这些调用适当的方法。说明这一点的程序是对本章前面给出的SimpleMidiPlayer的改编,称为AdjustableMidiPlayer.java。在正文中,您将对sleep的调用替换为以下内容:

        BufferedReader br = new BufferedReader(new
                                               InputStreamReader(System.in));
        String str = null;
        System.out.println("Enter lines of text.");
        System.out.println("Enter 'stop' to quit.");
        do {
            try {
                str = br.readLine();
                if (str.length() >= 2) {
                    byte[] bytes = str.getBytes();
                    if (bytes[0] == 27 && bytes[1] == 91) {
                        if (bytes[2] == 65) {
                            // up
                            increasePitch();
                        } else if (bytes[2] == 66) {
                            // down
                            decreasePitch();
                        } else if (bytes[2] == 67) {
                            //right
                            faster();
                        } else if (bytes[2] == 68) {
                            //left
                            slower();
                        }
                    }
                }
            } catch(java.io.IOException e) {
            }
        } while(!str.equals("stop"));
    }
where the new functions are given by
    private void increasePitch() {
        // 683 from www.ultimatemetal.com/forum/threads/midi-pitch-bend-range.680677/
        pitch += 683;
        for (int n = 0; n < 16; n++) {
            try {
                MidiMessage msg =
                    new ShortMessage(ShortMessage.PITCH_BEND,
                                     n,
                                     pitch & 0x7F, pitch >> 7);
                synthReceiver.send(msg, 0);
            } catch (Exception e) {
            }
        }
    }

    private void decreasePitch() {
        // 683 from www.ultimatemetal.com/forum/threads/midi-pitch-bend-range.680677/
        pitch -= 683;
        for (int n = 0; n < 16; n++) {
            try {
                MidiMessage msg =
                    new ShortMessage(ShortMessage.PITCH_BEND,
                                     n,
                                     pitch & 0x7F, pitch >> 7);
                synthReceiver.send(msg, 0);
            } catch (Exception e) {
            }
        }
    }

    float speed = 1.0f;

    private void faster() {
        speed *= 1.2f;
        sm_sequencer.setTempoFactor(speed);
    }

    private void slower() {
        speed /= 1.2f;
        sm_sequencer.setTempoFactor(speed);
    }

使用 TiMidity 代替默认的格维尔合成器

软合成器 TiMidity 可以作为后端合成器运行,使用 ALSA 序列器,如下所示:

$timidity -iA -B2,8 -Os -EFreverb=0

Opening sequencer port: 128:0 128:1 128:2 128:3

(对于 FluidSynth 来说也差不多。)这是在端口 128:0 上打开的,依此类推。

不幸的是,这对于 Java Sound 来说是不可见的,Java Sound 要么需要默认的 Gervill 合成器,要么需要原始的 MIDI 合成器,比如硬件合成器。正如在第十九章中所讨论的,你可以通过使用 ALSA 原始 MIDI 端口来解决这个问题。

您可以通过以下方式添加原始 MIDI 端口:

modprobe snd-seq snd-virmidi

这将把虚拟设备带入 ALSA 原始 MIDI 和 ALSA 音序器空间:

$amidi -l
Dir Device    Name
IO  hw:3,0    Virtual Raw MIDI (16 subdevices)
IO  hw:3,1    Virtual Raw MIDI (16 subdevices)
IO  hw:3,2    Virtual Raw MIDI (16 subdevices)
IO  hw:3,3    Virtual Raw MIDI (16 subdevices)

$aplaymidi -l
 Port    Client name                      Port name
 14:0    Midi Through                     Midi Through Port-0
 28:0    Virtual Raw MIDI 3-0             VirMIDI 3-0
 29:0    Virtual Raw MIDI 3-1             VirMIDI 3-1
 30:0    Virtual Raw MIDI 3-2             VirMIDI 3-2
 31:0    Virtual Raw MIDI 3-3             VirMIDI 3-3

虚拟原始 MIDI 端口 3-0 可以通过以下方式连接到 TiMidity 端口 0:

aconnect 28:0 128:0

玩到 TiMidity 的最后一步是把AdaptableMidiPlayer.java的一行字由此改过来:

if (info.toString().equals("SD20 [hw:2,0,0]")) {

对此:

if (info.toString().equals("VirMIDI [hw:3,0,0]")) {

结论

本章使用 MIDI API 构建了一些程序,并讨论了如何使用外部硬件合成器和软合成器,如 TiMidity。

十九、MIDI ALSA

ALSA 通过音序器 API 为 MIDI 设备提供了一些支持。客户端可以向音序器发送 MIDI 事件,音序器将根据事件的时间播放它们。然后,其他客户端可以接收这些有序事件,例如,合成它们。

资源

以下是一些资源:

ALSA 音序器客户端

ALSA 提供了一个音序器,可以从一组客户端接收 MIDI 事件,并根据事件中的定时信息播放给其他客户端。能够发送此类事件的客户端是文件读取器,如aplaymidi或其他序列器。客户端也可以读取应该播放的事件。可能的消费客户端包括分离器、路由器或软合成器,如 TiMidity。

TiMidity 可以运行一个 ALSA 音序器客户端,它会消耗 MIDI 事件并合成,根据 http://linux-audio.com/TiMidity-howto.html

timidity -iA -B2,8 -Os -EFreverb=0

在我的计算机上,这产生了以下内容:

Requested buffer size 2048, fragment size 1024
ALSA pcm 'default' set buffer size 2048, period size 680 bytes
TiMidity starting in ALSA server mode
Opening sequencer port: 129:0 129:1 129:2 129:3

然后它坐在那里等待连接。

FluidSynth 也可以用作服务器(参见http://tedfelix.com/linux/linux-midi.htmlTed 的 Linux MIDI 指南)。

 fluidsynth --server --audio-driver=alsa -C0 -R1 -l /usr/share/soundfonts/FluidR3_GM.sf2

ALSA 音序器发送 MIDI“连线”事件。这不包括 MIDI 文件事件,如文本或歌词元事件。这使得它对于 Karaoke 播放器来说毫无用处。可以修改文件阅读器aplaymid来将元事件发送给一个监听器(比如 Java MetaEventListener),但是由于这些事件来自文件阅读器而不是序列发生器,它们通常会在被排序播放之前到达。可惜。

pykaraoke这样的程序利用了 ALSA 序列器。然而,为了获得正确的歌词定时,它包括一个 MIDI 文件解析器,基本上作为第二个序列器,只提取和显示文本/歌词事件。

connect(连接)

程序aconnect可以用来列出 sequencer 服务器和客户机,比如 sequencer。我已经设置了两个客户端运行:TiMidity 和 seqdemo(稍后讨论)。这个命令

aconnect -o

显示以下内容:

client 14: 'Midi Through' [type=kernel]
    0 'Midi Through Port-0'
client 128: 'TiMidity' [type=user]
    0 'TiMidity port 0 '
    1 'TiMidity port 1 '
    2 'TiMidity port 2 '
    3 'TiMidity port 3 '
client 129: 'ALSA Sequencer Demo' [type=user]
    0 'ALSA Sequencer Demo'

当使用-i选项运行时,它会产生以下结果:

$aconnect -i
client 0: 'System' [type=kernel]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=kernel]
    0 'Midi Through Port-0'

程序aconnect可以通过以下方式在输入和输出客户端之间建立连接:

aconnect in out

seqdemo

Matthias Nagorni 的“ALSA 编程指南”中的程序seqdemo.c是一个基本的序列器客户端。它打开一个 MIDI 声音序列器客户端,然后进入一个轮询循环,打印关于收到的 MIDI 事件的信息。它简单介绍了 ALSA MIDI API。

seqdemo.c的代码如下:

/* seqdemo.c by Matthias Nagorni */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <alsa/asoundlib.h>

snd_seq_t *open_seq();
void midi_action(snd_seq_t *seq_handle);

snd_seq_t *open_seq() {

  snd_seq_t *seq_handle;
  int portid;

  if (snd_seq_open(&seq_handle, "default", SND_SEQ_OPEN_INPUT, 0) < 0) {
    fprintf(stderr, "Error opening ALSA sequencer.\n");
    exit(1);
  }
  snd_seq_set_client_name(seq_handle, "ALSA Sequencer Demo");
  if ((portid = snd_seq_create_simple_port(seq_handle, "ALSA Sequencer Demo",
            SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE,
            SND_SEQ_PORT_TYPE_APPLICATION)) < 0) {
    fprintf(stderr, "Error creating sequencer port.\n");
    exit(1);
  }
  return(seq_handle);
}

void midi_action(snd_seq_t *seq_handle) {

  snd_seq_event_t *ev;

  do {
    snd_seq_event_input(seq_handle, &ev);
    switch (ev->type) {
      case SND_SEQ_EVENT_CONTROLLER:
        fprintf(stderr, "Control event on Channel %2d: %5d       \r",
                ev->data.control.channel, ev->data.control.value);
        break;
      case SND_SEQ_EVENT_PITCHBEND:
        fprintf(stderr, "Pitchbender event on Channel %2d: %5d   \r",
                ev->data.control.channel, ev->data.control.value);
        break;
      case SND_SEQ_EVENT_NOTEON:
        fprintf(stderr, "Note On event on Channel %2d: %5d       \r",
                ev->data.control.channel, ev->data.note.note);
        break;
      case SND_SEQ_EVENT_NOTEOFF:
        fprintf(stderr, "Note Off event on Channel %2d: %5d      \r",
                ev->data.control.channel, ev->data.note.note);
        break;        ALSA Programming HOWTO
    }
    snd_seq_free_event(ev);
  } while (snd_seq_event_input_pending(seq_handle, 0) > 0);
}

int main(int argc, char *argv[]) {

  snd_seq_t *seq_handle;c
  int npfd;
  struct pollfd *pfd;

  seq_handle = open_seq();
  npfd = snd_seq_poll_descriptors_count(seq_handle, POLLIN);
  pfd = (struct pollfd *)alloca(npfd * sizeof(struct pollfd));
  snd_seq_poll_descriptors(seq_handle, pfd, npfd, POLLIN);
  while (1) {
    if (poll(pfd, npfd, 100000) > 0) {
      midi_action(seq_handle);
    }
  }
}

阿普莱米迪

该程序aplaymidi将发挥后端 MIDI 合成器,如 TiMidity。它需要一个端口名,可以通过以下内容找到:

aplaymidi -l

输出将如下所示:

 Port    Client name                      Port name
 14:0    Midi Through                     Midi Through Port-0
128:0    TiMidity                         TiMidity port 0
128:1    TiMidity                         TiMidity port 1
128:2    TiMidity                         TiMidity port 2
128:3    TiMidity                         TiMidity port 3
131:0    aseqdump                         aseqdump

端口号与aconnect使用的端口号相同。这些不是 ALSA 设备名(hw:0等等),而是 ALSA 序列器 API 所特有的。

然后,它可以向其中一个端口播放 MIDI 文件,如下所示:

aplaymidi -p 128:0 54154.mid

代码可以在 SourceArchive.com(http://alsa-utils.sourcearchive.com/documentation/1.0.8/aplaymidi_8c-source.html)找到。

原始 MIDI 端口

根据 RawMidi 接口( www.alsa-project.org/alsa-doc/alsa-lib/rawmidi.html ),RawMidi 接口“被设计为通过 Midi 线路写入或读取原始(未改变的)Midi 数据,而没有在接口中定义的任何时间戳。”

原始 MIDI 物理设备

raw MIDI 接口通常用于管理硬件 MIDI 设备。例如,如果我将 Edirol SD-20 合成器插入 USB 端口,它会在amidi下显示如下:

$amidi -l
Dir Device    Name
IO  hw:2,0,0  SD-20 Part A
IO  hw:2,0,1  SD-20 Part B
I   hw:2,0,2  SD-20 MIDI

这些名称使用与hw:...的 ALSA 回放和记录设备相同的模式。

原始 MIDI 虚拟设备

Linux 内核模块snd_virmidi可以创建虚拟的原始 MIDI 设备。首先添加模块(参见 https://wiki.allegro.cc/index.php?title=Using_TiMidity%2B%2B_with_ALSA_raw_MIDIAlsaMidiOverview [ http://alsa.opensrc.org/AlsaMidiOverview )。

modprobe snd-seq snd-virmidi

这将把虚拟设备带入 ALSA 原始 MIDI 和 ALSA 音序器空间:

$amidi -l
Dir Device    Name
IO  hw:3,0    Virtual Raw MIDI (16 subdevices)
IO  hw:3,1    Virtual Raw MIDI (16 subdevices)
IO  hw:3,2    Virtual Raw MIDI (16 subdevices)
IO  hw:3,3    Virtual Raw MIDI (16 subdevices)

$aplaymidi -l
 Port    Client name                      Port name
 14:0    Midi Through                     Midi Through Port-0
 28:0    Virtual Raw MIDI 3-0             VirMIDI 3-0
 29:0    Virtual Raw MIDI 3-1             VirMIDI 3-1
 30:0    Virtual Raw MIDI 3-2             VirMIDI 3-2
 31:0    Virtual Raw MIDI 3-3             VirMIDI 3-3

将 MIDI 客户端映射到 MIDI 原始空间

一些程序/API 使用 ALSA 序列器空间;其他人使用 ALSA raw MIDI 空间。虚拟端口允许使用一个空间的客户端使用不同空间的客户端。

例如,TiMidity 可以作为 sequencer 客户端运行,如下所示:

timidity -iA -B2,8 -Os -EFreverb=0

这仅显示在音序器空间中,不显示在原始 MIDI 空间中,并显示给aconnect -o如下:

$aconnect -o
client 14: 'Midi Through' [type=kernel]
    0 'Midi Through Port-0'
client 28: 'Virtual Raw MIDI 3-0' [type=kernel]
    0 'VirMIDI 3-0     '
client 29: 'Virtual Raw MIDI 3-1' [type=kernel]
    0 'VirMIDI 3-1     '
client 30: 'Virtual Raw MIDI 3-2' [type=kernel]
    0 'VirMIDI 3-2     '
client 31: 'Virtual Raw MIDI 3-3' [type=kernel]
    0 'VirMIDI 3-3     '
client 128: 'TiMidity' [type=user]
    0 'TiMidity port 0 '
    1 'TiMidity port 1 '
    2 'TiMidity port 2 '
    3 'TiMidity port 3 '

aconnect -i显示虚拟端口如下:

$aconnect -i
client 0: 'System' [type=kernel]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=kernel]
    0 'Midi Through Port-0'
client 28: 'Virtual Raw MIDI 3-0' [type=kernel]
    0 'VirMIDI 3-0     '
client 29: 'Virtual Raw MIDI 3-1' [type=kernel]
    0 'VirMIDI 3-1     '
client 30: 'Virtual Raw MIDI 3-2' [type=kernel]
    0 'VirMIDI 3-2     '
client 31: 'Virtual Raw MIDI 3-3' [type=kernel]
    0 'VirMIDI 3-3     '

然后,虚拟原始 MIDI 3-0 可以通过以下方式连接到 TiMidity 端口 0:

aconnect 28:0 128:0

然后客户可以发送 MIDI 信息到原始的 MIDI 设备hw:3,0,TiMidity 会合成它们。在前一章中,我通过展示如何用 TiMidity 替换默认的 Java 合成器来使用它。

关闭所有笔记

如果你在一个设备或软合成器上播放某个东西,那么如果这个东西被打断,它可能无法干净地播放完。例如,它可能在某个通道上启动了一个NOTE ON,但由于中断,它不会发送一个通知。合成器将继续播放音符。

要停止播放,使用amidi发送“原始”MIDI 命令。十六进制序列 00 B0 7B 00 将发送“通道 0 上的所有音符关闭”类似地,命令 00 B1 7B 00 将发送“通道 1 上的所有音符关闭”,并且只有 16 个可能的通道。

端口hw:1,0上原始设备的相关命令如下:

amidi -p hw:1,0 -S "00 B0 7B 00"
...

结论

本章简要讨论了 ALSA 下可用的 MIDI 模型。虽然这背后有一个重要的编程 API,但您主要使用了命令amidiaplaymidiaconnect,并且看到了使用seqdemo.c程序的 API。

二十、FluidSynth

FluidSynth 是一个播放 MIDI 文件的应用,也是一个 MIDI 应用库。

资源

以下是一些资源:

演员

fluidsynth是一个命令行 MIDI 播放器。它在 ALSA 的命令行下运行,如下所示:

fluidsynth -a alsa -l <soundfont> <files...>

一种常用的声音字体是/usr/share/sounds/sf2/FluidR3_GM.sf2

Qsynth 是fluidsynth的 GUI 界面。看起来像图 20-1 。

A435426_1_En_20_Fig1_HTML.jpg

图 20-1。

Qsynth

播放 MIDI 文件

FluidSynth API 包括以下内容:

  • 使用new_fluid_player创建的音序器
  • 使用new_fluid_synth创建的合成器
  • 使用new_fluid_audio_driver创建的音频播放器,它运行在一个单独的线程中
  • 一个“设置”对象,可用于控制其他组件的许多功能,由new_fluid_settings创建,并由fluid_settings_setstr等调用修改

使用 ALSA 播放 MIDI 文件序列的典型程序如下。它创建各种对象,设置音频播放器使用 ALSA,然后将每个声音字体和 MIDI 文件添加到播放器中。然后对fluid_player_play的调用依次播放每个 MIDI 文件。

#include <fluidsynth.h>
#include <fluid_midi.h>

int main(int argc, char** argv)
{
    int i;
    fluid_settings_t* settings;
    fluid_synth_t* synth;
    fluid_player_t* player;
    fluid_audio_driver_t* adriver;

    settings = new_fluid_settings();
    fluid_settings_setstr(settings, "audio.driver", "alsa");
    synth = new_fluid_synth(settings);
    player = new_fluid_player(synth);

    adriver = new_fluid_audio_driver(settings, synth);
    /* process command line arguments */
    for (i = 1; i < argc; i++) {
        if (fluid_is_soundfont(argv[i])) {
            fluid_synth_sfload(synth, argv[1], 1);
        } else {
            fluid_player_add(player, argv[i]);
        }
    }
    /* play the midi files, if any */
    fluid_player_play(player);
    /* wait for playback termination */
    fluid_player_join(player);
    /* cleanup */
    delete_fluid_audio_driver(adriver);
    delete_fluid_player(player);
    delete_fluid_synth(synth);
    delete_fluid_settings(settings);
    return 0;
}

计算机编程语言

pyFluidSynth 是一个绑定到 FluidSynth 的 Python,允许你向 FluidSynth 发送 MIDI 命令。

结论

本章简要讨论了 FluidSynth 的编程模型和 API。

二十一、TiMidity

TiMidity 被设计成一个独立的应用。为此,您应该构建一个新的“接口”它也可以被颠覆,就好像它是一个可以被调用的库。本章解释了这两种方式。

TiMidity 设计

TiMidity 被设计成一个独立的应用。当它被构建时,你得到了一个可执行文件,但是没有一个可以被调用的函数库,不像 FluidSynth。

TiMidity 你能做的就是增加不同的接口。例如,有 ncurses、Xaw 和哑接口可以在运行时调用,例如:

timidity -in ...
timidity -ia ...
timidity -id ...

还有其他更专业的应用,如 WRD、emacs、ALSA 和远程接口。

例如,Xaw 接口看起来如图 21-1 所示。

A435426_1_En_21_Fig1_HTML.jpg

图 21-1。

TiMidity with Xaw interface

这个想法似乎是,如果你想要额外的东西,也许你应该建立一个自定义界面,并驱动它从 TiMidity。

这并不总是适合我,因为我更喜欢能够以一种简单的方式将 TiMidity 嵌入到我自己的应用中。本章的其余部分从两方面来看。

  • 将 TiMidity 转化为一个库,并将其包含在您自己的代码中
  • 构建您自己的界面

把 TiMidity 变成图书馆

TiMidity 不是被设计成一个库,所以你必须说服它。这并不难;你只需要摆弄一下构建系统。

托管环境挂钩

一个由应用控制的系统在一个受管理的环境中工作得不是很好,比如 Windows(或者更近一些的环境,比如 Android)。在这样的环境中,你不能调用 TiMidity 的main,而是调用属于框架的main函数。这将依次调用应用中的适当函数。

要使用这样的钩子,你需要下载 TiMidity 的源代码,要么从包管理器,要么从 TiMidity++网站( http://timidity.sourceforge.net/ )。

出于谨慎,main函数的变体在文件timidity/timidity.c中。被各种define控制,可以有main或者win_main。更有趣的定义之一是ANOTHER_MAIN。如果这样定义,那么main函数的任何版本都不会被编译,你会得到一个自由主对象模块。

如果从顶级源目录按以下方式构建 TiMidity,将产生一个错误,即main函数未定义:

CFLAGS="-DANOTHER_MAIN" ./configure
make

这是一个钩子,你需要把 TiMidity 从一个独立的应用变成能够被另一个应用调用的库。请注意,您不能仅仅从构建中删除timidity/timidity.c。该文件包含太多其他关键功能!

建造图书馆

要将 TiMidity 构建为一个静态库,移除前面所示的main函数,并尝试构建 TiMidity。我发现我还需要指定我想要使用的输出系统,比如 ALSA。

CFLAGS="-DANOTHER_MAIN" ./configure --enable-audio=alsa
nake clean
make

这构建了几个.ar文件和许多对象.o模块,但无法构建最终的timidity可执行文件,因为(当然)没有main函数。它还会在timidity子目录中留下一堆未链接的文件。

通过从 TiMidity 源目录的顶部运行以下命令,可以将所有的目标模块收集到一个归档文件中:

ar cru  libtimidity.a */*.o
ranlib libtimidity.a

因为您必须从源代码开始构建 TiMidity,所以在您尝试构建这个备选库版本之前,请检查它是否在正常模式下工作。通过这种方式,你可以发现你需要,比如说,libasound-dev库来使用 ALSA,在你被其他东西弄混之前!

图书馆入口点

ANOTHER_MAIN构建的 TiMidity 暴露了这些公共入口点:

void timidity_start_initialize(void);
int timidity_pre_load_configuration(void);
int timidity_post_load_configuration(void);
void timidity_init_player(void);
int timidity_play_main(int nfiles, char **files);
int got_a_configuration;

它们似乎没有在任何方便的头文件中定义。

最小的应用

真正 TiMidity 的应用被编码为在许多不同的操作系统上使用许多不同版本的库。如前所述,在构建目标文件和库的过程中,大部分依赖项都被考虑进去了。

一个最小的应用只是在my_main.c中的库入口点周围包装你自己的main

#include <stdio.h>

extern void timidity_start_initialize(void);
extern int timidity_pre_load_configuration(void);
extern int timidity_post_load_configuration(void);
extern void timidity_init_player(void);
extern int timidity_play_main(int nfiles, char **files);
extern int got_a_configuration;

int main(int argc, char **argv)
{
    int err, main_ret;

    timidity_start_initialize();

    if ((err = timidity_pre_load_configuration()) != 0)
        return err;

    err += timidity_post_load_configuration();

    if (err) {
        printf("couldn't load configuration file\n");
        exit(1);
    }

    timidity_init_player();

    main_ret = timidity_play_main(argc, argv);

    return main_ret;
}

compile命令需要引入 TiMidity 库和任何其他需要的库,用于 ALSA 应用。

my_timidity: my_main.o
        gcc -g -o my_timidity my_main.o libtimidity.a  -lasound -lm

向 MIDI 文件播放背景视频

作为一个更复杂的例子,让我们看看在播放 MIDI 文件的同时播放一个视频文件。假设视频文件没有音频成分,并且没有尝试在两个流之间执行任何同步——这是额外的复杂性!

要播放视频文件,您可以使用 FFmpeg 库将视频流解码为视频帧。然后,您需要在某种 GUI 对象中显示这些帧,有许多工具包可以做到这一点。我选择了 Gtk 工具包,因为它是 Gnome 的基础,是 C 语言的,支持许多其他东西,比如 i18n 等等。我的代码是基于斯蒂芬·德朗格的“一个 ffmpeg 和 SDL 教程”( http://dranger.com/ffmpeg/ ),它使用了 SDL 工具包来显示。

这使用pthreads包在单独的线程中运行视频和 MIDI。我通过硬编码文件名和固定视频帧的大小来作弊。让它在 Gtk 3.0 下工作真的很糟糕,因为 Gtk 3.0 已经移除了像素图,而且花了太多太长的时间才发现发生了什么。

我将代码分成了两个文件,一个使用 Gtk 播放视频,另一个播放 TiMidity 库并调用视频。视频播放文件为video_code.c。此处省略了代码,因为它本质上是第十五章中描述的代码。

文件video_player.c设置 TiMidity 环境,调用视频在后台播放,然后调用play_midi。内容如下:

#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void timidity_start_initialize(void);
int timidity_pre_load_configuration(void);
int timidity_post_load_configuration(void);
void timidity_init_player(void);

void *init_gtk(void *args);
void init_ffmpeg();

#define MIDI_FILE "54154.mid"

static void *play_midi(void *args) {
    char *argv[1];
    argv[0] = MIDI_FILE;
    int argc = 1;

    timidity_play_main(argc, argv);

    printf("Audio finished\n");
    exit(0);
}

int main(int argc, char** argv)
{

    int i;

    /* Timidity stuff */
    int err;

    timidity_start_initialize();
    if ((err = timidity_pre_load_configuration()) == 0) {
        err = timidity_post_load_configuration();
    }
    if (err) {
        printf("couldn't load configuration file\n");
        exit(1);
    }

    timidity_init_player();

    init_ffmpeg();
    pthread_t tid_gtk;
    pthread_create(&tid_gtk, NULL, init_gtk, NULL);

    play_midi(NULL);
    return 0;
}

构建新界面

前面的部分播放了 MIDI 和背景视频,本质上是作为独立的应用,作为独立的非交互线程。TiMidity 允许一个用户界面的更大集成,可以动态地添加到 TiMidity 中。

共享对象

你可以构建自己的接口,添加到 TiMidity 中,而不用改变或重新编译 TiMidity。这样的接口被构建为可动态加载的共享库,当 TiMidity 开始时被加载。

你必须小心编译和链接标志来构建这些库(参见 http://stackoverflow.com/questions/7252550/loadable-bash-builtin “在 Linux 中构建共享对象”)。为了从my_interface.c构建共享对象if_my_interface.so,我使用了以下代码:

gcc  -fPIC $(CFLAGS) -c -o my_interface.o my_interface.c
gcc -shared -o if_my_interface.so my_interface.o

TiMidity 只会加载以if_开头的文件。它们可以驻留在任何目录中,默认为类似于/usr/lib/timidity/usr/local/lib/timidity(参见timidity -h中的“支持的动态加载接口”目录)。

加载动态模块的默认目录可以用选项-d覆盖,如下所示:

timidity -d. -im --trace 54154.mid

入口点

每个接口必须有一个可以被动态加载器调用的唯一函数。回想一下,使用命令行选项-i选择接口,例如timidity -iT ...,以选择 VT100 接口。你的接口必须有一个不被任何其他接口使用的 ASCII 字母标识符,比如说m代表“我的接口”然后加载程序将寻找一个函数,如下所示,其中函数名中的m是标识符:

ControlMode *interface_m_loader(void)

这个函数很简单:它只是返回类型为ControlMode的结构的地址,该结构在接口代码的其他地方定义。

ControlMode *interface_m_loader(void)
{
    return &ctl;
}

控制方式

ControlMode结构如下:

typedef struct {
  char *id_name, id_character;
  char *id_short_name;
  int verbosity, trace_playing, opened;

  int32 flags;

  int  (*open)(int using_stdin, int using_stdout);
  void (*close)(void);
  int (*pass_playing_list)(int number_of_files, char *list_of_files[]);
  int  (*read)(int32 *valp);
  int  (*write)(char *buf, int32 size);
  int  (*cmsg)(int type, int verbosity_level, char *fmt, ...);
  void (*event)(CtlEvent *ev);  /* Control events */
} ControlMode;

这定义了关于接口和一组函数的信息,这些函数由 TiMidity 调用,以响应 TiMidity 内的事件和动作。例如,对于“我的界面”,该结构如下:

ControlMode ctl=
    {
        "my interface", 'm',
        "my iface",
        1,          /* verbosity */
        0,          /* trace playing */
        0,          /* opened */
        0,          /* flags */
        ctl_open,
        ctl_close,
        pass_playing_list,
        ctl_read,
        NULL,       /* write */
        cmsg,
        ctl_event
    };

这些领域有些是显而易见的,但有些则不那么明显。

open

  • 调用这个函数来设置哪些文件用于 I/O。

close

  • 这叫做关闭它们。

pass_playing_list

  • 这个函数被传递了一个要播放的文件列表。最有可能的操作是遍历这个列表,对每个列表调用play_midi_file

read

  • 我还不确定这是干什么用的。

write

  • 我还不确定这是干什么用的。

cmsg

  • 这被称为信息消息。

event

  • 这是处理 MIDI 控制事件的主要功能。通常,对于每种类型的控制事件,它都是一个大开关。

包括文件

这太乱了。一个典型的接口将需要知道 TiMidity 使用的一些常数和函数。虽然这些是为 TiMidity 而有逻辑地组织起来的,但是对于一个新的界面来说,它们不是方便地组织起来的。所以,你必须不断地引入额外的包含,这些额外的包含指向其他外部的,需要更多的包含,等等。这些可能在不同的目录中,比如timidityutils,所以你必须指向许多不同的包含目录。

请注意,您将需要 TiMidity 的源代码来获得这些包含文件;你可以从 SourceForge TiMidity++下载( http://sourceforge.net/projects/timidity/?source=dlp )。

我的简单界面

这基本上和 TiMidity 中内置的“哑”接口做的是一样的。它是从当前目录加载的,并通过以下命令调用:

timidity -im -d. 54154.mid

代码在一个文件中,my_interface.c

下面的代码中有两个主要的函数,其余的都省略了。重要的功能有ctl_eventctl_lyric。函数ctl_event处理 TiMidity 产生的事件。对于这个接口,您只想在播放时打印歌词,所以当一个CTLE_LYRIC事件发生时,调用ctl_lyricctl_lyric功能使用 TiMidity 功能event2string查找歌词,并将其打印到输出,如果需要,根据歌词文本打印换行符。接口文件如下所示:

/*
  my_interface.c
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#ifndef NO_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif

#include "support.h"
#include "timidity.h"
#include "output.h"
#include "controls.h"
#include "instrum.h"
#include "playmidi.h"
#include "readmidi.h"

static int ctl_open(int using_stdin, int using_stdout);
static void ctl_close(void);
static int ctl_read(int32 *valp);
static int cmsg(int type, int verbosity_level, char *fmt, ...);
static void ctl_total_time(long tt);
static void ctl_file_name(char *name);
static void ctl_current_time(int ct);
static void ctl_lyric(int lyricid);
static void ctl_event(CtlEvent *e);
static int pass_playing_list(int number_of_files, char *list_of_files[]);

#define ctl karaoke_control_mode

ControlMode ctl=
    {
        "my interface", 'm',
        "my iface",
        1,          /* verbosity */
        0,          /* trace playing */
        0,          /* opened */
        0,          /* flags */
        ctl_open,
        ctl_close,
        pass_playing_list,
        ctl_read,
        NULL,       /* write */
        cmsg,
        ctl_event
    };

static FILE *outfp;
int karaoke_error_count;
static char *current_file;
struct midi_file_info *current_file_info;

static int pass_playing_list(int number_of_files, char *list_of_files[]) {
    int n;

    for (n = 0; n < number_of_files; n++) {
        printf("Playing list %s\n", list_of_files[n]);

        current_file = list_of_files[n];

        play_midi_file( list_of_files[n]);
    }
    return 0;
}

/*ARGSUSED*/
static int ctl_open(int using_stdin, int using_stdout)
{
    // sets output channel and prints info about the file

}

static void ctl_close(void)
{
    // close error channel

}

/*ARGSUSED*/
static int ctl_read(int32 *valp)
{
    return RC_NONE;
}

static int cmsg(int type, int verbosity_level, char *fmt, ...)
{
    // prints an error message

    return 0;
}

static void ctl_total_time(long tt)
{
    // counts playing time

}

static void ctl_file_name(char *name)
{
    // prints playing status

}

static void ctl_current_time(int secs)
{
    // keeps track of current time

}

static void ctl_lyric(int lyricid)
{
    char *lyric;

    current_file_info = get_midi_file_info(current_file, 1);

    lyric = event2string(lyricid);
    if(lyric != NULL)
        {
            if(lyric[0] == ME_KARAOKE_LYRIC)
                {
                    if(lyric[1] == '/' || lyric[1] == '\\')
                        {
                            fprintf(outfp, "\n%s", lyric + 2);
                            fflush(outfp);
                        }
                    else if(lyric[1] == '@')
                        {
                            if(lyric[2] == 'L')
                                fprintf(outfp, "\nLanguage: %s\n", lyric + 3);
                            else if(lyric[2] == 'T')
                                fprintf(outfp, "Title: %s\n", lyric + 3);
                            else
                                fprintf(outfp, "%s\n", lyric + 1);
                        }
                    else
                        {
                            fputs(lyric + 1, outfp);
                            fflush(outfp);
                        }
                }
            else
                {
                    if(lyric[0] == ME_CHORUS_TEXT || lyric[0] == ME_INSERT_TEXT)
                        fprintf(outfp, "\r");
                    fputs(lyric + 1, outfp);
                    fflush(outfp);
                }
        }
}

static void ctl_event(CtlEvent *e)
{
    switch(e->type)
        {
        case CTLE_NOW_LOADING:
            ctl_file_name((char *)e->v1);
            break;
        case CTLE_LOADING_DONE:
            // MIDI file is loaded, about to play
            current_file_info = get_midi_file_info(current_file, 1);
            if (current_file_info != NULL) {
                printf("file info not NULL\n");
            } else {
                printf("File info is NULL\n");
            }
            break;
        case CTLE_PLAY_START:

            ctl_total_time(e->v1);
            break;
        case CTLE_CURRENT_TIME:
            ctl_current_time((int)e->v1);
            break;
#ifndef CFG_FOR_SF
        case CTLE_LYRIC:
            ctl_lyric((int)e->v1);
            break;
#endif
        }
}

/*
 * interface_<id>_loader();
 */
ControlMode *interface_m_loader(void)
{
    return &ctl;
}

它被编译成接口文件if_my_interface.so,如下所示:

gcc  -fPIC -c -o my_interface.o my_interface.c
gcc -shared  -o if_my_interface.so my_interface.o

运行我的简单界面

当我试图使用标准包 TiMidity v2.13.2-40.1 运行该接口时,它在一个内存释放调用中崩溃。代码被剥离了,所以追踪原因并不容易,我还没有费心去做——我不确定软件包发行版是针对什么库、代码版本等等进行编译的。

我从源头上建立了自己的 TiMidity 的副本。这工作得很好。请注意,当您从源代码构建 TiMidity 时,您需要指定它可以加载动态模块,例如,使用以下代码:

congfigure --enable-audio=alsa --enable-vt100 --enable-debug –enable-dynamic

与源建立在子目录 TiMidity++-2.14.0,玩使用这个界面是由

TiMidity++-2.14.0/timidity/timidity -d. -im 54154.mid

向 MIDI 文件播放背景视频

你可以从播放之前给出的视频中获取代码,并把它作为 TiMidity 系统的“后端”作为“视频”接口。本质上所有需要做的就是从简单的接口改变ctl_open来调用 Gtk 代码播放视频,改变接口的身份。

新的“视频”界面是video_player_interface.c。唯一重要的变化是对ctl_open的修改,现在内容如下:

extern void init_gtk(void *args);

/*ARGSUSED*/
static int ctl_open(int using_stdin, int using_stdout)
{

    outfp=stdout;
    ctl.opened=1;

    init_ffmpeg();

    /* start Gtk in its own thread */
    pthread_t tid_gtk;
    pthread_create(&tid_gtk, NULL, init_gtk, NULL);

    return 0;
}

if_video_player.so

构建命令如下所示:

CFLAGS = -ITiMidity++-2.14.0/timidity -ITiMidity++-2.14.0 -ITiMidity++-2.14.0/utils  $(shell pkg-config --cflags gtk+-3.0 libavformat libavcodec libswscale libavutil )

LIBS3 =  $(shell pkg-config --libs gtk+-3.0 libavformat libavcodec libswscale libavutil )

video_code.o: video_code.c
        gcc  -fPIC $(CFLAGS) -c -o video_code.o video_code.c

if_video_player.so: video_player_interface.c video_code.o
        gcc  -fPIC $(CFLAGS) -c -o video_player_interface.o video_player_interface.c
        gcc -shared -o if_video_player.so video_player_interface.o video_code.o \
        $(LIBS3)

它使用以下命令运行:

TiMidity++-2.14.0/timidity/timidity -d. -iv
 54154.mid

结论

TiMidity 不是为其他应用设计的。要么你添加一个新的接口,要么你绕开 TiMidity 的设计去生产一个库。本章展示了这两种机制,并用简单和更复杂的例子进行了说明。

二十二、Karaoke 系统概述

这一章简要总结了连续的几章。

从我的角度来看,这本书的全部目的是记录在我构建 Linux Karaoke 系统的过程中 Linux sound 发生了什么。这一章着眼于我利用前几章的材料所做的各种探索。

首先,我的目标是什么?

  • 能够播放 KAR 文件(一种可能的 Karaoke 文件格式)
  • 每次至少显示一行歌词,突出显示应该唱的字符
  • 对于中文歌曲,显示歌词的拼音(英文)形式以及中文字符
  • 在背景中播放电影
  • 以某种形式展示旋律
  • 显示与旋律相对唱的音符
  • 以某种方式给结果打分

我所做的一切都没有接近这些目标。让我挑选出我迄今为止探索的亮点:

  • 最简单的“现成”系统是 PyKaraoke,kmid 是它的忠实追随者。这些播放 KAR 文件和突出显示歌词,但仅此而已。
  • 向这种系统添加麦克风输入的最简单方法是使用外部混音器。这些也可以做混响和其他效果。
  • Jack 和 PulseAudio 可以轻松地用于添加麦克风输入作为播放,但效果需要更多的工作。
  • Java 在几乎所有方面都很酷——除了延迟最终会毁了它。
  • FluidSynth 可以被黑客攻击以提供挂钩来悬挂 Karaoke。但是它是 CPU 密集型的,没有为其他处理留出空间。
  • TiMidity 是一个独立的系统,具有可配置的后端。它可以被配置成一个简陋的 Karaoke 系统。但是可以通过黑客攻击使其成为一个库,这赋予了它更多的潜力。它不像 FluidSynth 那样占用大量 CPU 资源。
  • 播放背景电影可以使用 FFmpeg 和 Gtk 之类的 GUI 来完成。Gtk 也有在视频上叠加高亮歌词的机制,但是 Gtk 2 和 Gtk 3 的机制不同。
  • TiMidity 可以与 FFmpeg 和 Gtk 结合使用,在电影背景下显示突出显示的歌词。
  • 尽管 Java 库 TarsosDSP 可以提供大量信息,但目前还看不到计分。

以下章节涵盖了这些主题:

用户级工具

  • Karaoke 是一种“观众参与”的音响系统,在这种系统中,配乐和通常的旋律随着歌词的移动显示一起播放。本章考虑了播放 Karaoke 的功能、格式和用户级工具。

解码松肯卡拉 DVD 上的 DKD 文件

  • 这一章是关于从我的 Songken 卡拉 DVD 中获取信息,这样我就可以开始编写播放歌曲的程序。它不直接参与在 Linux 下播放声音,作为附录给出。

Java 声音

  • Java 声音对 Karaoke 没有直接支持。本章着眼于如何将 Java 声音库与其他库(如 Swing)结合起来,为 MIDI 文件提供一个 Karaoke 播放器。

副标题

  • 许多 Karaoke 系统使用加在某种电影上的字幕。本章着眼于如何在 Linux 系统上做到这一点。选择有限,但有可能。

流体合成

  • FluidSynth 是一个播放 MIDI 文件的应用,也是一个 MIDI 应用库。它没有播放 Karaoke 文件的挂钩。本章讨论 FluidSynth 的一个扩展,它添加了适当的钩子,然后使用这些钩子来构建各种各样的 Karaoke 系统。

TiMidity

  • TiMidity 被设计成一个独立的应用,具有一种特殊的可扩展性。开箱后,它可以播放 Karaoke,但不是很好。这一章着眼于如何与 TiMidity 建立一个 Karaoke 系统。

二十三、Karaoke 用户级工具

Karaoke 是一种“观众参与”的音响系统,在这种系统中,配乐和通常的旋律随着歌词的移动显示一起播放。在此范围内,可以有变化或不同的特征。

  • 歌词可以一次全部显示,而音乐按顺序播放。
  • 歌词可以与旋律线同步突出显示。
  • 旋律线可能会一直播放或可以关闭。
  • 一些球员还将包括一个歌手唱这首歌。
  • 有些有人声的玩家会在有人唱歌的时候关掉人声。
  • 有些演奏者会给出旋律音符的图形显示。
  • 一些演奏者会给出旋律的图形显示,还会显示歌手所唱的音符。
  • 有些选手会根据对歌手准确性的一些评价来打分。这种评分的基础通常是未知的。
  • 有些播放器允许你改变播放速度和播放音高。
  • 大多数播放器将接受两个麦克风,并可以为歌手的声音添加混响效果。
  • 很多播放器会让你提前选择歌曲,建立动态播放列表。

Karaoke 在亚洲很受欢迎,在欧洲国家也有追随者。Karaoke 系统被认为起源于亚洲,尽管根据维基百科( http://en.wikipedia.org/wiki/Karaoke )记载的历史有点模糊。

www.karawin.fr/defenst.php 中描述了 Karaoke 的各种文件格式。本章考虑了播放 Karaoke 的功能、格式和用户级工具。

视频光盘系统

视频 CD 是光盘上一种较老的视频存储形式。分辨率相当低,一般为 352×240 像素,帧率为每秒 25 帧。虽然它们被一些电影使用,但是它们已经被 DVD 电影所取代。然而,它们曾一度被广泛用于 Karaoke 唱片。

来自亚洲的较便宜的 CD/DVD 播放器通常有麦克风输入,可以与 VCD 光盘一起用作 Karaoke 播放器。通常文件是 AVI 或 MPEG 格式的简单电影,所以你可以跟着唱。虽然歌词通常会随着旋律及时突出显示,但没有诸如乐谱或旋律显示之类的高级功能。

如果您有 VCD 光盘,它们可以作为 IC9660 文件安装在您的计算机上,但是在 Linux 系统上,您不能直接提取这些文件。像 VLC、MPlayer 和图腾这样的玩家可以播放它们的文件。

你需要使用类似vcdimager的东西从 VCD 光盘中提取文件。这可能在你的包系统中,或者你可以从 GNU 开发者网站( www.gnu.org/software/vcdimager/ )下载并从源代码编译它。然后,可以使用以下内容将视频文件提取为 MPEG 或 AVI 文件:

          vcdxrip --cdrom-device=/dev/cdrom --rip

(在我的系统上,我不得不将/dev/cdrom替换为/dev/sr1,因为我无法从默认的 DVD 播放器中提取。我通过运行mount找到了它是什么设备,然后用umount卸载了它。)

CD+G 光盘

根据维基百科的“CD+G”页面( https://en.wikipedia.org/wiki/CD%2BG ),“CD+G(也称为 CD+Graphics)是光盘标准的扩展,当在兼容设备上播放时,可以在光盘上的音频数据旁边呈现低分辨率的图形。CD+G 光盘通常用于 Karaoke 机,Karaoke 机利用这一功能在屏幕上显示光盘上歌曲的歌词。

每首歌曲由两个文件组成:一个音频文件和一个包含歌词的视频文件(可能还有一些背景场景)。

您可以使用这种格式购买许多光盘。你不能在电脑上直接播放它们。Rhythmbox 将播放音频,但不播放视频。VLC 和图腾不喜欢他们。

将文件翻录到电脑上存储在硬盘上并不那么简单。音频光盘没有正常意义上的文件系统。例如,您不能使用 Unix mount命令挂载它们;它们甚至不是 ISO 格式的。相反,你需要使用一个类似于cdrdao的程序将文件解压成一个二进制文件,然后对其进行处理。

 $ cdrdao read-cd --driver generic-mmc-raw --device /dev/cdroms/cdrom0 --read-subchan rw_raw mycd.toc

前面的代码创建了一个数据文件和一个目录文件。

CDG 文件的格式显然还没有公开发布,但是由 Jim Bumgardner(早在 1995 年!)在“CD+G 揭示:在软件中回放 Karaoke 曲目”( http://jbum.com/cdg_revealed.html )。

声音榨汁机等程序会提取音轨,但留下视频。

MP3+G 文件

MP3+G 文件是适用于普通电脑的 CD+G 文件。它们由包含音频的 MP3 文件和包含歌词的 CDG 文件组成。通常它们是用拉链拉在一起的。

很多卖 CD+G 文件的网站也卖 MP3+G 文件。各种网站给出了如何创建自己的 MP3+G 文件的说明。免费网站不多。

来自cdgtools-0.3.2的程序cgdrip.py可以从音频光盘中抓取 CD+G 文件,并将其转换为一对 MP3+G 文件。(Python)源代码中的指令如下:

# To start using cdgrip immediately, try the following from the
# command-line (replacing the --device option by the path to your
# CD device):
#
#  $ cdrdao read-cd --driver generic-mmc-raw --device /dev/cdroms/cdrom0 --read-subchan rw_raw mycd.toc
#  $ python cdgrip.py --with-cddb --delete-bin-toc mycd.toc
#
# You may need to use a different --driver option or --read-subchan mode
# to cdrdao depending on your CD device. For more in depth details, see
# the usage instructions below.

购买 CD+G 或 MP3+G 文件

有很多卖 CD+G 和 MP3+G 歌曲的网站。只要在谷歌上搜索一下。然而,每首歌的平均价格约为 3 美元,如果你想建立一个大的收藏,这可能会变得昂贵。一些网站会对大量购买给予折扣,但即使 100 首歌曲 30 美元,费用也可能很高。

拥有大量收藏的网站来来去去。在撰写本文时,aceume.com 为我们提供了 14000 首英文歌曲,售价 399 美元。但你可以花 600 美元购买他们的 AK3C Android 一体机云 Karaoke 播放器,其中包含 21,000 首英文歌曲和 35,000 首中文歌曲。这使得建造自己的 Karaoke 播放器的经济性变得不稳定。我将忽略这个问题,这是你的选择!

将 MP3+G 转换为视频文件

工具ffmpeg可以将音频和视频合并成一个视频文件,例如:

ffmpeg -i Track1.cdg -i Track1.mp3 -y Track1.avi

使用以下内容创建包含视频和音频的 AVI 文件:

avconv -i Track1.cdg -i Track1.mp3 test.avi
avconv -i test.avi -c:v libx264 -c:a copy outputfile.mp4

这个可以用 VLC,MPlayer,Rhythmbox 等等来玩。

有一个程序叫做 cdg2video 。它的最后日期是 2011 年 2 月,FFmpeg 内部的变化意味着它不再编译。即使您修复了这些明显的变化,C 编译器也会对不推荐使用的 FFmpeg 函数提出大量的抱怨。

MPEG-4 文件

使用 MPEG-4 视频播放器的 Karaoke 系统越来越普遍。这些将所有信息嵌入到视频中。这些档案的球员没有计分系统。

有些人认为它们的音质更好;例如 http://boards.straightdope.com/sdmb/showthread.php?t=83441。我认为与其说是格式的问题,不如说是合成器的问题。当然高端合成器制造商如雅马哈不会同意!

MPEG-4 文件肯定比相应的 MIDI 文件要大,你需要一个大容量的磁盘来存放它们。

有很多卖 MP4 歌曲的网站。只要在谷歌上搜索一下。然而,每首歌的平均价格约为 3 美元,如果你想建立一个大的收藏,这可能会变得非常昂贵。

在撰写本文时,似乎还没有一个网站出售大量的 MPEG-4 歌曲。但是,过去有过,将来也可能有。

Karaoke 机

有许多 Karaoke 机都配有 DVD。在大多数情况下,歌曲存储为 MIDI 文件,歌曲轨道在一个 MIDI 文件中,歌词在另一个文件中。一些较新的系统将使用 WMA 文件的配乐,这使得一个轨道有声乐供应和其他没有声乐。这种系统通常包括评分机制,尽管评分的依据并不明确。最新的是基于硬盘的,通常是 MP4 文件。他们似乎没有计分系统。这些系统的供应商定期更换,即使系统本身只是重新贴牌。我拥有 Malata 和 Sonken 的系统,但它们是多年前购买的。我不相信更新的型号一定是改进的。

我拥有的两个系统表现出不同的特点。Sonken MD-388 1 可播放多种语言的歌曲,如中文、韩语、英语等。我妻子是中国人,但我看不懂汉字。有一个英文化的脚本叫做拼音,Sonken 显示了汉字和拼音,所以我也可以跟着唱。看起来像图 23-1 。

A435426_1_En_23_Fig1_HTML.jpg

图 23-1。

Screen dump of Sonken player

万利达 MDVD-6619 2 播放中文歌曲不显示拼音。但它确实显示了你应该唱的音符和你实际唱的音符。图 23-2 显示我跑调了。

A435426_1_En_23_Fig2_HTML.jpg

图 23-2。

Screen dump of Malata player

MIDI 播放器

MIDI 格式的 Karaoke 文件可以从几个站点找到,通常以.kar结尾。任何 MIDI 播放器如 TiMidity 都可以播放这样的文件。然而,它们并不总是显示与旋律同步的歌词。

查找 MIDI 文件

网上有几个网站提供 MIDI 格式的文件。

KAR 文件格式

卡拉 MIDI 文件没有正式的标准。有一种被广泛接受的行业格式,称为 MIDI Karaoke 类型 1 文件格式。

以下来自迷笛 Karaoke FAQ(http://gnese.free.fr/Projects/KaraokeTime/Fichiers/karfaq.html):

  • 什么是 MIDI 卡拉 Type 1(。KAR)文件格式?MIDI Karaoke 文件是一种标准的 MIDI 文件类型 1,它包含一个单独的轨道,其中歌曲的歌词作为文本事件输入。将一个 MIDI Karaoke 文件加载到音序器中,以检查该文件曲目的内容。第一个轨道包含文本事件,用于将文件识别为 MIDI Karaoke 文件。@KMIDI KARAOKE FILE文本事件就是为此目的而使用的。可选文本事件@V0100表示格式版本号。以@I开头的任何内容都是您想要包含在文件中的任何信息。
  • 第二个轨道包含歌曲歌词的文本元事件。第一个事件是@LENGL。它标识了歌曲的语言,在本例中是英语。接下来的几个事件从@T开始,它标识了歌曲的标题。您最多可以有三个这样的事件。第一个事件应该包含歌曲的标题。一些程序(如 Soft Karaoke)读取此事件以获取要在文件打开对话框中显示的歌曲名称。第二个事件通常包含歌曲的表演者或作者。第三个事件可以包含任何版权信息或任何其他内容。
  • 第二音轨的剩余部分包含歌曲的歌词。每个事件都是事件发生时应该唱的音节。如果文字以\开头,则表示清空屏幕,在屏幕顶部显示文字。如果文本以/开头,则表示转到下一行。
  • 重要提示:一个.kar文件中每个屏幕只能有三行,以便软 Karaoke 正确播放该文件。换句话说,一行歌词的每一行开头只能有两个正斜杠。下一行必须以反斜杠开始。

这种格式有几个缺点,列举如下:

  • 没有指定可能的语言列表,只有英语。
  • 未指定文本的编码(例如,Unicode UTF-8)。
  • 没有办法识别传送旋律的通道。

皮卡拉奥克

PyKaraoke 是用 Python 写的专用 Karaoke 播放器,使用了 Pygame、WxPython 等多种库。它会播放歌曲并显示你在歌词中的位置。一个“烟雾进入你的眼睛”( www.midikaraoke.com/cgi-bin/songdir/jump.cgi?ID=1280 )的屏幕截图看起来像图 23-3 。

A435426_1_En_23_Fig3_HTML.jpg

图 23-3。

Screen dump of PyKaraoke

PyKaraoke 播放配乐并显示歌词。它不能通过播放歌手的输入来充当合适的 Karaoke 系统。但是 PyKaraoke 使用 PulseAudio 系统,所以你可以同时播放其他节目。特别是,你可以让 PyKaraoke 在一个窗口运行,而pa-mic-2-speaker在另一个窗口运行。PulseAudio 将混合两个输出流,并一起播放两个源。当然,没有额外的工作,在这样的系统中不可能得分。

kmid【3】

是 KDE 的一名 Karaoke 歌手。它会播放歌曲并显示你在歌词中的位置。一个“烟雾进入你的眼睛”的屏幕截图看起来像图 23-4 。

A435426_1_En_23_Fig4_HTML.jpg

图 23-4。

kmid screen dump. kmid uses either TiMidity or FluidSynth as a MIDI back end.

kmid播放音轨并显示歌词。它不能通过播放歌手的输入来充当合适的 Karaoke 系统。但是kmid可以使用 PulseAudio 系统,所以可以同时播放其他节目。特别是,你可以让kmid在一个窗口中运行,而pa-mic-2-speaker在另一个窗口中运行。PulseAudio 将混合两个输出流,并一起播放两个源。当然,没有额外的工作,在这样的系统中不可能得分。

麦克风输入和混响效果

几乎所有的个人电脑和笔记本电脑都有声卡来播放音频。虽然几乎所有这些都有麦克风输入,但有些没有。例如,我的戴尔笔记本电脑没有,Raspberry Pi 没有,许多 Android 电视媒体盒也没有。

那些没有麦克风输入的电脑通常有 USB 端口。他们通常会接受 USB 声卡,如果 USB 有麦克风输入,那么这是公认的。

如果你想支持两个或更多的麦克风,那么你需要相应数量的声卡或混音设备。我见过百灵达 MX-400 MicroMix,四通道紧凑型低噪声混音器,20 美元,或者你可以在电子产品网站上找到电路图(音频混音器的谷歌电路图)。

混响是一种效果,通过添加具有不同延迟的(人工)回声,赋予声音更饱满的“身体”。百灵达还生产 MIX800 MiniMix,它可以混合两个具有混响效果的麦克风,还具有一个用于线路输入的直通端口(因此您可以播放音乐并控制麦克风)。(我没有百灵达的链接。)一个类似的单元是 UNIFY K9 混响电脑 Karaoke 混音器。

中国的 DVD 播放器通常有双麦克风输入,具有混音和混响功能。因为它们的价格只有 13 美元。诚然,对于 1000 台来说,说明混音和混响应该不会太贵。我猜他们用的是三菱 M65845AFP ( www.datasheetcatalog.org/datasheet/MitsubishiElectricCorporation/mXuuvys.pdf ),“带麦克风混音电路的数字回声”数据手册显示了许多可能的配置,适合喜欢自己构建的用户。

结论

有各种各样的 Karaoke 系统,使用 VCD 光盘或专用系统。MIDI 格式的 Karaoke 文件可以用普通的 MIDI 软件来播放,还有几个 Linux 的 Karaoke 播放器。

Footnotes 1

这个 Sonken 已经不卖了。然而,也有不同品牌下销售的类似型号。

2

较新的型号在中国销售,但目前只有非常有限的英语曲目。

3

似乎已经从当前的 KDE 版本中消失了。这是一个真正的耻辱,因为它非常好。

二十四、MP3+G

本章探讨如何使用 MP3+G 格式的 Karaoke 文件。文件从服务器下载到与显示设备(我的电视)相连的(小)电脑上。使用运行在 Linux 或 Windows 上的 Java Swing 应用选择文件。

在第二十三章中,我讨论了用于 Karaoke 的 MP3+G 格式。每首“歌曲”由两个文件组成:音频的 MP3 文件和视频的低质量 CDG 文件(主要是歌词)。通常这两个文件是压缩在一起的。

使用cdrdaocdgrip.py可以从 CDG Karaoke 光盘中提取文件。当给定 MP3 文件作为参数时,VLC 可以播放它们。它将从同一个目录中提取 CDG 文件。

许多人已经收集了相当多的 MP3+G 歌曲。在这一章中,你将考虑如何列出并播放它们,同时保存喜爱歌曲的列表。本章着眼于一个 Java 应用来执行这一点,这实际上只是标准的 Swing 编程。本章不考虑特殊的音频或 Karaoke 功能。

我把我的文件保存在服务器上。我可以在家里的其他计算机上以多种方式访问它们:Samba 共享、HTTP 下载、SSH 文件系统(sshfs)等等。一些机制不如其他机制可移植;比如sshfs不是标准的 Windows 应用,SMB/Samba 也不是标准的 Android 客户端。所以,在使用sshfs(标准 Linux 下的一个显而易见的工具)让一切工作正常后,我将应用转换成 HTTP 访问。这有它自己的皱纹。

环境如图 24-1 所示。

A435426_1_En_24_Fig1_HTML.gif

图 24-1。

Client requesting songs on an HTTP server to play on PC

Linux 和 Windows 的 Java 客户端应用如图 24-2 所示。

A435426_1_En_24_Fig2_HTML.jpg

图 24-2。

User interface on client

这显示了歌曲的主窗口,在它的右边是两个人的收藏夹窗口,Jan 和 Linda。该应用处理多种语言;显示英语、韩语和中文。

过滤器可以应用于主歌曲列表。例如,对歌手 Sting 的过滤给出了图 24-3 。

A435426_1_En_24_Fig3_HTML.jpg

图 24-3。

Songs by Sting

当点击播放时,关于选择的信息被发送到媒体播放器,目前是一个连接到我的 HiFi/TV 的 CubieBoard2。媒体计算机从 HTTP 服务器获取文件。使用 VLC 在媒体计算机上播放文件,因为它可以处理 MP3+G 文件。

文件组织

如果 MP3+G 歌曲是从 CDG Karaoke 光盘中抓取的,那么自然的组织将是把文件存储在目录中,每个目录对应一张光盘。通过按普通艺术家、音乐风格等对目录进行分组,你可以给出更多的结构。你可以假设一个目录结构,音乐文件作为叶节点。这些文件保存在 HTTP 服务器上。

我的服务器上目前有大量这样的文件。需要向客户提供关于这些文件的信息。经过一些试验后,使用 Java 的对象序列化方法创建并序列化了一个SongInformationVector。序列化文件也保存在 HTTP 服务器上。当客户机启动时,它从 HTTP 服务器获取这个文件并反序列化它。

构建这个向量意味着遍历 HTTP 服务器上的目录树,并在遍历过程中记录信息。遍历目录树的 Java 代码相当简单。如果您希望它是独立于操作系统的,这有点乏味,但是 Java 1.7 引入了一些机制来使这变得更容易。这些属于新的 I/O ( NIO.2)系统。第一类重要性是 java.nio.file.Path ,它是一个可以用来在文件系统中定位文件的对象。它通常代表一个依赖于系统的文件路径。一个表示文件位置的字符串,比如说,在 Linux 或 Windows 文件系统中,可以用下面的代码转换成一个Path对象:

Path startingDir = FileSystems.getDefault().getPath(dirString);

从给定路径遍历文件系统是通过遍历文件树并在每个点调用一个节点“访问者”来完成的。visitor 是SimpleFileVisitor<Path>的子类,只有对于叶节点,您才需要重写该方法。

public FileVisitResult visitFile(Path file, BasicFileAttributes attr)

遍历是通过以下方式完成的:

Visitor pf = new Visitor();
Files.walkFileTree(startingDir, pf);

Java 教程网站上的“遍历文件树”( http://docs.oracle.com/javase/tutorial/essential/io/walk.html )给出了对此的完整解释。您使用它将所有歌曲信息从光盘加载到SongTable.java中的歌曲路径向量中。

歌曲信息

关于每首歌曲的信息应该包括它在文件系统中的路径、艺术家的名字、歌曲的标题以及任何其他有用的信息。这个信息必须从歌曲的文件路径中提取出来。在我当前的设置中,文件如下所示:

/server/KARAOKE/Sonken/SK-50154 - Crosby, Stills - Carry On.mp3

每首歌曲都有一个合理的唯一标识符(SK-50154),一个唯一的路径,一个艺术家和标题。相当简单的模式匹配代码可以提取这些部分,如下所示:

Path file = ...
String fname = file.getFileName().toString();
if (fname.endsWith(".zip") ||
    fname.endsWith(".mp3")) {
    String root = fname.substring(0, fname.length()-4);
    String parts[] = root.split(" - ", 3);
    if (parts.length != 3)
        return;

        String index = parts[0];
        String artist = parts[1];
        String title = parts[2];

        SongInformation info = new SongInformation(file,
                                                   index,
                                                   title,
                                                   artist);

(用cdrip.py制作的图案不太一样,但是代码很容易改。)

SongInformation类捕获这些信息,并且还包含针对不同字段进行字符串模式匹配的方法。例如,要检查标题是否匹配,请使用以下命令:

public boolean titleMatch(String pattern) {
    return title.matches("(?i).*" + pattern + ".*");
}

这提供了使用 Java 正则表达式支持的不区分大小写的匹配。详见 Lars Vogel 的《Java Regex 教程》( www.vogella.com/articles/JavaRegularExpressions/article.html )。

以下是完整的SongInformation文件:

import java.nio.file.Path;
import java.io.Serializable;

public class SongInformation implements Serializable {

    // Public fields of each song record

    public String path;

    public String index;

    /**
     * song title in Unicode
     */
    public String title;

    /**
     * artist in Unicode
     */
    public String artist;

    public SongInformation(Path path,
                           String index,
                           String title,
                           String artist) {
        this.path = path.toString();
        this.index = index;
        this.title = title;
        this.artist = artist;
    }

    public String toString() {
        return "(" + index + ") " + artist + ": " + title;
    }

    public boolean titleMatch(String pattern) {
        return title.matches("(?i).*" + pattern + ".*");
    }

    public boolean artistMatch(String pattern) {
        return artist.matches("(?i).*" + pattern + ".*");
    }

    public boolean numberMatch(String pattern) {
        return index.equals(pattern);
    }
}

歌曲表

SongTable通过遍历文件树构建了一个SongInformation对象的向量。

如果有很多歌曲(比如说,数以千计),这会导致启动时间很慢。为了减少这种情况,一旦加载了一个表,就通过将它写入一个ObjectOutputStream来将它作为一个持久对象保存到磁盘上。下次程序启动时,会尝试使用ObjectInputStream从这里读回它。注意,您没有使用 Java 持久性 API ( http://en.wikibooks.org/wiki/Java_Persistence/What_is_Java_persistence%3F )。它是为 J2EE 设计的,对我们来说太重了。

SongTable还包括基于模式和标题(或艺术家或编号)之间的匹配来构建更小的歌曲表的代码。它可以搜索模式和歌曲之间的匹配,并基于匹配建立新的表。它包含一个指向原始表的指针,以便以后恢复。这允许模式搜索使用相同的数据结构。

SongTable的代码如下:

import java.util.Vector;
import java.io.FileInputStream;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.FileSystems;
import java.nio.file.attribute.*;

class Visitor
    extends SimpleFileVisitor<Path> {

    private Vector<SongInformation> songs;

    public Visitor(Vector<SongInformation> songs) {
        this.songs = songs;
    }

    @Override
    public FileVisitResult visitFile(Path file,
                                   BasicFileAttributes attr) {
        if (attr.isRegularFile()) {
            String fname = file.getFileName().toString();
            //System.out.println("Regular file " + fname);
            if (fname.endsWith(".zip") ||
                fname.endsWith(".mp3") ||
                fname.endsWith(".kar")) {
                String root = fname.substring(0, fname.length()-4);
                //System.err.println(" root " + root);
                String parts[] = root.split(" - ", 3);
                if (parts.length != 3)
                    return java.nio.file.FileVisitResult.CONTINUE;

                String index = parts[0];
                String artist = parts[1];
                String title = parts[2];

                SongInformation info = new SongInformation(file,
                                                           index,
                                                           title,
                                                           artist);
                songs.add(info);
            }
        }

        return java.nio.file.FileVisitResult.CONTINUE;
    }
}

public class SongTable {

    private static final String SONG_INFO_ROOT = "/server/KARAOKE/KARAOKE/";

    private static Vector<SongInformation> allSongs;

    public Vector<SongInformation> songs =
        new Vector<SongInformation>  ();

    public static long[] langCount = new long[0x23];

    public SongTable(Vector<SongInformation> songs) {
        this.songs = songs;
    }

    public SongTable(String[] args) throws java.io.IOException,
                                           java.io.FileNotFoundException {
        if (args.length >= 1) {
            System.err.println("Loading from " + args[0]);
            loadTableFromSource(args[0]);
            saveTableToStore();
        } else {
            loadTableFromStore();
        }
    }

    private boolean loadTableFromStore() {
        try {

            File storeFile = new File("/server/KARAOKE/SongStore");

            FileInputStream in = new FileInputStream(storeFile);
            ObjectInputStream is = new ObjectInputStream(in);
            songs = (Vector<SongInformation>) is.readObject();
            in.close();
        } catch(Exception e) {
            System.err.println("Can't load store file " + e.toString());
            return false;
        }
        return true;
    }

    private void saveTableToStore() {
        try {
            File storeFile = new File("/server/KARAOKE/SongStore");
            FileOutputStream out = new FileOutputStream(storeFile);
            ObjectOutputStream os = new ObjectOutputStream(out);
            os.writeObject(songs);
            os.flush();
            out.close();
        } catch(Exception e) {
            System.err.println("Can't save store file " + e.toString());
        }
    }

    private void loadTableFromSource(String dir) throws java.io.IOException,
                              java.io.FileNotFoundException {

        Path startingDir = FileSystems.getDefault().getPath(dir);
        Visitor pf = new Visitor(songs);
        Files.walkFileTree(startingDir, pf);
    }

    public java.util.Iterator<SongInformation> iterator() {
        return songs.iterator();
    }

    public SongTable titleMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.titleMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

     public SongTable artistMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.artistMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

    public SongTable numberMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.numberMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

    public String toString() {
        StringBuffer buf = new StringBuffer();
        for (SongInformation song: songs) {
            buf.append(song.toString() + "\n");
        }
        return buf.toString();
    }

    public static void main(String[] args) {
        // for testing
        SongTable songs = null;
        try {
            songs = new SongTable(new String[] {SONG_INFO_ROOT});
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }

        System.out.println(songs.artistMatches("Tom Jones").toString());

        System.exit(0);
    }
}

收藏夹

我已经为我的家庭环境系统建立了这个系统,并且我有一群定期来访的朋友。我们每个人都有自己喜欢的歌曲要唱,所以我们在纸片上列出了丢失的歌曲、洒了酒的歌曲等等。所以,这个系统包括了一个最喜欢的歌曲列表。

每个收藏夹列表本质上都是另一个SongTable。但是我在桌子周围放了一个JList来显示它。JList使用了一个DefaultListModel,构造函数通过遍历这个表并添加元素将一个歌曲表加载到这个列表中。

        int n = 0;
        java.util.Iterator<SongInformation> iter = favouriteSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }

其他 Swing 代码在底部添加了三个按钮:

  • 将歌曲添加到列表
  • 从列表中删除歌曲
  • 播放音乐

将一首歌曲添加到列表意味着从主歌曲表中取出所选项目,并将其添加到该表中。主表被传递到构造函数中,只是为了获取它的选择而保留。所选对象被添加到 Swing JList和 favorites SongTable中。

播放一首歌曲的方法很简单:歌曲的完整路径被写入标准输出,换行结束。然后,管道中的另一个程序可以拾取它;这将在本章的后面介绍。

最喜欢的东西如果不能从一天坚持到下一天就没什么用了!因此,与之前相同的对象存储方法被用于完整的歌曲表。每次对服务器进行更改时,都会保存每个收藏夹文件。

以下是Favourites的代码:

import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.util.regex.*;
import java.io.*;
import java.nio.file.FileSystems;
import java.nio.file.*;

public class Favourites extends JPanel {
    private DefaultListModel model = new DefaultListModel();
    private JList list;

    // whose favoutites these are
    private String user;

    // songs in this favourites list
    private final SongTable favouriteSongs;

    // pointer back to main song table list
    private final SongTableSwing songTable;

    // This font displays Asian and European characters.
    // It should be in your distro.
    // Fonts displaying all Unicode are zysong.ttf and Cyberbit.ttf
    // See http://unicode.org/resources/fonts.html
    private Font font = new Font("WenQuanYi Zen Hei", Font.PLAIN, 16);

    private int findIndex = -1;

    public Favourites(final SongTableSwing songTable,
                      final SongTable favouriteSongs,
                      String user) {
        this.songTable = songTable;
        this.favouriteSongs = favouriteSongs;
        this.user = user;

        if (font == null) {
            System.err.println("Can't find font");
        }

        int n = 0;
        java.util.Iterator<SongInformation> iter = favouriteSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }

        BorderLayout mgr = new BorderLayout();

        list = new JList(model);
        list.setFont(font);
        JScrollPane scrollPane = new JScrollPane(list);

        setLayout(mgr);
        add(scrollPane, BorderLayout.CENTER);

        JPanel bottomPanel = new JPanel();
        bottomPanel.setLayout(new GridLayout(2, 1));
        add(bottomPanel, BorderLayout.SOUTH);

        JPanel searchPanel = new JPanel();
        bottomPanel.add(searchPanel);
        searchPanel.setLayout(new FlowLayout());

        JPanel buttonPanel = new JPanel();
        bottomPanel.add(buttonPanel);
        buttonPanel.setLayout(new FlowLayout());

        JButton addSong = new JButton("Add song to list");
        JButton deleteSong = new JButton("Delete song from list");
        JButton play = new JButton("Play");

        buttonPanel.add(addSong);
        buttonPanel.add(deleteSong);
        buttonPanel.add(play);

        play.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    playSong();
                }
            });

        deleteSong.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    SongInformation song = (SongInformation) list.getSelectedValue();
                    model.removeElement(song);
                    favouriteSongs.songs.remove(song);
                    saveToStore();
                }
            });

        addSong.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    SongInformation song = songTable.getSelection();
                    model.addElement(song);
                    favouriteSongs.songs.add(song);
                    saveToStore();
                }
            });
     }

    private void saveToStore() {
        try {
            File storeFile = new File("/server/KARAOKE/favourites/" + user);
            FileOutputStream out = new FileOutputStream(storeFile);
            ObjectOutputStream os = new ObjectOutputStream(out);
            os.writeObject(favouriteSongs.songs);
            os.flush();
            out.close();
        } catch(Exception e) {
            System.err.println("Can't save favourites file " + e.toString());
        }
    }

    /**
     * "play" a song by printing its file path to standard out.
     * Can be used in a pipeline this way
     */
    public void playSong() {
        SongInformation song = (SongInformation) list.getSelectedValue();
        if (song == null) {
            return;
        }
        System.out.println(song.path.toString());
    }

    class SongInformationRenderer extends JLabel implements ListCellRenderer {

        public Component getListCellRendererComponent(
                                                      JList list,
                                                      Object value,
                                                      int index,
                                                      boolean isSelected,
                                                      boolean cellHasFocus) {
            setText(value.toString());
            return this;
        }
    }
}

所有收藏夹

这里没什么特别的。它只是加载每个人的表,并构建一个放在JTabbedPane中的Favourites对象。它还添加了一个用于添加更多用户的新选项卡。

AllFavourites的代码如下:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Vector;
import java.nio.file.*;
import java.io.*;

public class AllFavourites extends JTabbedPane {
    private SongTableSwing songTable;

    public AllFavourites(SongTableSwing songTable) {
        this.songTable = songTable;

        loadFavourites();

        NewPanel newP = new NewPanel(this);
        addTab("NEW", null, newP);
    }

    private void loadFavourites() {
        String userHome = System.getProperty("user.home");
        Path favouritesPath = FileSystems.getDefault().getPath("/server/KARAOKE/favourites");
        try {
            DirectoryStream<Path> stream =
                Files.newDirectoryStream(favouritesPath);
            for (Path entry: stream) {
                int nelmts = entry.getNameCount();
                Path last = entry.subpath(nelmts-1, nelmts);
                System.err.println("Favourite: " + last.toString());
                File storeFile = entry.toFile();

                FileInputStream in = new FileInputStream(storeFile);
                ObjectInputStream is = new ObjectInputStream(in);
                Vector<SongInformation> favouriteSongs =
                    (Vector<SongInformation>) is.readObject();
                in.close();
                for (SongInformation s: favouriteSongs) {
                    System.err.println("Fav: " + s.toString());
                }

                SongTable favouriteSongsTable = new SongTable(favouriteSongs);
                Favourites f = new Favourites(songTable,
                                              favouriteSongsTable,
                                              last.toString());
                addTab(last.toString(), null, f, last.toString());
                System.err.println("Loaded favs " + last.toString());
            }
        } catch(Exception e) {
            System.err.println(e.toString());
        }
    }

    class NewPanel extends JPanel {
        private JTabbedPane pane;

        public NewPanel(final JTabbedPane pane) {
            this.pane = pane;

            setLayout(new FlowLayout());
            JLabel nameLabel = new JLabel("Name of new person");
            final JTextField nameField = new JTextField(10);
            add(nameLabel);
            add(nameField);

            nameField.addActionListener(new ActionListener(){
                    public void actionPerformed(ActionEvent e){
                        String name = nameField.getText();

                        SongTable songs = new SongTable(new Vector<SongInformation>());
                        Favourites favs = new Favourites(songTable, songs, name);

                        pane.addTab(name, null, favs);
                    }});

        }
    }
}

摇摆歌单

这主要是加载不同歌曲表和构建 Swing 界面的代码。它还根据匹配的模式过滤显示表。最初加载的表被保留用于恢复和修补匹配。SongTableSwing的代码如下:

import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.util.regex.*;
import java.io.*;

public class SongTableSwing extends JPanel {
   private DefaultListModel model = new DefaultListModel();
    private JList list;
    private static SongTable allSongs;

    private JTextField numberField;
    private JTextField langField;
    private JTextField titleField;
    private JTextField artistField;

    // This font displays Asian and European characters.
    // It should be in your distro.
    // Fonts displaying all Unicode are zysong.ttf and Cyberbit.ttf
    // See http://unicode.org/resources/fonts.html
    private Font font = new Font("WenQuanYi Zen Hei", Font.PLAIN, 16);
    // font = new Font("Bitstream Cyberbit", Font.PLAIN, 16);

    private int findIndex = -1;

    /**
     * Describe <code>main</code> method here.
     *
     * @param args a <code>String</code> value
     */
    public static final void main(final String[] args) {
        if (args.length >= 1 &&
            args[0].startsWith("-h")) {
            System.err.println("Usage: java SongTableSwing [song directory]");
            System.exit(0);
        }

        allSongs = null;
        try {
            allSongs = new SongTable(args);
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }

        JFrame frame = new JFrame();
        frame.setTitle("Song Table");
        frame.setSize(700, 800);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        SongTableSwing panel = new SongTableSwing(allSongs);
        frame.getContentPane().add(panel);

        frame.setVisible(true);

        JFrame favourites = new JFrame();
        favourites.setTitle("Favourites");
        favourites.setSize(600, 800);
        favourites.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        AllFavourites lists = new AllFavourites(panel);
        favourites.getContentPane().add(lists);

        favourites.setVisible(true);

    }

    public SongTableSwing(SongTable songs) {

        if (font == null) {
            System.err.println("Can't fnd font");
        }

        int n = 0;
        java.util.Iterator<SongInformation> iter = songs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
            // model.add(n++, iter.next().toString());
        }

        BorderLayout mgr = new BorderLayout();

        list = new JList(model);
        // list = new JList(songs);
        list.setFont(font);
        JScrollPane scrollPane = new JScrollPane(list);

        // Support DnD
        list.setDragEnabled(true);

        setLayout(mgr);
        add(scrollPane, BorderLayout.CENTER);

        JPanel bottomPanel = new JPanel();
        bottomPanel.setLayout(new GridLayout(2, 1));
        add(bottomPanel, BorderLayout.SOUTH);

        JPanel searchPanel = new JPanel();
        bottomPanel.add(searchPanel);
        searchPanel.setLayout(new FlowLayout());

        JLabel numberLabel = new JLabel("Number");
        numberField = new JTextField(5);

        JLabel langLabel = new JLabel("Language");
        langField = new JTextField(8);

        JLabel titleLabel = new JLabel("Title");
        titleField = new JTextField(20);
        titleField.setFont(font);

        JLabel artistLabel = new JLabel("Artist");
        artistField = new JTextField(10);
        artistField.setFont(font);

        searchPanel.add(numberLabel);
        searchPanel.add(numberField);
        searchPanel.add(titleLabel);
        searchPanel.add(titleField);
        searchPanel.add(artistLabel);
        searchPanel.add(artistField);

        titleField.getDocument().addDocumentListener(new DocumentListener() {
                public void changedUpdate(DocumentEvent e) {
                    // rest find to -1 to restart any find searches
                    findIndex = -1;
                    // System.out.println("reset find index");
                }
                public void insertUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void removeUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset remove find index");
                }
            }
            );
        artistField.getDocument().addDocumentListener(new DocumentListener() {
                public void changedUpdate(DocumentEvent e) {
                    // rest find to -1 to restart any find searches
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void insertUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void removeUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
            }
            );

        titleField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});
        artistField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});

        JPanel buttonPanel = new JPanel();
        bottomPanel.add(buttonPanel);
        buttonPanel.setLayout(new FlowLayout());

        JButton find = new JButton("Find");
        JButton filter = new JButton("Filter");
        JButton reset = new JButton("Reset");
        JButton play = new JButton("Play");
        buttonPanel.add(find);
        buttonPanel.add(filter);
        buttonPanel.add(reset);
        buttonPanel.add(play);

        find.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    findSong();
                }
            });

        filter.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    filterSongs();
                }
            });

        reset.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    resetSongs();
                }
            });

        play.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    playSong();
                }
            });

     }

    public void findSong() {
        String number = numberField.getText();
        String language = langField.getText();
        String title = titleField.getText();
        String artist = artistField.getText();

        if (number.length() != 0) {
            return;
        }

        for (int n = findIndex + 1; n < model.getSize(); n++) {
            SongInformation info = (SongInformation) model.getElementAt(n);

            if ((title.length() != 0) && (artist.length() != 0)) {
                if (info.titleMatch(title) && info.artistMatch(artist)) {
                        findIndex = n;
                        list.setSelectedIndex(n);
                        list.ensureIndexIsVisible(n);
                        break;
                }
            } else {
                if ((title.length() != 0) && info.titleMatch(title)) {
                    findIndex = n;
                    list.setSelectedIndex(n);
                    list.ensureIndexIsVisible(n);
                    break;
                } else if ((artist.length() != 0) && info.artistMatch(artist)) {
                    findIndex = n;
                    list.setSelectedIndex(n);
                    list.ensureIndexIsVisible(n);
                    break;

                }
            }

        }
    }

    public void filterSongs() {
        String title = titleField.getText();
        String artist = artistField.getText();
        String number = numberField.getText();
        SongTable filteredSongs = allSongs;

        if (allSongs == null) {
            return;
        }

        if (title.length() != 0) {
            filteredSongs = filteredSongs.titleMatches(title);
        }
        if (artist.length() != 0) {
            filteredSongs = filteredSongs.artistMatches(artist);
        }
        if (number.length() != 0) {
            filteredSongs = filteredSongs.numberMatches(number);
        }

        model.clear();
        int n = 0;
        java.util.Iterator<SongInformation> iter = filteredSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }
    }

    public void resetSongs() {
        artistField.setText("");
        titleField.setText("");
        model.clear();
        int n = 0;
        java.util.Iterator<SongInformation> iter = allSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }
    }
    /**
     * "play" a song by printing its file path to standard out.
     * Can be used in a pipeline this way
     */
    public void playSong() {
        SongInformation song = (SongInformation) list.getSelectedValue();
        if (song == null) {
            return;
        }
        System.out.println(song.path);
    }

    public SongInformation getSelection() {
        return (SongInformation) (list.getSelectedValue());
    }

    class SongInformationRenderer extends JLabel implements ListCellRenderer {

        public Component getListCellRendererComponent(
                                                      JList list,
                                                      Object value,
                                                      int index,
                                                      boolean isSelected,
                                                      boolean cellHasFocus) {
            setText(value.toString());
            return this;
        }
    }
}

播放歌曲

每当“播放”一首歌曲时,它的文件路径都被写入标准输出。这使得它适用于 bash shell 管道,如下所示:

#!/bin/bash

VLC_OPTS="--play-and-exit --fullscreen"

java  SongTableSwing |
while read line
do
        if expr match "$line" ".*mp3"
        then
                vlc $VLC_OPTS "$line"
        elif expr match "$line" ".*zip"
        then
                rm -f /tmp/karaoke/*
                unzip -d /tmp/karaoke "$line"
                vlc $VLC_OPTS /tmp/karaoke/*.mp3
        fi
done

可见光通讯

VLC 是一个非常灵活的媒体播放器。它依靠大量的插件来增强其基本的核心功能。您在前面的章节中看到,如果一个目录包含一个 MP3 文件和一个具有相同基本名称的 CDG 文件,那么通过让它播放 MP3 文件,它也将显示 CDG 视频。

Karaoke 玩家的普遍期望是,你可以调整速度和音高。目前 VLC 不能调整音高,但它有一个插件来调整速度(同时保持音高不变)。这个插件可以通过 VLC 的 Lua 接口访问。设置完成后,您可以从启动 VLC 的进程(如命令行 shell)通过标准输入发送如下命令:

rate 1.04

这将改变速度,保持音高不变。

设置 VLC 接受来自stdin的 Lua 命令可通过以下命令选项完成:

vlc -I luaintf --lua-intf cli ...

注意,这去掉了标准的 GUI 控件(菜单等等),只从stdin开始控制 VLC。

目前,给 VLC 增加俯仰控制并不简单。深呼吸。

  • 关闭 PulseAudio,启动 Jack。
  • 运行jack-rack并安装TAP_pitch过滤器。
  • 用 Jack 输出运行 VLC。
  • 使用qjackctl,通过jack-rack钩住 VLC 输出,输出到系统。
  • 通过jack-rack图形用户界面控制俯仰。

通过网络播放歌曲

实际上,我想把服务器磁盘上的歌曲播放到与电视相连的 Raspberry Pi 或 CubieBoard 上,并从我腿上的上网本上控制播放。这是一个分布式系统。

在计算机上安装服务器文件很简单:可以使用 NFS、Samba 等等。我目前使用的sshfs如下:

sshfs -o idmap=user -o rw -o allow_other newmarch@192.168.1.101:/home/httpd/html /server

对于远程访问/控制,我用 TCP 客户机/服务器替换了上一节的run命令。在客户端,控制播放器,我有这个:

java SongTableSwing | client 192.168.1.7

在(Raspberry Pi/CubieBoard)服务器上,我运行以下代码:

#!/bin/bash
set -x
VLC_OPTS="--play-and-exit -f"

server |
while read line
do
        if expr match "$line" ".*mp3"
        then
                vlc $VLC_OPTS "$line"
        elif expr match "$line" ".*zip"
        then
                rm -f /tmp/karaoke/*
                unzip -d /tmp/karaoke "$line"
                vlc $VLC_OPTS /tmp/karaoke/*.mp3
        fi
done

客户机/服务器文件只是标准的 TCP 文件。客户端从标准输入中读取换行符结束的字符串,并将其写入服务器,服务器将同一行打印到标准输出中。这里是client.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>

#define SIZE 1024
char buf[SIZE];
#define PORT 13000
int main(int argc, char *argv[]) {
    int sockfd;
    int nread;
    struct sockaddr_in serv_addr;
    if (argc != 2) {
        fprintf(stderr, "usage: %s IPaddr\n", argv[0]);
        exit(1);
    }

    while (fgets(buf, SIZE , stdin) != NULL) {
        /* create endpoint */
        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
            perror(NULL); exit(2);
        }
        /* connect to server */
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
        serv_addr.sin_port = htons(PORT);

        while (connect(sockfd,
                       (struct sockaddr *) &serv_addr,
                       sizeof(serv_addr)) < 0) {
            /* allow for timesouts etc */
            perror(NULL);
            sleep(1);
        }

        printf("%s", buf);
        nread = strlen(buf);
        /* transfer data and quit */
        write(sockfd, buf, nread);
        close(sockfd);
    }
}

这里是server.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <signal.h>

#define SIZE 1024
char buf[SIZE];
#define TIME_PORT 13000

int sockfd, client_sockfd;

void intHandler(int dummy) {
    close(client_sockfd);
    close(sockfd);
    exit(1);
}

int main(int argc, char *argv[]) {
    int sockfd, client_sockfd;
    int nread, len;
    struct sockaddr_in serv_addr, client_addr;
    time_t t;

    signal(SIGINT, intHandler);

    /* create endpoint */
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror(NULL); exit(2);
    }
    /* bind address */
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(TIME_PORT);
    if (bind(sockfd,
             (struct sockaddr *) &serv_addr,
             sizeof(serv_addr)) < 0) {
        perror(NULL); exit(3);
    }
    /* specify queue */
    listen(sockfd, 5);
    for (;;) {
        len = sizeof(client_addr);
        client_sockfd = accept(sockfd,
                               (struct sockaddr *) &client_addr,
                               &len);
        if (client_sockfd == -1) {
            perror(NULL); continue;
        }
        while ((nread = read(client_sockfd, buf, SIZE-1)) > 0) {
            buf[nread] = '\0';
            fputs(buf, stdout);
            fflush(stdout);
        }
        close(client_sockfd);
    }
}

结论

本章展示了如何为 MP3+G 文件构建播放器。

二十五、使用 Java 声音的 Karaoke 应用

Java 对 Karaoke 没有库支持。这太具体应用了。在这一章中,我给你一个可以播放 KAR 文件的 Karaoke 播放器的代码。播放器将显示两行要播放的歌词,已经播放的单词用红色突出显示。在顶部,它显示了一个简单的钢琴键盘,其音符在 MIDI 文件的通道 1 中播放。中间显示旋律线,中间竖线显示当前播放的音符。

玩家长得像图 25-1 。

A435426_1_En_25_Fig1_HTML.jpg

图 25-1。

User interface of karaoke player

图 25-2 显示了 UML 图。

A435426_1_En_25_Fig2_HTML.gif

图 25-2。

Class diagram of karaoke player

资源

以下是一些资源:

Karaoke 播放器

KaraokePlayer类提取 Karaoke 文件的文件名,并创建一个MidiPlayer来处理该文件。

/*
 * KaraokePlayer.java
 *
 */

import javax.swing.*;

public class KaraokePlayer {

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("KaraokePlayer: usage: " +
                             "KaraokePlayer <midifile>");
            System.exit(1);
        }
        String  strFilename = args[0];

        MidiPlayer midiPlayer = new MidiPlayer();
        midiPlayer.playMidiFile(strFilename);
    }
}

媒体播放机

MidiPlayer类从文件中创建一个Sequence。很多地方都需要序列信息,所以不是通过参数传递序列,而是存储在一个单独的(静态)对象中,一个SequenceInformation。这使得序列有效地成为系统的全局对象。

然后,播放器获得默认的音序器,并将 MIDI 事件传输到两个接收器对象:播放事件的默认合成器和管理所有 GUI 处理的DisplayReceiverSequencer方法getTransmitter()名不副实:每次调用它时,它都返回一个新的发送器,每次都向各自的接收器播放相同的事件。

以下摘自 Java SE 文档,具体来说,第十章,“发送和接收 MIDI 消息”( http://docs.oracle.com/javase/7/docs/technotes/guides/sound/programmer_guide/chapter10.html ):

  • 这段代码(在他们的例子中)引入了对MidiDevice.getTransmitter方法的双重调用,将结果分配给inPortTrans1inPortTrans2。如前所述,一个设备可以拥有多个发射机和接收机。每次为给定设备调用MidiDevice.getTransmitter()时,都会返回另一个发送器,直到没有更多的发送器可用为止,此时会抛出一个异常。

这样,序列器可以发送给两个接收器。

接收者没有得到MetaMessages。这些包含文本或歌词事件等信息。DisplayReceiver被注册为MetaEventListener,这样它就可以管理这些事件以及其他事件。

MidiPlayer如下所示:

import javax.sound.midi.MidiSystem;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Transmitter;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.SysexMessage;

import java.io.File;
import java.io.IOException;

public class MidiPlayer {

    private DisplayReceiver receiver;

    public  void playMidiFile(String strFilename) throws Exception {
        File    midiFile = new File(strFilename);

        /*
         *      We try to get a Sequence object, loaded with the content
         *      of the MIDI file.
         */
        Sequence        sequence = null;
        try {
            sequence = MidiSystem.getSequence(midiFile);
        }
        catch (InvalidMidiDataException e) {
            e.printStackTrace();
            System.exit(1);
        }
        catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        if (sequence == null) {
            out("Cannot retrieve Sequence.");
        } else {
            SequenceInformation.setSequence(sequence);
            playMidi(sequence);
        }
    }

    public  void playMidi(Sequence sequence) throws Exception {

        Sequencer sequencer = MidiSystem.getSequencer(true);
        sequencer.open();
        sequencer.setSequence(sequence);

        receiver = new DisplayReceiver(sequencer);
        sequencer.getTransmitter().setReceiver(receiver);
        sequencer.addMetaEventListener(receiver);

        if (sequencer instance of Synthesizer) {
            Debug.println("Sequencer is also a synthesizer");
        } else {
            Debug.println("Sequencer is not a synthesizer");
        }
        //sequencer.start();

        /*
        Synthesizer synthesizer = MidiSystem.getSynthesizer();
        synthesizer.open();

        if (synthesizer.getDefaultSoundbank() == null) {
            // then you know that java sound is using the hardware soundbank
            Debug.println("Synthesizer using h/w soundbank");
        } else Debug.println("Synthesizer using s/w soundbank");

        Receiver synthReceiver = synthesizer.getReceiver();
        Transmitter seqTransmitter = sequencer.getTransmitter();
        seqTransmitter.setReceiver(synthReceiver);
        MidiChannel[] channels = synthesizer.getChannels();
        Debug.println("Num channels is " + channels.length);
        */
        sequencer.start();

        /* default synth doesn't support pitch bending
        Synthesizer synthesizer = MidiSystem.getSynthesizer();
        MidiChannel[] channels = synthesizer.getChannels();
        for (int i = 0; i < channels.length; i++) {
            System.out.printf("Channel %d has bend %d\n", i, channels[i].getPitchBend());
            channels[i].setPitchBend(16000);
            System.out.printf("Channel %d now has bend %d\n", i, channels[i].getPitchBend());
        }
        */

        /* set volume - doesn't work */
        /*KaraokeUML
        for (int i = 0; i < channels.length; i++) {
            channels[i].controlChange(7, 0);
        }
        */
        /*
        System.out.println("Turning notes off");
        for (int i = 0; i < channels.length; i++) {
            channels[i].allNotesOff();
            channels[i].allSoundOff();
        }
        */

        /* set volume - doesn't work either */
        /*
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        if (synthReceiver == MidiSystem.getReceiver())
            System.out.println("Reciver is default");
        else
            System.out.println("Reciver is not default");
        System.out.println("Receiver is " + synthReceiver.toString());
        //synthReceiver = MidiSystem.getReceiver();
        System.out.println("Receiver is now " + synthReceiver.toString());
        ShortMessage volMessage = new ShortMessage();
        int midiVolume = 1;
        for (Receiver rec: synthesizer.getReceivers()) {
            System.out.println("Setting vol on recveiver " + rec.toString());
        for (int i = 0; i < channels.length; i++) {
            try {
                // volMessage.setMessage(ShortMessage.CONTROL_CHANGE, i, 123, midiVolume);
                volMessage.setMessage(ShortMessage.CONTROL_CHANGE, i, 7, midiVolume);
            } catch (InvalidMidiDataException e) {
                e.printStackTrace();
}
            synthReceiver.send(volMessage, -1);
            rec.send(volMessage, -1);
        }
        }
        System.out.println("Changed midi volume");
        */
        /* master volume control using sysex */
        /* http://www.blitter.com/∼russtopia/MIDI/∼jglatt/tech/midispec/mastrvol.htm */
        /*
        SysexMessage sysexMessage = new SysexMessage();
        /* volume values from http://www.bandtrax.com.au/sysex.htm */
        /* default volume 0x7F * 128 + 0x7F from */
        /*
        byte[] data = {(byte) 0xF0, (byte) 0x7F, (byte) 0x7F, (byte) 0x04,
                       (byte) 0x01, (byte) 0x0, (byte) 0x7F, (byte) 0xF7};
        sysexMessage.setMessage(data, data.length);
        synthReceiver.send(sysexMessage, -1);
        for (Receiver rec: synthesizer.getReceivers()) {
            System.out.println("Setting vol on recveiver " + rec.toString());
            rec.send(sysexMessage, -1);
        }
        */
     }

    public DisplayReceiver getReceiver() {
        return receiver;
    }

    private static void out(String strMessage)
    {
        System.out.println(strMessage);
    }
}

显示接收器

DisplayReceiver收集了作为ReceiverShortMessages和作为MetaEventListenerMetaMessages。看音符和歌词都需要这些。

DisplayReceiver解码发送给它的笔记和文本。反过来,它将这些传递给一个MidiGUI来显示它们。这个类如下:

/**
 * DisplayReceiver
 *
 * Acts as a Midi receiver to the default Java Midi sequencer.
 * It collects Midi events and Midi meta messages from the sequencer.
 * these are handed to a UI object for display.
 *
 * The current UI object is a MidiGUI but could be replaced.
 */

import javax.sound.midi.*;
import javax.swing.SwingUtilities;

public class DisplayReceiver implements Receiver,
                                        MetaEventListener {
    private MidiGUI gui;KaraokeUML
    private Sequencer sequencer;
    private int melodyChannel = SequenceInformation.getMelodyChannel();

    public DisplayReceiver(Sequencer sequencer) {
        this.sequencer = sequencer;
        gui = new MidiGUI(sequencer);
    }

    public void close() {
    }

    /**
     * Called by a Transmitter to receive events
     * as a Receiver
     */
    public void send(MidiMessage msg, long timeStamp) {
        // Note on/off messages come from the midi player
        // but not meta messages

        if (msg instanceof ShortMessage) {
            ShortMessage smsg = (ShortMessage) msg;

            String strMessage = "Channel " + smsg.getChannel() + " ";

            switch (smsg.getCommand())
                {
                case Constants.MIDI_NOTE_OFF:
                    strMessage += "note Off " +
                        getKeyName(smsg.getData1()) + " " + timeStamp;
                    break;

                case Constants.MIDI_NOTE_ON:
                    strMessage += "note On " +
                        getKeyName(smsg.getData1()) + " " + timeStamp;
                    break;
                }
            Debug.println(strMessage);
            if (smsg.getChannel() == melodyChannel) {
                gui.setNote(timeStamp, smsg.getCommand(), smsg.getData1());
            }

        }
    }

    public void meta(MetaMessage msg) {
        Debug.println("Reciever got a meta message");
        if (((MetaMessage) msg).getType() == Constants.MIDI_TEXT_TYPE) {
            setLyric((MetaMessage) msg);
        } else if (((MetaMessage) msg).getType() == Constants.MIDI_END_OF_TRACK)  {
            System.exit(0);
        }
    }

    public void setLyric(MetaMessage message) {
        byte[] data = message.getData();
        String str = new String(data);
        Debug.println("Lyric +\"" + str + "\" at " + sequencer.getTickPosition());
        gui.setLyric(str);

    }

    private static String[] keyNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};

    public static String getKeyName(int keyNumber) {
        if (keyNumber > 127) {
            return "illegal value";
        } else {
            int note = keyNumber % 12;
            int octave = keyNumber / 12;
            return keyNames[note] + (octave - 1);
        }
    }

}

图形系统

用两种方法调用MidiGUI:setLyric()setNote()。GUI 由三个主要区域组成:一个区域在旋律播放时提供“钢琴”视图(pianoPanel),一个区域显示完整的旋律音符(melodyPanel),一组Panel显示歌词。setNote()相当简单,因为它只调用了pianoPanel. setLyric()中的drawNote(),而pianoPanel. setLyric()要复杂得多。

大多数 Karaoke 播放器会显示几行歌词。随着歌词的播放,文本通常会改变颜色以与之匹配。到了一行末尾,焦点会切换到下一行,上一行会被另一行歌词替换。

每行必须容纳一行歌词。该线必须能够对播放的歌词做出反应。这由稍后显示的AttributedTextPanel处理。主要任务是将歌词中的变化传递给所选面板,以便它可以用正确的颜色显示它们。

这里MidiGUI的另一个主要任务是当检测到行尾时在AttributedTextPanel之间切换焦点,并更新下一行文本。新的文本行不能来自播放的歌词,而是必须从包含所有音符和歌词的序列中构建。便利类SequenceInformation(稍后显示)接受一个Sequence对象,并有一个方法提取一组LyricLine对象。显示一条线的每个面板都被赋予该数组中的一条线。

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.sound.midi.*;
import java.util.Vector;
import java.util.Map;
import java.io.*;

public class MidiGUI extends JFrame {
    //private GridLayout mgr = new GridLayout(3,1);
    private BorderLayout mgr = new BorderLayout();

    private PianoPanel pianoPanel;
    private MelodyPanel melodyPanel;

    private AttributedLyricPanel lyric1;
    private AttributedLyricPanel lyric2;
    private AttributedLyricPanel[] lyricLinePanels;
    private int whichLyricPanel = 0;

    private JPanel lyricsPanel = new JPanel();

    private Sequencer sequencer;
    private Sequence sequence;
    private Vector<LyricLine> lyricLines;

    private int lyricLine = -1;

    private boolean inLyricHeader = true;
    private Vector<DurationNote> melodyNotes;

    private Map<Character, String> pinyinMap;

    private int language;

    public MidiGUI(final Sequencer sequencer) {
        this.sequencer = sequencer;
        sequence = sequencer.getSequence();

        // get lyrics and notes from Sequence Info
        lyricLines = SequenceInformation.getLyrics();
        melodyNotes = SequenceInformation.getMelodyNotes();
        language = SequenceInformation.getLanguage();

        pianoPanel = new PianoPanel(sequencer);
        melodyPanel = new MelodyPanel(sequencer);

        pinyinMap = CharsetEncoding.loadPinyinMap();
        lyric1 = new AttributedLyricPanel(pinyinMap);
        lyric2 = new AttributedLyricPanel(pinyinMap);
        lyricLinePanels = new AttributedLyricPanel[] {
            lyric1, lyric2};

        Debug.println("Lyrics ");

        for (LyricLine line: lyricLines) {
            Debug.println(line.line + " " + line.startTick + " " + line.endTick +
                          " num notes " + line.notes.size());
        }

        getContentPane().setLayout(mgr);
        /*
        getContentPane().add(pianoPanel);
        getContentPane().add(melodyPanel);

        getContentPane().add(lyricsPanel);
        */
        getContentPane().add(pianoPanel, BorderLayout.PAGE_START);
        getContentPane().add(melodyPanel,  BorderLayout.CENTER);

        getContentPane().add(lyricsPanel,  BorderLayout.PAGE_END);

        lyricsPanel.setLayout(new GridLayout(2, 1));
        lyricsPanel.add(lyric1);
        lyricsPanel.add(lyric2);
        setLanguage(language);

        setText(lyricLinePanels[whichLyricPanel], lyricLines.elementAt(0).line);

        Debug.println("First lyric line: " + lyricLines.elementAt(0).line);
        if (lyricLine < lyricLines.size() - 1) {
            setText(lyricLinePanels[(whichLyricPanel+1) % 2], lyricLines.elementAt(1).line);
            Debug.println("Second lyric line: " + lyricLines.elementAt(1).line);
        }

        // handle window closing
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    sequencer.stop();
                    System.exit(0);
                }
            });

        // handle resize events
        addComponentListener(new ComponentAdapter() {
                public void componentResized(ComponentEvent e) {
                    Debug.printf("Component has resized to width %d, height %d\n",
                                      getWidth(), getHeight());
                    // force resize of children - especially the middle MelodyPanel
                    e.getComponent().validate();
                }
                public void componentShown(ComponentEvent e) {
                    Debug.printf("Component is visible with width %d, height %d\n",
                                      getWidth(), getHeight());
                }
            });

        setSize(1600, 900);
        setVisible(true);
    }

    public void setLanguage(int lang) {
        lyric1.setLanguage(lang);
        lyric2.setLanguage(lang);
    }

    /**
     * A lyric starts with a header section
     * We have to skip over that, but can pick useful
     * data out of it
     */

    /**
     * header format is
     *   \@Llanguage code
     *   \@Ttitle
     *   \@Tsinger
     */

    public void setLyric(String txt) {
        Debug.println("Setting lyric to " + txt);
        if (inLyricHeader) {
            if (txt.startsWith("@")) {
                Debug.println("Header: " + txt);
                return;
            } else {
                inLyricHeader = false;
            }
        }

        if ((lyricLine == -1) && (txt.charAt(0) == '\\')) {
            lyricLine = 0;
            colourLyric(lyricLinePanels[whichLyricPanel], txt.substring(1));
            // lyricLinePanels[whichLyricPanel].colourLyric(txt.substring(1));
            return;
        }

        if (txt.equals("\r\n") || (txt.charAt(0) == '/') || (txt.charAt(0) == '\\')) {
            if (lyricLine < lyricLines.size() -1)
                Debug.println("Setting next lyric line to \"" +
                              lyricLines.elementAt(lyricLine + 1).line + "\"");

            final int thisPanel = whichLyricPanel;
            whichLyricPanel = (whichLyricPanel + 1) % 2;

            Debug.println("Setting new lyric line at tick " +
                          sequencer.getTickPosition());

            lyricLine++;

            // if it's a \ r /, the rest of the txt should be the next  word to
            // be coloured

            if ((txt.charAt(0) == '/') || (txt.charAt(0) == '\\')) {
                Debug.println("Colouring newline of " + txt);
                colourLyric(lyricLinePanels[whichLyricPanel], txt.substring(1));
            }

            // Update the current line of text to show the one after next
            // But delay the update until 0.25 seconds after the next line
            // starts playing, to preserve visual continuity
            if (lyricLine + 1 < lyricLines.size()) {
                /*
                  long startNextLineTick = lyricLines.elementAt(lyricLine).startTick;
                  long delayForTicks = startNextLineTick - sequencer.getTickPosition();
                  Debug.println("Next  current "  + startNextLineTick + " " + sequencer.getTickPosition());
                  float microSecsPerQNote = sequencer.getTempoInMPQ();
                  float delayInMicroSecs = microSecsPerQNote * delayForTicks / 24 + 250000L;
                */

                final Vector<DurationNote> notes = lyricLines.elementAt(lyricLine).notes;

                final int nextLineForPanel = lyricLine + 1;

                if (lyricLines.size() >= nextLineForPanel) {
                    Timer timer = new Timer((int) 1000,
                                            new ActionListener() {
                                                public void actionPerformed(ActionEvent e) {
                                                    if (nextLineForPanel >= lyricLines.size()) {
                                                        return;
                                                    }
                                                    setText(lyricLinePanels[thisPanel], lyricLines.elementAt(nextLineForPanel).line);
                                                    //lyricLinePanels[thisPanel].setText(lyricLines.elementAt(nextLineForPanel).line);

                                                }
                                            });
                    timer.setRepeats(false);
                    timer.start();
                } else {
                    // no more lines
                }
            }
        } else {
            Debug.println("Playing lyric " + txt);
            colourLyric(lyricLinePanels[whichLyricPanel], txt);
            //lyricLinePanels[whichLyricPanel].colourLyric(txt);
        }
    }

    /**
     * colour the lyric of a panel.
     * called by one thread, makes changes in GUI thread
     */
    private void colourLyric(final AttributedLyricPanel p, final String txt) {
        SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    Debug.print("Colouring lyric \"" + txt + "\"");
                    if (p == lyric1) Debug.println(" on panel 1");
                    else Debug.println(" on panel 2");
                    p.colourLyric(txt);
                }
            }
            );
    }

    /**
     * set the lyric of a panel.
     * called by one thread, makes changes in GUI thread
     */
    private void setText(final AttributedLyricPanel p, final String txt) {
        SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    Debug.println("Setting text \"" + txt + "\"");
                    if (p == lyric1) Debug.println(" on panel 1");
                    else Debug.println(" on panel 2");
                    p.setText(txt);
                }
            }
            );
    }

    public void setNote(long timeStamp, int onOff, int note) {
        Debug.printf("Setting note in gui to %d\n", note);

        if (onOff == Constants.MIDI_NOTE_OFF) {
            pianoPanel.drawNoteOff(note);
        } else if (onOff == Constants.MIDI_NOTE_ON) {
            pianoPanel.drawNoteOn(note);
        }
    }
}

AttributedLyricPanel

显示一行歌词的面板必须能够以两种颜色显示文本:已经播放的歌词和尚未播放的歌词。Java AttributedText类对此很有用,因为文本可以用不同的属性标记,比如颜色。这被包裹在一个AttributedTextPanel中,稍后显示。

一个小问题与语言有关。中文既有汉字形式,也有一种罗马化的拼音形式。说中文的人可以阅读汉字。像我这样的人只能理解拼音形式。因此,如果语言是中文,那么AttributedTextPanel会在汉字旁边显示拼音。语言身份也应该传递给AttributedLyricPanel

AttributedLyricPanel如下所示:

import javax.swing.*;
import java.awt.*;
import java.awt.font.*;
import java.text.*;
import java.util.Map;

public class AttributedLyricPanel extends JPanel {

    private final int PINYIN_Y = 40;
    private final int TEXT_Y = 90;

    private String text;
    private AttributedString attrText;
    private int coloured = 0;
    private Font font = new Font(Constants.CHINESE_FONT, Font.PLAIN, 36);
    private Font smallFont = new Font(Constants.CHINESE_FONT, Font.PLAIN, 24);
    private Color red = Color.RED;
    private int language;
    private String pinyinTxt = null;

    private Map<Character, String> pinyinMap = null;

    public AttributedLyricPanel(Map<Character, String> pinyinMap) {
        this.pinyinMap = pinyinMap;
    }

    public Dimension getPreferredSize() {
        return new Dimension(1000, TEXT_Y + 20);
    }

    public void setLanguage(int lang) {
        language = lang;
        Debug.printf("Lang in panel is %X\n", lang);
    }

    public boolean isChinese() {
        switch (language) {
        case SongInformation.CHINESE1:
        case SongInformation.CHINESE2:
        case SongInformation.CHINESE8:
        case SongInformation.CHINESE131:
        case SongInformation.TAIWANESE3:
        case SongInformation.TAIWANESE7:
        case SongInformation.CANTONESE:
            return true;
        }
        return false;
    }

    public void setText(String txt) {
        coloured = 0;
        text = txt;
        Debug.println("set text " + text);
        attrText = new AttributedString(text);
        if (text.length() == 0) {
            return;
        }
        attrText.addAttribute(TextAttribute.FONT, font, 0, text.length());

        if (isChinese()) {
            pinyinTxt = "";
            for (int n = 0; n < txt.length(); n++) {
                char ch = txt.charAt(n);
                String pinyin = pinyinMap.get(ch);
                if (pinyin != null) {
                    pinyinTxt += pinyin + " ";
                } else {
                    Debug.printf("No pinyin map for character \"%c\"\n", ch);
                }
            }

        }

        repaint();
    }

    public void colourLyric(String txt) {
        coloured += txt.length();
        if (coloured != 0) {
            repaint();
        }
    }

    /**
     * Draw the string with the first part in red, rest in green.
     * String is centred
     */

    @Override
    public void paintComponent(Graphics g) {
        if ((text.length() == 0) || (coloured > text.length())) {
            return;
        }
        g.setFont(font);
        FontMetrics metrics = g.getFontMetrics();
        int strWidth = metrics.stringWidth(text);
        int panelWidth = getWidth();
        int offset = (panelWidth - strWidth) / 2;

        if (coloured != 0) {
            try {
                attrText.addAttribute(TextAttribute.FOREGROUND, red, 0, coloured);
            } catch(Exception e) {
                System.out.println(attrText.toString() + " " + e.toString());
            }
        }
        g.clearRect(0, 0, getWidth(), getHeight());
        try {
            g.drawString(attrText.getIterator(), offset, TEXT_Y);
        } catch (Exception e) {
            System.err.println("Attr Str exception on " + text);
        }
        // Draw the Pinyin if it's not zero
        if (pinyinTxt != null && pinyinTxt.length() != 0) {
            g.setFont(smallFont);
            metrics = g.getFontMetrics();
            strWidth = metrics.stringWidth(pinyinTxt);
            offset = (panelWidth - strWidth) / 2;

            g.drawString(pinyinTxt, offset, PINYIN_Y);
            g.setFont(font);
        }
    }
}

钢琴面板

PianoPanel展示了一个类似钢琴的键盘。当音符打开时,它会将音符涂成蓝色,并将之前播放的任何音符恢复正常。关闭便笺时,便笺会恢复正常颜色(黑色或白色)。

音符着色由setNote调用,因为没有来自音序器的开/关信息。

PianoPanel如下所示:

import java.util.Vector;
import javax.swing.*;
import java.awt.*;
import javax.sound.midi.*;

public class PianoPanel extends JPanel {

    private final int HEIGHT = 100;
    private final int HEIGHT_OFFSET = 10;

    long timeStamp;
    private Vector<DurationNote> notes;
    private Vector<DurationNote> sungNotes;
    private int lastNoteDrawn = -1;
    private Sequencer sequencer;
    private Sequence sequence;
    private int maxNote;
    private int minNote;

    private Vector<DurationNote> unresolvedNotes = new Vector<DurationNote> ();

    private int playingNote = -1;

    public PianoPanel(Sequencer sequencer) {

        maxNote = SequenceInformation.getMaxMelodyNote();
        minNote = SequenceInformation.getMinMelodyNote();
        Debug.println("Max: " + maxNote + " Min " + minNote);
    }

    public Dimension getPreferredSize() {
        return new Dimension(1000, 120);
    }

    public void drawNoteOff(int note) {
        if (note < minNote || note > maxNote) {
            return;
        }

        Debug.println("Note off played is " + note);
        if (note != playingNote) {
            // Sometimes "note off" followed immediately by "note on"
            // gets mixed up to "note on" followed by "note off".
            // Ignore the "note off" since the next note has already
            // been processed
            Debug.println("Ignoring note off");
            return;
        }
        playingNote = -1;
        repaint();
    }

    public void drawNoteOn(int note) {
        if (note < minNote || note > maxNote) {
            return;
        }

        Debug.println("Note on played is " + note);
        playingNote = note;
        repaint();

    }

    private void drawPiano(Graphics g, int width, int height) {
        int noteWidth = width / (Constants.MIDI_NOTE_C8 - Constants.MIDI_NOTE_A0);
        for (int noteNum =  Constants.MIDI_NOTE_A0; // A0
             noteNum <=  Constants.MIDI_NOTE_C8; // C8
             noteNum++) {

            drawNote(g, noteNum, noteWidth);
        }
    }

    private void drawNote(Graphics g, int noteNum, int width) {
        if (isWhite(noteNum)) {
            noteNum -= Constants.MIDI_NOTE_A0;
            g.setColor(Color.WHITE);
            g.fillRect(noteNum*width, HEIGHT_OFFSET, width, HEIGHT);
            g.setColor(Color.BLACK);
            g.drawRect(noteNum*width, HEIGHT_OFFSET, width, HEIGHT);
        } else {
            noteNum -= Constants.MIDI_NOTE_A0;
            g.setColor(Color.BLACK);
            g.fillRect(noteNum*width, HEIGHT_OFFSET, width, HEIGHT);
        }
        if (playingNote != -1) {
            g.setColor(Color.BLUE);
            g.fillRect((playingNote - Constants.MIDI_NOTE_A0) * width, HEIGHT_OFFSET, width, HEIGHT);
        }
    }

    private boolean isWhite(int noteNum) {
        noteNum = noteNum % 12;
        switch (noteNum) {
        case 1:
        case 3:
        case 6:
        case 8:
        case 10:
        case 13:
            return false;
        default:
            return true;
        }
    }

    @Override
    public void paintComponent(Graphics g) {

        int ht = getHeight();
        int width = getWidth();

        drawPiano(g, width, ht);

    }
}

melodyangel

MelodyPanel是一个滚动面板,显示旋律的所有音符。当前播放的音符在显示屏上居中。这是通过将所有音符绘制到一个BufferedImage中,然后每 50 毫秒复制一次相关部分来完成的。

MelodyPanel如下所示:

import java.util.Vector;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.sound.midi.*;
import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.*;

public class MelodyPanel extends JPanel {

    private static int DBL_BUF_SCALE = 2;
    private static final int NOTE_HEIGHT = 10;
    private static final int SLEEP_MSECS = 5;

    private long timeStamp;
    private Vector<DurationNote> notes;
    private Sequencer sequencer;
    private Sequence sequence;
    private int maxNote;
    private int minNote;
    private long tickLength = -1;
    private long currentTick = -1;
    private Image image = null;

    /**
     * The panel where the melody notes are shown in a
     * scrolling panel
     */
    public MelodyPanel(Sequencer sequencer) {

        maxNote = SequenceInformation.getMaxMelodyNote();
        minNote = SequenceInformation.getMinMelodyNote();
        Debug.println("Max: " + maxNote + " Min " + minNote);
        notes = SequenceInformation.getMelodyNotes();
        this.sequencer = sequencer;
        tickLength = sequencer.getTickLength() + 1000; // hack to make white space at end, plus fix bug

        //new TickPointer().start();
        // handle resize events
        addComponentListener(new ComponentAdapter() {
                public void componentResized(ComponentEvent e) {
                    Debug.printf("Component melody panel has resized to width %d, height %d\n",
                                      getWidth(), getHeight());
                }
                public void componentShown(ComponentEvent e) {
                    Debug.printf("Component malody panel is visible with width %d, height %d\n",
                                      getWidth(), getHeight());
                }
            });

    }

    /**
     * Redraw the melody image after each tick
     * to give a scrolling effect
     */
    private class TickPointer extends Thread {
        public void run() {
            while (true) {
                currentTick = sequencer.getTickPosition();
                MelodyPanel.this.repaint();
                /*
                SwingUtilities.invokeLater(
                                            new Runnable() {
                                                public void run() {
                                                    synchronized(MelodyPanel.this) {
                                                    MelodyPanel.this.repaint();
                                                    }
                                                }
                                            });
                */
                try {
                    sleep(SLEEP_MSECS);
                } catch (Exception e) {
                    // keep going
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * Draw the melody into a buffer so we can just copy bits to the screen
     */
    private void drawMelody(Graphics g, int front, int width, int height) {
        try {
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);
        g.setColor(Color.BLACK);

        String title = SequenceInformation.getTitle();
        if (title != null) {
            //Font f = new Font("SanSerif", Font.ITALIC, 40);
            Font f = new Font(Constants.CHINESE_FONT, Font.ITALIC, 40);
            g.setFont(f);
            int strWidth = g.getFontMetrics().stringWidth(title);
            g.drawString(title, (front - strWidth/2), height/2);
            Debug.println("Drawn title " + title);
        }

        for (DurationNote note: notes) {
            long startNote = note.startTick;
            long endNote = note.endTick;
            int value = note.note;

            int ht = (value - minNote) * (height - NOTE_HEIGHT) / (maxNote - minNote) + NOTE_HEIGHT/2;
            // it's upside down
            ht = height - ht;

            long start = front + (int) (startNote * DBL_BUF_SCALE);
            long end = front + (int) (endNote * DBL_BUF_SCALE);

            drawNote(g, ht, start, end);
            //g.drawString(title, (int)start, (int)height/2);
        }
        } catch(Exception e) {
            System.err.println("Drawing melody error " + e.toString());
        }
    }

    /**
     * Draw a horizontal bar to represent a nore
     */
    private void drawNote(Graphics g, int height, long start, long end) {
        Debug.printf("Drawing melody at start %d end %d height %d\n", start, end,  height - NOTE_HEIGHT/2);

        g.fillRect((int) start, height - NOTE_HEIGHT/2, (int) (end-start), NOTE_HEIGHT);
    }

    /**
     * Draw a vertical line in the middle of the screen to
     * represent where we are in the playing notes
     */
    private void paintTick(Graphics g, long width, long height) {
        long x = (currentTick * width) / tickLength;
        g.drawLine((int) width/2, 0, (int) width/2, (int) height);
        //System.err.println("Painted tcik");
    }

    // leave space at the front of the image to draw title, etc
    int front = 1000;

    /**
     * First time, draw the melody notes into an off-screen buffer
     * After that, copy a segment of the buffer into the image,
     * with the centre of the image the current note
     */
    @Override
    public void paintComponent(Graphics g) {
        int ht = getHeight();
        int width = getWidth();
        //int front = width / 2;

        synchronized(this) {
        if (image == null) {
            /*
             * We want to stretch out the notes so that they appear nice and wide on the screen.
             * A DBL_BUF_SCALE of 2 does this okay. But then tickLength * DBL_BUF_SCALE may end
             * up larger than an int, and we can't make a BufferedImage wider than MAXINT.
             * So we may have to adjust DBL_BUF_SCALE.
             *
             * Yes, I know we ask Java to rescale images on the fly, but that costs in runtime.
             */

            Debug.println("tick*DBLBUFSCALE " + tickLength * DBL_BUF_SCALE);

            if ((long) (tickLength * DBL_BUF_SCALE) > (long) Short.MAX_VALUE) {
                // DBL_BUF_SCALE = ((float)  Integer.MAX_VALUE) / ((float) tickLength);
                DBL_BUF_SCALE = 1;
                Debug.println("Adjusted DBL_BUF_SCALE to "+ DBL_BUF_SCALE);
            }

            Debug.println("DBL_BUF_SCALE is "+ DBL_BUF_SCALE);

            // draw melody into a buffered image
            Debug.printf("New buffered img width %d ht %d\n", tickLength, ht);
            image = new BufferedImage(front + (int) (tickLength * DBL_BUF_SCALE), ht, BufferedImage.TYPE_INT_RGB);
            Graphics ig = image.getGraphics();
            drawMelody(ig, front, (int) (tickLength * DBL_BUF_SCALE), ht);
            new TickPointer().start();

            try {
                File outputfile = new File("saved.png");
                ImageIO.write((BufferedImage) image, "png", outputfile);
            } catch (Exception e) {
                System.err.println("Error in image write " + e.toString());
            }

        }
        //System.err.printf("Drawing img from %d ht %d width %d\n",
        //                front + (int) (currentTick * DBL_BUF_SCALE - width/2), ht, width);

        boolean b = g.drawImage(image, 0, 0, width, ht,
                                front + (int) (currentTick * DBL_BUF_SCALE - width/2), 0,
                                front + (int) (currentTick * DBL_BUF_SCALE + width/2), ht,
                    null);
        /*System.out.printf("Ht of BI %d, width %d\n", ((BufferedImage)image).getHeight(),
                          ((BufferedImage) image).getWidth());
        */

        //if (b) System.err.println("Drawn ok"); else System.err.println("NOt drawn ok");
        paintTick(g, width, ht);
        }
    }

}

序列信息

SequenceInformation类是一个方便的类,由其他几个类使用。它存储序列、歌词线和旋律音符的副本,以通过用户界面显示歌词和旋律,以及歌曲标题、设置音符显示比例的最大和最小音符,以及旋律在哪个频道上。

public class SequenceInformation {

    private static Sequence sequence = null;
    private static Vector<LyricLine> lyricLines = null;
    private static Vector<DurationNote> melodyNotes = null;
    private static int lang = -1;
    private static String title = null;
    private static String performer = null;
    private static int maxNote;
    private static int minNote;

    private static int melodyChannel = -1;// no such channel
    ...
}

该类的方法如下:

public static void setSequence(Sequence seq)
public static long getTickLength()
public static int getMelodyChannel()
public static int getLanguage()
public static String getTitle()
public static String getPerformer()
public static Vector<LyricLine> getLyrics()
public static Vector<DurationNote> getMelodyNotes()
public static int getMaxMelodyNote()
public static int getMinMelodyNote()

getLyrics()的代码需要遍历序列中的轨道,寻找MIDI_TEXT_TYPE类型的MetaMessage,然后将它们添加到当前行,或者在换行符上开始新的一行。在这个过程中,它从文件的开始处获取表演者和标题的元数据。

    /*
     * Build a vector of lyric lines
     * Each line has a start and an end tick
     * and a string for the lyrics in that line
     */
    public static Vector<LyricLine> getLyrics() {
        if (lyricLines != null) {
            return lyricLines;
        }

        lyricLines = new Vector<LyricLine> ();
        LyricLine nextLyricLine = new LyricLine();
        StringBuffer buff = new StringBuffer();
        long ticks = 0L;

        Track[] tracks = sequence.getTracks();
        for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
            for (int n = 0; n < tracks[nTrack].size(); n++) {
                MidiEvent evt = tracks[nTrack].get(n);
                MidiMessage msg = evt.getMessage();
                ticks = evt.getTick();

                if (msg instanceof MetaMessage) {
                    Debug.println("Got a meta mesg in seq");
                    if (((MetaMessage) msg).getType() == Constants.MIDI_TEXT_TYPE) {
                        MetaMessage message = (MetaMessage) msg;

                        byte[] data = message.getData();
                        String str = new String(data);
                        Debug.println("Got a text mesg in seq \"" + str + "\" " + ticks);

                        if (ticks == 0) {
                            if (str.startsWith("@L")) {
                                lang = decodeLang(str.substring(2));
                            } else if (str.startsWith("@T")) {
                                if (title == null) {
                                    title = str.substring(2);
                                } else {
                                    performer = str.substring(2);
                                }
                            }

                        }
                        if (ticks > 0) {
                            //if (str.equals("\r") || str.equals("\n")) {
                            if ((data[0] == '/') || (data[0] == '\\')) {
                                if (buff.length() == 0) {
                                    // blank line -  maybe at start of song
                                    // fix start time from NO_TICK
                                    nextLyricLine.startTick = ticks;
                                } else {
                                    nextLyricLine.line = buff.toString();
                                    nextLyricLine.endTick = ticks;
                                    lyricLines.add(nextLyricLine);
                                    buff.delete(0, buff.length());

                                    nextLyricLine = new LyricLine();
                                }
                                buff.append(str.substring(1));
                            } else {
                                if (nextLyricLine.startTick == Constants.NO_TICK) {
                                    nextLyricLine.startTick = ticks;
                                }
                                buff.append(str);
                            }
                        }
                    }
                }
            }
            // save last line (but only once)
            if (buff.length() != 0) {
                nextLyricLine.line = buff.toString();
                nextLyricLine.endTick = ticks;
                lyricLines.add(nextLyricLine);
                buff.delete(0, buff.length());
            }
        }
        if (Debug.DEBUG) {
            dumpLyrics();
        }
        return lyricLines;
    }

getMelodyNotes()的代码遍历序列,在旋律通道中寻找 MIDI 开/关音符。代码有点混乱,因为一些歌曲有“不干净”的数据:它们可能包含超出允许范围的音符值,有时会重叠,而不是一个音符在下一个音符开始之前结束。这段代码如下:

    /*
     * gets a vector of lyric notes
     * side-effect: sets last tick
     */
    public static Vector<DurationNote> getMelodyNotes() {
        if (melodyChannel == -1) {
            getMelodyChannel();
        }

        if (melodyNotes != null) {
            return melodyNotes;
        }

        melodyNotes = new Vector<DurationNote> ();
        Vector<DurationNote> unresolvedNotes = new Vector<DurationNote> ();

        Track[] tracks = sequence.getTracks();
        for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
            for (int n = 0; n < tracks[nTrack].size(); n++) {
                MidiEvent evt = tracks[nTrack].get(n);
                MidiMessage msg = evt.getMessage();
                long ticks = evt.getTick();

                if (msg instanceof ShortMessage) {
                    ShortMessage smsg= (ShortMessage) msg;
                    if (smsg.getChannel() == melodyChannel) {
                        int note = smsg.getData1();
                        if (note < Constants.MIDI_NOTE_A0 || note > Constants.MIDI_NOTE_C8) {
                            continue;
                        }

                        if (smsg.getCommand() == Constants.MIDI_NOTE_ON) {
                            // note on
                            DurationNote dnote = new DurationNote(ticks, note);
                            melodyNotes.add(dnote);
                            unresolvedNotes.add(dnote);

                        } else if (smsg.getCommand() == Constants.MIDI_NOTE_OFF) {
                            // note off
                            for (int m = 0; m < unresolvedNotes.size(); m++) {
                                DurationNote dnote = unresolvedNotes.elementAt(m);
                                if (dnote.note == note) {
                                    dnote.duration = ticks - dnote.startTick;
                                    dnote.endTick = ticks;
                                    unresolvedNotes.remove(m);
                                }
                            }

                        }

                    }
                }
            }
        }
        return melodyNotes;
    }

任何复杂度的最后一个方法是getMelodyChannel()。MIDI 信息不区分哪个通道包含旋律。大多数歌曲都有 1 频道的旋律,但不是全部。因此,必须使用一种启发式方法:搜索第一个要播放的音符非常接近第一个真正歌词的频道。这不是 100%可靠的。

    public static int getMelodyChannel() {
        boolean firstNoteSeen[] = {false, false, false, false, false, false, false, false,
                                   false, false, false, false, false, false, false, false};
        boolean possibleChannel[] = {false, false, false, false, false, false, false, false,
                                   false, false, false, false, false, false, false, false};
        if (melodyChannel != -1) {
            return melodyChannel;
        }

        if (lyricLines == null) {
            lyricLines = getLyrics();
        }

        long startLyricTick = ((LyricLine) lyricLines.get(0)).startTick;
        Debug.printf("Lyrics start at %d\n", startLyricTick);

        Track[] tracks = sequence.getTracks();
        for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
            Track track = tracks[nTrack];
            for (int nEvent = 0; nEvent < track.size(); nEvent++) {
                MidiEvent evt = track.get(nEvent);
                MidiMessage msg = evt.getMessage();
                if (msg instanceof ShortMessage) {
                    ShortMessage smsg= (ShortMessage) msg;
                    int channel = smsg.getChannel();
                    if (firstNoteSeen[channel]) {
                        continue;
                    }
                    if (smsg.getCommand() == Constants.MIDI_NOTE_ON) {
                        long tick = evt.getTick();
                        Debug.printf("First note on for channel %d at tick %d\n",
                                          channel, tick);
                        firstNoteSeen[channel] = true;
                        if (Math.abs(startLyricTick - tick) < 10) {
                            // close enough - we hope!
                            melodyChannel = channel;
                            possibleChannel[channel] = true;
                            Debug.printf("Possible melody channel is %d\n", channel);
                        }
                        if (tick > startLyricTick + 11) {
                            break;
                        }
                    }
                }
            }
        }

        return melodyChannel;
    }

其他方法相对简单,在此省略。

拼音

对于中文文件,我的目标之一是显示中国象形文字的拼音(罗马化形式)。为此,我需要能够将任何序列的汉字改写成拼音形式。我找不到字符及其对应字符的列表。最接近的是汉英词典( www.mandarintools.com/worddict.html ),你可以从里面下载词典作为文本文件。该文件中的典型行如下:

不賴 不赖 [bu4 lai4] /not bad/good/fine/

每一行都有繁体字、简化字、[…]的拼音,然后是英文意思。

我使用下面的 shell 脚本创建了一个字符/拼音对列表:

#!/bin/bash

# get pairs of character + pinyin by throwing away other stuff in the dictionary

awk '{print $2, $3}' cedict_ts.u8 | grep -v '[A-Z]' |
  grep -v '^.[^ ]' | sed -e 's/\[//' -e 's/\]//' -e 's/[0-9]$//' |
    sort | uniq -w 1 > pinyinmap.txt

给出如下所示的行:

好 hao
妁 shuo
如 ru
妃 fei

然后可以将它读入 Java Map,然后可以进行快速查找,将中文翻译成拼音。

带采样的 Karaoke 播放器

到目前为止所描述的 Karaoke 播放器在功能上等同于kmidipykar。它播放 KAR 文件,显示音符,滚动歌词。要跟着唱,你需要使用 ALSA 或 PulseAudio 播放器。

但是 Java 也可以播放样本声音,这在前面的章节中已经讨论过了。因此,可以将该代码引入 Karaoke 播放器,以提供更完整的解决方案。对于 MIDI,Java 通常只给出一个 Gervill 合成器,这是一个通过 PulseAudio 默认设备播放的软件合成器。实际的输出设备不能通过 Java 访问,而是由底层的 PulseAudio 输出设备控制。但是对于采样媒体,输入设备是可以控制的。因此,在下面的代码中,选择框允许选择采样的输入设备,并将输出设备保留为默认值。

/*
 * KaraokePlayer.java
 *
 */

import javax.swing.*;
import javax.sound.sampled.*;

public class KaraokePlayerSampled {

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("KaraokePlayer: usage: " +
                               "KaraokePlayer <midifile>");
            System.exit(1);
        }
        String  strFilename = args[0];

        Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo();

        String[] possibleValues = new String[mixerInfo.length];
        for(int cnt = 0; cnt < mixerInfo.length; cnt++){
            possibleValues[cnt] = mixerInfo[cnt].getName();
        }
        Object selectedValue = JOptionPane.showInputDialog(null, "Choose mixer", "Input",
                                                       JOptionPane.INFORMATION_MESSAGE, null,
                                                       possibleValues, possibleValues[0]);

        System.out.println("Mixer string selected " + ((String)selectedValue));

        Mixer mixer = null;
        for(int cnt = 0; cnt < mixerInfo.length; cnt++){
            if (mixerInfo[cnt].getName().equals((String)selectedValue)) {
                mixer = AudioSystem.getMixer(mixerInfo[cnt]);
                System.out.println("Got a mixer");
                break;
            }
        }//end for loop

        MidiPlayer midiPlayer = new MidiPlayer();
        midiPlayer.playMidiFile(strFilename);

        SampledPlayer sampledPlayer = new SampledPlayer(/* midiPlayer.getReceiver(), */ mixer);
        sampledPlayer.playAudio();
    }
}

播放样本媒体的代码与您之前看到的非常相似。

import java.io.IOException;

import javax.sound.sampled.Line;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.TargetDataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.Control;

import javax.swing.*;

public class SampledPlayer {

    private DisplayReceiver receiver;
    private Mixer mixer;

    public SampledPlayer(/* DisplayReceiver receiver, */ Mixer mixer) {
        this.receiver = receiver;
        this.mixer = mixer;
    }

    //This method creates and returns an
    // AudioFormat object for a given set of format
    // parameters.  If these parameters don't work
    // well for you, try some of the other
    // allowable parameter values, which are shown
    // in comments following the declarations.
    private static AudioFormat getAudioFormat(){
        float sampleRate = 44100.0F;
        //8000,11025,16000,22050,44100
        int sampleSizeInBits = 16;
        //8,16
        int channels = 1;
        //1,2
        boolean signed = true;
        //true,false
        boolean bigEndian = false;
        //true,false
        return new AudioFormat(sampleRate,
                               sampleSizeInBits,
                               channels,
                               signed,
                               bigEndian);
    }//end getAudioFormat

    public  void playAudio() throws Exception {
        AudioFormat audioFormat;
        TargetDataLine targetDataLine;

        audioFormat = getAudioFormat();
        DataLine.Info dataLineInfo =
            new DataLine.Info(
                              TargetDataLine.class,
                              audioFormat);
        targetDataLine = (TargetDataLine)
            AudioSystem.getLine(dataLineInfo);

        targetDataLine.open(audioFormat,
                            audioFormat.getFrameSize() * Constants.FRAMES_PER_BUFFER);
        targetDataLine.start();

        /*
        for (Control control: targetDataLine.getControls()) {
            System.out.println("Target control: " + control.getType());
        }
        */

        playAudioStream(new AudioInputStream(targetDataLine), mixer);
    } // playAudioFile

    /** Plays audio from the given audio input stream. */
    public  void playAudioStream(AudioInputStream audioInputStream, Mixer mixer) {

        new AudioPlayer(audioInputStream, mixer).start();
    } // playAudioStream

    class AudioPlayer extends Thread {
        AudioInputStream audioInputStream;
        SourceDataLine dataLine;
        AudioFormat audioFormat;

        // YIN stuff
        // PitchProcessorWrapper ppw;

        AudioPlayer( AudioInputStream audioInputStream, Mixer mixer) {
            this.audioInputStream = audioInputStream;

            // Set to nearly max, like Midi sequencer does
            Thread curr = Thread.currentThread();
            Debug.println("Priority on sampled: " + curr.getPriority());
            int priority = Thread.NORM_PRIORITY
                + ((Thread.MAX_PRIORITY - Thread.NORM_PRIORITY) * 3) / 4;
            curr.setPriority(priority);
            Debug.println("Priority now on sampled: " + curr.getPriority());

            // Audio format provides information like sample rate, size, channels.
            audioFormat = audioInputStream.getFormat();
            Debug.println( "Play input audio format=" + audioFormat );

            // Open a data line to play our type of sampled audio.
            // Use SourceDataLine for play and TargetDataLine for record.

            if (mixer == null) {
                System.out.println("can't find a mixer");
            } else {
                Line.Info[] lines = mixer.getSourceLineInfo();
                if (lines.length >= 1) {
                    try {
                        dataLine = (SourceDataLine) AudioSystem.getLine(lines[0]);
                        System.out.println("Got a source line for " + mixer.toString());
                    } catch(Exception e) {
                    }
                } else {
                    System.out.println("no source lines for this mixer " + mixer.toString());
                }
            }

                for (Control control: mixer.getControls()) {
                    System.out.println("Mixer control: " + control.getType());
                }

            DataLine.Info info = null;
            if (dataLine == null) {
                info = new DataLine.Info( SourceDataLine.class, audioFormat );
                if ( !AudioSystem.isLineSupported( info ) ) {
                    System.out.println( "Play.playAudioStream does not handle this type of audio on this system." );
                    return;
                }
            }

            try {
                // Create a SourceDataLine for play back (throws LineUnavailableException).
                if (dataLine == null) {
                    dataLine = (SourceDataLine) AudioSystem.getLine( info );
                }
                Debug.println( "SourceDataLine class=" + dataLine.getClass() );

                // The line acquires system resources (throws LineAvailableException).
                dataLine.open( audioFormat,
                               audioFormat.getFrameSize() * Constants.FRAMES_PER_BUFFER);

                for (Control control: dataLine.getControls()) {
                    System.out.println("Source control: " + control.getType());
                }
                // Adjust the volume on the output line.
                if( dataLine.isControlSupported( FloatControl.Type.VOLUME) ) {
                    // if( dataLine.isControlSupported( FloatControl.Type.MASTER_GAIN ) ) {
                    //FloatControl volume = (FloatControl) dataLine.getControl( FloatControl.Type.MASTER_GAIN );
                    FloatControl volume = (FloatControl) dataLine.getControl( FloatControl.Type.VOLUME);
                    System.out.println("Max vol " + volume.getMaximum());
                    System.out.println("Min vol " + volume.getMinimum());
                    System.out.println("Current vol " + volume.getValue());
                    volume.setValue( 60000.0F );
                    System.out.println("New vol " + volume.getValue());
                } else {
                    System.out.println("Volume control not supported");
                }
                if (dataLine.isControlSupported( FloatControl.Type.REVERB_RETURN)) {
                    System.out.println("reverb return supported");
                } else {
                    System.out.println("reverb return not supported");
                }
                if (dataLine.isControlSupported( FloatControl.Type.REVERB_SEND)) {
                    System.out.println("reverb send supported");
                } else {
                    System.out.println("reverb send not supported");
                }

            } catch ( LineUnavailableException e ) {
                e.printStackTrace();
            }

            // ppw = new PitchProcessorWrapper(audioInputStream, receiver);
        }

        public void run() {

            // Allows the line to move data in and out to a port.
            dataLine.start();

            // Create a buffer for moving data from the audio stream to the line.
            int bufferSize = (int) audioFormat.getSampleRate() * audioFormat.getFrameSize();
            bufferSize =  audioFormat.getFrameSize() * Constants.FRAMES_PER_BUFFER;
            Debug.println("Buffer size: " + bufferSize);
            byte [] buffer = new byte[bufferSize];

            try {
                int bytesRead = 0;
                while ( bytesRead >= 0 ) {
                    bytesRead = audioInputStream.read( buffer, 0, buffer.length );
                    if ( bytesRead >= 0 ) {
                        int framesWritten = dataLine.write( buffer, 0, bytesRead );
                        // ppw.write(buffer, bytesRead);
                    }
                } // while
            } catch ( IOException e ) {
                e.printStackTrace();
            }

            // Continues data line I/O until its buffer is drained.
            dataLine.drain();

            Debug.println( "Sampled player closing line." );
            // Closes the data line, freeing any resources such as the audio device.
            dataLine.close();
        }
    }

    // Turn into a GUI version or pick up from prefs
    public void listMixers() {
        try{
            Mixer.Info[] mixerInfo =
                AudioSystem.getMixerInfo();
            System.out.println("Available mixers:");
            for(int cnt = 0; cnt < mixerInfo.length;
                cnt++){
                System.out.println(mixerInfo[cnt].
                                   getName());

                Mixer mixer = AudioSystem.getMixer(mixerInfo[cnt]);
                Line.Info[] sourceLines = mixer.getSourceLineInfo();
                for (Line.Info s: sourceLines) {
                    System.out.println("  Source line: " + s.toString());
                }
                Line.Info[] targetLines = mixer.getTargetLineInfo();
                for (Line.Info t: targetLines) {
                    System.out.println("  Target line: " + t.toString());
                }

            }//end for loop
        } catch(Exception e) {
        }
    }
}

对设备选择的评论

如果选择了默认设备,则输入和输出设备是 PulseAudio 默认设备。通常这两个都是计算机的声卡。但是,可以使用 PulseAudio 音量控制等功能来更改默认设备。这些可以设置输入设备和/或输出设备。该对话还可以用于设置采样媒体的输入设备。

这提出了一些可能的情况:

  • 默认 PulseAudio 器件选择相同的输入和输出器件。
  • 默认 PulseAudio 器件选择不同的输入和输出器件。
  • 默认的 PulseAudio 设备用于输出,而 ALSA 设备用于输入,但物理设备是相同的。
  • 默认的 PulseAudio 设备用于输出,ALSA 设备用于输入,物理设备不同。

使用不同的器件会产生时钟漂移问题,即器件具有不同步的不同时钟。最糟糕的情况似乎是第二种情况,在我的系统上播放一首三分钟的歌曲时,我可以听到播放采样音频时有明显的延迟,而 KAR 文件播放得很愉快。它还在播放采样音频时引入了明显的延迟。

表演

程序top可以让你很好的了解各种进程使用了多少 CPU。我目前的电脑是一台高端戴尔笔记本电脑,配有四核英特尔 i7-2760QM CPU,运行频率为 2.4GHz。根据 CPU 基准测试( www.cpubenchmark.net/ ),该处理器位于“高端 CPU 图表”中在这台电脑上,用各种 KAR 文件进行了测试,PulseAudio 占用了大约 30%的 CPU,而 Java 占用了大约 60%。有时会超过这些数字。附加功能已经所剩无几了!

此外,在播放 MIDI 文件时,有时 Java 进程会挂起,以高达 600%的 CPU 使用率恢复(我不知道top如何设法记录这一点)。这使得它实际上无法使用,我不知道问题出在哪里。

结论

Java 声音对 Karaoke 没有直接支持。本章介绍了如何将 Java 声音库与 Swing 等其他库结合起来,为 MIDI 文件创建一个 Karaoke 播放器。运行这些程序需要一台高端电脑。

二十六、字幕和隐藏字幕

许多 Karaoke 系统使用字幕 1 叠加在某种电影上。像kmid这样的程序和我的 Java 程序在某种画布对象上播放歌词。这给了一个相当无聊的背景。视频 CD 或 MPEG-4 文件有更好的背景,但歌词被硬编码到背景视频中,所以对它们进行操作的机会很小。CD+G 文件将歌词与视频分开,但似乎没有任何方法可以直接从 Linux 播放它们。它们可以被转换成 MP3+G,并且它们可以由 VLC 播放,它加载 MP3 文件并拾取相应的.cdg文件。

本章考虑可以独立制作的字幕,以某种方式与视频和音频结合,然后播放。目前的情况并不完全令人满意。

资源

查看以下资源:

字幕格式

本章关注的是所谓的软字幕,字幕存储在一个独立于视频或音频的文件中,并在渲染过程中被合并。维基百科页面“字幕(captioning)”(http://en.wikipedia.org/wiki/Subtitle_(captioning`)是一篇长文,探讨了许多关于字幕的问题。它还包含一个字幕格式列表,但是在这个上下文中最有用的是 SubStation Alpha。

MPlayer

根据 MPlayer 页面“字幕和 OSD”(www.mplayerhq.hu/DOCS/HTML/en/subosd.htm),以下是 MPlayer 可识别的格式:

  1. 沃博布
  2. 外出留言
  3. 闭路字幕
  4. 微 DVD
  5. subrip(子 ip)
  6. 子观众
  7. 萨米人
  8. 虚拟播放器
  9. 无线电报
  10. 社会保障总署(Social Security Administration)
  11. PJS(凤凰日本学会)
  12. MPsub
  13. aititle
  14. JACOsub

可见光通讯

根据 VLC ( www.videolan.org/vlc/features.php?cat=sub ,Linux 下支持的字幕格式包括以下几种:

  1. 数字影碟
  2. 文本文件(MicroDVD、SubRIP、SubViewer、SSA1-5、SAMI、VPlayer)
  3. 隐藏字幕
  4. 沃博布
  5. 通用字幕格式(USF)
  6. SVCD/CVD
  7. 二乙烯基苯
  8. 外出留言
  9. CMML(移动通信)
  10. 凯特

如果您播放某种视频文件,比如说XYZ.mpg,并且还有一个具有相同根名称和适当扩展名的文件,比如XYZ.ass(变电站 Alpha 的扩展名),那么 VLC 将自动加载字幕文件并播放它。如果字幕文件有不同的名称,那么它可以从 VLC 菜单视频➤字幕轨道加载。然而,这似乎没有共享名称可靠。

Gnome 字幕制作

看到“Gnome 字幕 1.3 出来了!”( http://gnome-subtitles.sourceforge.net/ )。Gnome 支持 Adobe Encore DVD、Advanced Sub Station、Alpha AQ、Title DKS 字幕格式 FAB 副标题 Karaoke 歌词 LRC Karaoke 歌词 VKT MAC Sub MicroDVD MPlayer MPlayer 2 MP Sub Panimator Phoenix Japanimation Society Power DivX Sofni Sub creator 1 . x Sub rip Sub Station Alpha Sub viewer 1.0、SubViewer 2.0 和 ViPlay 字幕文件。

阿尔法变电站

SSA/ASS 规范位于 MooDub.free ( http://moodub.free.fr/video/ass-specs.doc )。它很简短,似乎包含了一些关于后来的规范和实现的小错误。比如时间格式不一样。还是后来的都是错的?

SSA/ASS 文件可以独立使用。它们也可以包含在诸如 Matroska 文件的容器格式中,这将在第三章中简要讨论。当它们被嵌入到 MKV 文件中时,会受到一些限制( www.matroska.org/technical/specs/subtitles/ssa.html ),例如文本被转换成 UTF-8 Unicode。

ASS 文件分为几个部分。

  1. 关于字幕文件期望的环境的一般信息,例如 X 和 Y 分辨率
  2. 颜色和字体等样式信息
  3. 事件信息,其中给出了字幕文本以及定时信息和要应用的任何特殊效果

在正常情况下,您不会使用文本编辑器直接创建这样的文件。相反,程序 Aegisub 为您提供了一个创建文件的 GUI 环境。实际上,您只需输入文本行,以及要显示的每行的开始和结束时间。

图 26-1 为屏幕截图。

A435426_1_En_26_Fig1_HTML.jpg

图 26-1。

Aegisub screenshot

许多特殊效果是可能的。比尔·克雷斯韦尔博客上的视频( https://billcreswell.wordpress.com/tag/aegisub/ )就是一个很好的例子。下面是 YouTube 的直接链接: www.youtube.com/watch?v=0Z0dgdglrAo

为了完整起见,下面是我创建的一个 ASS 文件的一部分:

[Script Info]
; Script generated by Aegisub 2.1.9
; http://www.aegisub.org/
Title: Default Aegisub file
ScriptType: v4.00+
WrapStyle: 0
PlayResX: 640
PlayResY: 480
ScaledBorderAndShadow: yes
Video Aspect Ratio: 0
Video Zoom: 6
Video Position: 0

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00FFFFFF,&H00B4FCFC,&H00000008,&H80000008,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:18.22,0:00:19.94,Default,,0000,0000,0000,,Here comes the sun
Dialogue: 0,0:00:20.19,0:00:21.75,Default,,0000,0000,0000,,doo doo doo doo
Dialogue: 0,0:00:22.16,0:00:24.20,Default,,0000,0000,0000,,Here comes the sun
Dialogue: 0,0:00:24.61,0:00:28.24,Default,,0000,0000,0000,,I said it's alright
...

屁股文件里的 Karaoke 效果

ASS 文件中的一行基本上由开始显示的时间、结束显示的时间和文本本身组成。然而,Karaoke 用户习惯于在播放时突出显示文本。

ASS 支持两大高光样式。

  1. 一次突出显示一个单词。
  2. 文本通过从左侧填充来突出显示。

这些效果是通过在文本中嵌入“Karaoke 覆盖”来实现的。这些都在{}中,持续时间为百分之一秒。

详情如下:

  1. 单词高亮显示对表单{\k<time>}的覆盖将高亮显示后面的单词time百分之一秒。下面是一个例子:

    {\k100}Here {\k150}comes {\k50}the {\k150}sun
    
    
  2. 填充高亮显示对表单{\kf<time>}的覆盖将在百分之一秒的时间内逐渐填充接下来的单词time。下面是一个例子:

    {\kf100}Here {\kf150}comes {\kf50}the {\kf150}sun
    
    

    三种风格出现如下:

  3. Lines with no highlighting (see Figure 26-2)

    A435426_1_En_26_Fig2_HTML.jpg

    图 26-2。

    Subtitles without highlighting

  4. Word highlighting (see Figure 26-3)

    A435426_1_En_26_Fig3_HTML.jpg

    图 26-3。

    Subtitles with word highlighting

  5. Fill highlighting (see Figure 26-4)

    A435426_1_En_26_Fig4_HTML.jpg

    图 26-4。

    Subtitles with fill highlighting

多线 Karaoke

理想情况下,Karaoke 系统应该有一个“前瞻”机制,这样你就可以在唱出下一行之前看到它。这可以通过在不同高度显示两行重叠时间的文本来实现。算法如下:

When line N with markup is shown,
    show line N+1 without markup
After line N is finished, continue showing line N+1
When line N+1 is due to show,
     finish showing unmarked line N+1
     show line N+1 with markup

下面是歌曲《太阳来了》的歌词:

Here comes the sun
doo doo doo doo
Here comes the sun
I said it's alright

生成的 ASS 文件应该如下所示:

Dialogue: 0,0:00:18.22,0:00:19.94,Default,,0000,0000,0100,,{\kf16}Here {\kf46}comes {\kf43}the {\kf67}sun
Dialogue: 0,0:00:18.22,0:00:20.19,Default,,0000,0000,0000,,doo doo doo doo
Dialogue: 0,0:00:20.19,0:00:21.75,Default,,0000,0000,0000,,{\kf17}doo {\kf25}doo {\kf21}doo {\kf92}doo
Dialogue: 0,0:00:20.19,0:00:22.16,Default,,0000,0000,0100,,Here comes the sun
Dialogue: 0,0:00:22.16,0:00:24.20,Default,,0000,0000,0100,,{\kf17}Here {\kf46}comes {\kf43}the {\kf97}sun
Dialogue: 0,0:00:22.16,0:00:24.61,Default,,0000,0000,0000,,I said it's alright

图 26-5 显示了它的样子。

A435426_1_En_26_Fig5_HTML.jpg

图 26-5。

Multiline subtitles

利巴斯

变电站阿尔法和它的渲染器似乎经历了复杂的历史。据《古今:VSFilter》(http://blog.aegisub.org/2010/02/old-and-present-vsfilter.html)记载,ASS 格式大约在 2004 年定型,渲染器 vs filter 就是在那个时候开源的。然而,在 2007 年左右,VSFilter 的开发停止了,出现了几个分支。这些对格式引入了几个扩展,比如 Aegisub 的blur标签。其中一些分支后来被合并了,一些被放弃了,其中一些分支的代码仍然存在。

libass ( http://code.google.com/p/libass/ )是 Linux 的主要渲染库。另一种替代方法 xy-vsfilter 号称更快、更可靠等等,但似乎没有 Linux 实现。libass 支持一些后来的扩展。这些似乎是 Aegisub 2008 的扩展,根据“vs filter hacks”(http://blog.aegisub.org/2008/07/vsfilter-hacks.html)。

将 KAR 文件转换成带屁股字幕的 MKV 文件

请遵循以下步骤:

  1. 要从 KAR 或 MIDI 文件中提取歌词,使用第十八章中给出的 Java DumpSequence,如下所示,获取所有事件的转储:

    java DumpSequence  song.kar  > song.dump
    
    
  2. 对于仅行显示,使用 Aegisub 2.1.9 生成的以下 Python 脚本提取歌词并保存为 ASS 格式:

    #!/usr/bin/python
    
    import fileinput
    import string
    import math
    
    TEXT_STR = "Dialogue: 0,%s,%s,Default,,0000,0000,0000,Karaoke,"
    
    textStr = TEXT_STR
    startTime = -1
    endTime = -1
    
    def printPreface():
        print '[Script Info]\r\n\
    ; Script generated by Aegisub 2.1.9\r\n\
    ; http://www.aegisub.org/\r\n\
    Title: Default Aegisub file\r\n\
    ScriptType: v4.00+\r\n\
    WrapStyle: 0\r\n\
    PlayResX: 640\r\n\
    PlayResY: 480\r\n\
    ScaledBorderAndShadow: yes\r\n\
    Video Aspect Ratio: 0\r\n\
    Video Zoom: 6\r\n\
    Video Position: 0\r\n\
    \r\n\
    [V4+ Styles]\r\n\
    Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n\
    Style: Default,Arial,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\r\n\
    \r\n\
    [Events]\r\n\
    Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
    
    def timeFormat(s):
        global microSecondsPerTick
    
        tf = float(s)
        tf /= 62.6  #ticks per sec
    
        # This should be right , but is too slow
        #tf = (tf * microSecondsPerTick) / 1000000
    
        t = int(math.floor(tf))
        hundredths = round((tf-t)*100)
        secs = t % 60
        t /= 60
        mins = t % 60
        t /= 60
        hrs = t
        return "%01d:%02d:%02d.%02d" % (hrs, mins, secs, hundredths)
    
    def doLyric(words):
        global textStr
        global startTime
        global endTime
        global TEXT_STR
    
        if words[1] == "0:":
            #print "skipping"
            return
    
        time = string.rstrip(words[1], ':')
        if startTime == -1:
            startTime = time
        #print words[1],
        if len(words) == 5:
            if words[4][0] == '\\' or words[4][0] == '/':
                #print "My name is %s and weight is %d kg!" % ('Zara', 21)
                #print startTime, endTime
                print textStr % (timeFormat(startTime), timeFormat(endTime)) + "\r\n",
                textStr = TEXT_STR + words[4][:1]
                startTime = -1
            else:
                textStr += words[4]
        else:
            textStr += ' '
    
        endTime = time
    
    printPreface()
    
    for line in fileinput.input():
        words = line.split()
    
        if len(words)  >= 2:
            if words[0] == "Resolution:":
                ticksPerBeat = words[1]
            elif words[0] == "Length:":
                numTicks = int(words[1])
            elif words[0] == "Duration:":
                duration = int(words[1])
                microSecondsPerTick = duration/numTicks
                # print "Duration %d numTicks %d microSecondsPerTick %d" % (duration, numTicks, microSecondsPerTick)
    
        if len(words) >= 3 and words[2] == "Text":
            doLyric(words)
    
    

    下面是一个例子:

    python lyric2ass4kar.py song.dump > song.ass
    
    
  3. 对于填充歌词显示,使用下面的 Python 脚本提取歌词并以 ASS 格式保存:

     #!/usr/bin/python
    
    import fileinput
    import string
    import math
    
    TEXT_STR = "Dialogue: 0,%s,%s,Default,,0000,0000,0000,,"
    
    textStr = "{\kf%d}"
    plainTextStr = ""
    startTime = -1
    startWordTime = -1
    endTime = -1
    
    def printPreface():
        print '[Script Info]\r\n\
    ; Script generated by Aegisub 2.1.9\r\n\
    ; http://www.aegisub.org/\r\n\
    Title: Default Aegisub file\r\n\
    ScriptType: v4.00+\r\n\
    WrapStyle: 0\r\n\
    PlayResX: 640\r\n\
    PlayResY: 480\r\n\
    ScaledBorderAndShadow: yes\r\n\
    Video Aspect Ratio: 0\r\n\
    Video Zoom: 6\r\n\
    Video Position: 0\r\n\
    \r\n\
    [V4+ Styles]\r\n\
    Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n\
    Style: Default,Arial,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\r\n\
    \r\n\
    [Events]\r\n\
    Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
    
    def timeFormat(s):
        global microSecondsPerTick
    
        tf = float(s)
    
        # frames per sec should be 60: 120 beats/min, 30 ticks per beat
        # but it is too slow on 54154
        tf /= 62.6  #ticks per sec
    
        # This should be right , but is too slow
        # tf = (tf * microSecondsPerTick) / 1000000
    
        t = int(math.floor(tf))
        hundredths = round((tf-t)*100)
        secs = t % 60
        t /= 60
        mins = t % 60
        t /= 60
        hrs = t
        return "%01d:%02d:%02d.%02d" % (hrs, mins, secs, hundredths)
    
    def durat(end, start):
        fend = float(end)
        fstart = float(start)
        d = (fend - fstart) / 62.9
        #print end, start, d
        return round(d*100)
    
    def doLyric(words):
        global textStr
        global plainTextStr
        global startTime
        global endTime
        global TEXT_STR
        global startWordTime
        global lineNum
    
        if words[1] == "0:":
            #print "skipping"
            return
    
        time = string.rstrip(words[1], ':')
        if startTime == -1:
            startTime = time
            startWordTime = time
            previousEndTime = time
        #print words[1],
        if len(words) == 5:
            if words[4][0] == '\\' or words[4][0] == '/':
                #print "My name is %s and weight is %d kg!" % ('Zara', 21)
                #print startTime, endTime
                dur = durat(time, startWordTime)
                textStr = textStr % (dur)
                if len(words[4]) == 1:
                    print TEXT_STR % (timeFormat(startTime),
                                      timeFormat(endTime)) + \
                                      textStr + "\r\n",
    
                # next word
                textStr = "{\kf%d}" + words[4][1:]
                startTime = -1
            else:
                textStr += words[4]
        else:
            # it's a space, gets lost by the split
            dur = durat(time, startWordTime)
            textStr = textStr % (dur) + " {\kf%d}"
            startWordTime = time
    
        endTime = time
    
    printPreface()
    # print "Dialogue: 0,0:00:18.22,0:00:19.94,Default,,0000,0000,0000,,{\k16}Here {\k46}comes {\k43}the {\k67}sun"
    
    for line in fileinput.input():
        words = line.split()
    
        if len(words)  >= 2:
            if words[0] == "Resolution:":
                ticksPerBeat = words[1]
            elif words[0] == "Length:":
                numTicks = int(words[1])
            elif words[0] == "Duration:":
                duration = int(words[1])
                microSecondsPerTick = duration/numTicks
                # print "Duration %d numTicks %d microSecondsPerTick %d" % (duration, numTicks, microSecondsPerTick)
    
        if len(words) >= 3 and words[2] == "Text":
            doLyric(words)
    
    

    下面是一个例子:

    python lyric2karaokeass4kar.py song.dump > song.ass
    
    
  4. 对于多行歌词显示,使用下面的 Python 脚本提取歌词并以 ASS 格式保存:

     #!/usr/bin/python
    
    import fileinput
    import string
    import math
    
    START_EVENTS = ["Dialogue: 0,%s,%s,Default,,0000,0000,0000,,",
                    "Dialogue: 0,%s,%s,Default,,0000,0000,0100,,"]
    
    TEXT_STR = "Dialogue: 0,%s,%s,Default,,0000,0000,0000,,"
    TEXT_STR2 = "Dialogue: 0,%s,%s,Default,,0000,0000,0100,,"
    
    textStr = "{\kf%d}"
    plainTextStr = ""
    startTime = -1
    previousStartTime = -1
    startWordTime = -1
    endTime = -1
    previousEndTime = -1
    lineNum = 0
    
    def printPreface():
        print '[Script Info]\r\n\
    ; Script generated by Aegisub 2.1.9\r\n\
    ; http://www.aegisub.org/\r\n\
    Title: Default Aegisub file\r\n\
    ScriptType: v4.00+\r\n\
    WrapStyle: 0\r\n\
    PlayResX: 640\r\n\
    PlayResY: 480\r\n\
    ScaledBorderAndShadow: yes\r\n\
    Video Aspect Ratio: 0\r\n\
    Video Zoom: 6\r\n\
    Video Position: 0\r\n\
    \r\n\
    [V4+ Styles]\r\n\
    Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n\
    Style: Default,Arial,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\r\n\
    \r\n\
    [Events]\r\n\
    Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
    
    def timeFormat(s):
        global microSecondsPerTick
    
        tf = float(s)
        # print "factori is %f instead of %f" % ((1.0*microSecondsPerTick / 1000000), (1.0/62.9))
        # frames per sec should be 60: 120 beats/min, 30 ticks per beat
        # but it is too slow on 54154
        tf /= 62.6  #ticks per sec
    
        # This should be right , but is too slow
        # tf = (tf * microSecondsPerTick) / 1000000
    
        t = int(math.floor(tf))
        hundredths = round((tf-t)*100)
        secs = t % 60
        t /= 60#!/usr/bin/python
    
    import fileinput
    import string
    import math
    
    START_EVENTS = ["Dialogue: 0,%s,%s,Default,,0000,0000,0000,,",
                    "Dialogue: 0,%s,%s,Default,,0000,0000,0100,,"]
    
    TEXT_STR = "Dialogue: 0,%s,%s,Default,,0000,0000,0000,,"
    TEXT_STR2 = "Dialogue: 0,%s,%s,Default,,0000,0000,0100,,"
    
    textStr = "{\kf%d}"
    plainTextStr = ""
    startTime = -1
    previousStartTime = -1
    startWordTime = -1
    endTime = -1
    previousEndTime = -1
    lineNum = 0
    
    def printPreface():
        print '[Script Info]\r\n\
    ; Script generated by Aegisub 2.1.9\r\n\
    ; http://www.aegisub.org/\r\n\
    Title: Default Aegisub file\r\n\
    ScriptType: v4.00+\r\n\
    WrapStyle: 0\r\n\
    PlayResX: 640\r\n\
    PlayResY: 480\r\n\
    ScaledBorderAndShadow: yes\r\n\
    Video Aspect Ratio: 0\r\n\
    Video Zoom: 6\r\n\
    Video Position: 0\r\n\
    \r\n\
    [V4+ Styles]\r\n\
    Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n\
    Style: Default,Arial,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\r\n\
    \r\n\
    [Events]\r\n\
    Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
    
    def timeFormat(s):
        global microSecondsPerTick
    
        tf = float(s)
        # print "factori is %f instead of %f" % ((1.0*microSecondsPerTick / 1000000), (1.0/62.9))
        # frames per sec should be 60: 120 beats/min, 30 ticks per beat
        # but it is too slow on 54154
        tf /= 62.6  #ticks per sec
    
        # This should be right , but is too slow
        # tf = (tf * microSecondsPerTick) / 1000000
    
        t = int(math.floor(tf))
        hundredths = round((tf-t)*100)
        secs = t % 60
        t /= 60
        mins = t % 60
        t /= 60
        hrs = t
        return "%01d:%02d:%02d.%02d" % (hrs, mins, secs, hundredths)
    
    def durat(end, start):
        fend = float(end)
        fstart = float(start)
        d = (fend - fstart) / 62.9
        #print end, start, d
        return round(d*100)
    
    def doLyric(words):
        global textStr
        global plainTextStr
        global startTime
        global endTime
        global previousStartTime
        global previousEndTime
        global TEXT_STR
        global startWordTime
        global lineNum
    
        if words[1] == "0:":
            #print "skipping"
            return
    
        time = string.rstrip(words[1], ':')
        if startTime == -1:
            startTime = time
            startWordTime = time
            previousEndTime = time
        #print words[1],
        if len(words) == 5:
            if words[4][0] == '\\' or words[4][0] == '/':
                #print "My name is %s and weight is %d kg!" % ('Zara', 21)
                #print startTime, endTime
                dur = durat(time, startWordTime)
                textStr = textStr % (dur)
    
                if len(words[4]) == 1:
    
                    if previousStartTime != -1:
                        print START_EVENTS[lineNum % 2] % (timeFormat(previousStartTime),
                                                           timeFormat(previousEndTime)) + \
                                                           plainTextStr + "\r\n",
                    print START_EVENTS[lineNum % 2] % (timeFormat(startTime),
                                                       timeFormat(endTime)) + \
                                                       textStr + "\r\n",
    
                # next word
                lineNum += 1
                #previousEndTime = time
                textStr = "{\kf%d}" + words[4][1:]
                plainTextStr = words[4][1:]
                previousStartTime = startTime
                startTime = -1
            else:
                textStr += words[4]
                plainTextStr += words[4]
        else:
            #print textStr
            #dur = duration(time, startWordTime)
            dur = durat(time, startWordTime)
            textStr = textStr % (dur) + " {\kf%d}"
            plainTextStr += ' '
            startWordTime = time
    
        endTime = time
    
    printPreface()
    # print "Dialogue: 0,0:00:18.22,0:00:19.94,Default,,0000,0000,0000,,{\k16}Here {\k46}comes {\k43}the {\k67}sun"
    
    for line in fileinput.input():
        words = line.split()
    
        if len(words)  >= 2:
            if words[0] == "Resolution:":
                ticksPerBeat = words[1]
            elif words[0] == "Length:":
                numTicks = int(words[1])
            elif words[0] == "Duration:":
                duration = int(words[1])
                microSecondsPerTick = duration/numTicks
                # print "Duration %d numTicks %d microSecondsPerTick %d" % (duration, numTicks, microSecondsPerTick)
    
        if len(words) >= 3 and words[2] == "Text":
            doLyric(words)
        mins = t % 60
        t /= 60
        hrs = t
        return "%01d:%02d:%02d.%02d" % (hrs, mins, secs, hundredths)
    
    def durat(end, start):
        fend = float(end)
        fstart = float(start)
        d = (fend - fstart) / 62.9
        #print end, start, d
        return round(d*100)
    
    def doLyric(words):
        global textStr
        global plainTextStr
        global startTime
        global endTime
        global previousStartTime
        global previousEndTime
        global TEXT_STR
        global startWordTime
        global lineNum
    
        if words[1] == "0:":
            #print "skipping"
            return
    
        time = string.rstrip(words[1], ':')
        if startTime == -1:
            startTime = time
            startWordTime = time
            previousEndTime = time
        #print words[1],
        if len(words) == 5:
            if words[4][0] == '\\' or words[4][0] == '/':
                #print "My name is %s and weight is %d kg!" % ('Zara', 21)
                #print startTime, endTime
                dur = durat(time, startWordTime)
                textStr = textStr % (dur)
    
                if len(words[4]) == 1:
    
                    if previousStartTime != -1:
                        print START_EVENTS[lineNum % 2] % (timeFormat(previousStartTime),
                                                           timeFormat(previousEndTime)) + \
                                                           plainTextStr + "\r\n",
                    print START_EVENTS[lineNum % 2] % (timeFormat(startTime),
                                                       timeFormat(endTime)) + \
                                                       textStr + "\r\n",
    
                # next word
                lineNum += 1
                #previousEndTime = time
                textStr = "{\kf%d}" + words[4][1:]
                plainTextStr = words[4][1:]
                previousStartTime = startTime
                startTime = -1
            else:
                textStr += words[4]
                plainTextStr += words[4]
        else:
            #print textStr
            #dur = duration(time, startWordTime)
            dur = durat(time, startWordTime)
            textStr = textStr % (dur) + " {\kf%d}"
            plainTextStr += ' '
            startWordTime = time
    
        endTime = time
    
    printPreface()
    # print "Dialogue: 0,0:00:18.22,0:00:19.94,Default,,0000,0000,0000,,{\k16}Here {\k46}comes {\k43}the {\k67}sun"
    
    for line in fileinput.input():
        words = line.split()
    
        if len(words)  >= 2:
            if words[0] == "Resolution:":
                ticksPerBeat = words[1]
            elif words[0] == "Length:":
                numTicks = int(words[1])
            elif words[0] == "Duration:":
                duration = int(words[1])
                microSecondsPerTick = duration/numTicks
                # print "Duration %d numTicks %d microSecondsPerTick %d" % (duration, numTicks, microSecondsPerTick)
    
        if len(words) >= 3 and words[2] == "Text":
            doLyric(words)
    
    

    下面是一个例子:

    python lyric2karaokeass4kar.py song.dump > song.ass
    
    
  5. 使用fluidsynth将 MIDI 声音文件转换成 WAV 文件。

    fluidsynth -F song.wav /usr/share/sounds/sf2/FluidR3_GM.sf2 song.kar
    
    
  6. 将 WAV 文件转换成 MP3。

    lame song.wav song.mp3
    
    
  7. 为你的背景找一个合适的纯视频文件(我用了我的 Karaoke 光盘中的一个),然后把它们合并成一个 MKV 文件。

    mkvmerge -o 54154.mkv 54154.mp3 54154.ass BACK01.MPG
    
    

生成的 MKV 文件可以作为独立文件由 MPlayer 播放。

mplayer song.mkv

它也可以由 VLC 演奏,但只有在屁股文件存在的情况下。

vlc song.mkv

根据所选的 Karaoke 效果,屏幕截图在本章的前面已经显示过了。

然而,时机是个问题。默认的 MIDI 速度是每分钟 120 拍,常见的节拍率是每拍 30 拍。这导致每秒 60 个 MIDI 滴答声的速率。但是,您现在播放的是 MP3 文件和 ASS 文件,这两个文件都不再是 MIDI 文件,也不一定是同步的。从 MIDI 到 ASS 的转换速度为每秒 60 个节拍,歌词运行太慢。通过实验,我发现至少对于某些文件来说,62.9 是一个合理的比率。

HTML5 字幕

HTML5 支持视频类型,尽管具体哪种浏览器支持哪种视频格式是可变的。这包括使用 HTML 5.1 track 元素支持字幕和隐藏字幕。搜索会找到几篇详细讨论这个问题的文章。

你需要准备一份时间和文字说明文件。示例中显示的格式是一个.vtt文件,如下所示:

WEBVTT

1
00:00:01.000 --> 00:00:30.000  D:vertical A:start
This is the first line of text, displaying from 1-30 seconds

2
00:00:35.000 --> 00:00:50.000
And the second line of text
separated over two lines from 35 to 50 seconds

这里第一行是WEBVTT,文本块由空行分隔。VTT 文件的格式在“Web vtt:Web 视频文本轨迹格式”( http://dev.w3.org/html5/webvtt/ )中指定。

HTML 然后引用音频/视频文件和字幕文件,如下所示:

    <video  controls>
      <source src="output.webm" controls>
      <track src="54154.vtt" kind="subtitles" srclang="en" label="English" default />
      <!-- fallback for rubbish browsers -->
    </video>

图 26-6 显示了一个屏幕截图。

A435426_1_En_26_Fig6_HTML.jpg

图 26-6。

HTML5 subtitles

似乎没有任何机制来逐步突出显示一行中的单词。JavaScript 也许可以做到这一点,但是粗略看了一下,似乎不太可能。这使得它还不适合 Karaoke。

结论

本章讨论了将字幕文本叠加到变化的视频图像上的方法。这是可行的,但只有几种可行的机制。

Footnotes 1

严格来说,字幕指的是说话的内容,而隐藏字幕可能包括其他声音,如关门声。对于 Karaoke 来说,没必要区分。

二十七、Karaoke FluidSynth

FluidSynth 是一个播放 MIDI 文件的应用,也是一个 MIDI 应用库。它没有播放 Karaoke 文件的挂钩。本章讨论了对 FluidSynth 的一个扩展,该扩展添加了适当的挂钩,然后使用这些挂钩来构建各种 Karaoke 系统。

资源

以下是一些资源:

演员

fluidsynth是一个命令行 MIDI 播放器。它在 ALSA 下运行,命令行如下:

fluidsynth -a alsa -l <soundfont> <files...>

播放 MIDI 文件

FluidSynth API 包括以下内容:

  • 使用new_fluid_player创建的音序器
  • 使用new_fluid_synth创建的合成器
  • 一个使用new_fluid_audio_driver创建的音频播放器,在一个单独的线程中运行
  • 一个“设置”对象,可用于控制其他组件的许多功能,用new_fluid_settings创建,用fluid_settings_setstr等调用修改

使用 ALSA 播放 MIDI 文件序列的典型程序如下。它创建各种对象,设置音频播放器使用 ALSA,然后将每个声音字体和 MIDI 文件添加到播放器中。然后对fluid_player_play的调用依次播放每个 MIDI 文件。该程序只是第二十章中所示程序的重复。

#include <fluidsynth.h>
#include <fluid_midi.h>

int main(int argc, char** argv)
{
    int i;
    fluid_settings_t* settings;
    fluid_synth_t* synth;
    fluid_player_t* player;
    fluid_audio_driver_t* adriver;

    settings = new_fluid_settings();
    fluid_settings_setstr(settings, "audio.driver", "alsa");
    synth = new_fluid_synth(settings);
    player = new_fluid_player(synth);

    adriver = new_fluid_audio_driver(settings, synth);
    /* process command line arguments */
    for (i = 1; i < argc; i++) {
        if (fluid_is_soundfont(argv[i])) {
            fluid_synth_sfload(synth, argv[1], 1);
        } else {
            fluid_player_add(player, argv[i]);
        }
    }
    /* play the midi files, if any */
    fluid_player_play(player);
    /* wait for playback termination */
    fluid_player_join(player);
    /* cleanup */
    delete_fluid_audio_driver(adriver);
    delete_fluid_player(player);
    delete_fluid_synth(synth);
    delete_fluid_settings(settings);
    return 0;
}

用回调扩展 FluidSynth

回调是在应用中注册的函数,当某些事件发生时被调用。要构建 Karaoke 播放器,您需要了解以下内容:

  • 当一个文件被加载时,你可以从中提取所有的歌词,以便在适当的时候显示
  • 当每个元歌词或文本事件作为音序器的输出出现时,您可以看到将要演唱的歌词

第一个非常简单:FluidSynth 有一个函数fluid_player_load可以加载一个文件。您可以更改代码,在该函数中添加一个合适的回调函数,以便访问加载的 MIDI 文件。

从音序器中获取歌词或文本事件并不容易,因为它们本来就不应该出现!MIDI 规范允许在 MIDI 文件中使用这些事件类型,但它们不是连线类型,因此永远不应该从音序器发送到合成器。Java MIDI API 通过对元事件处理程序的带外调用使它们可用。FluidSynth 只是把它们扔掉。

另一方面,FluidSynth 已经有一个回调来处理从音序器发送到合成器的 MIDI 事件。它是函数fluid_synth_handle_midi_event,通过调用fluid_player_set_playback_callback进行设置。您需要做的是首先改变现有的 FluidSynth 代码,以便让歌词和文本事件通过。然后插入一个新的回放回调函数,该回调函数将截取这些事件并对它们进行处理,同时将所有其他事件传递给默认处理程序。默认处理程序无论如何都会忽略任何此类事件,因此不需要对其进行更改。

我给 FluidSynth 添加了一个新函数,fluid_player_set_onload_callback,并添加了适当的代码来传递一些元事件。接下来就是编写一个 onload 回调来遍历来自解析后的输入文件的 MIDI 数据,并编写一个合适的 MIDI 事件回调来处理截获的元事件,同时将其余的传递给默认的处理程序。

这些改变是为了给出一个新的源包fluidsynth-1.1.6-karaoke.tar.bz2。如果你只是想从一个补丁文件开始工作,那就是fluid.patch。补丁已经提交给 FluidSynth 维护人员。

要从这个包开始构建,像平常一样做。

tar jxf fluidsynth-1.1.6-karaoke.tar.bz2
cd fluidsynth-1.1.6
./configure
make clean
make

为了获得 ALSA 的支持,你需要安装libasound2-dev包,就像 Jack 和其他包一样。你可能不会安装很多,所以不要运行make install,否则你会覆盖普通的fluidsynth包,它可能会有更多的特性。

前面修改的程序是karaoke_player.c,只打印歌词行和歌词事件,如下所示:

#include <fluidsynth.h>
#include <fluid_midi.h>

/**
 * This MIDI event callback filters out the TEXT and LYRIC events
 * and passes the rest to the default event handler.
 * Here we just print the text of the event, more
 * complex handling can be done
 */
int event_callback(void *data, fluid_midi_event_t *event) {
    fluid_synth_t* synth = (fluid_synth_t*) data;
    int type = fluid_midi_event_get_type(event);
    int chan = fluid_midi_event_get_channel(event);
    if (synth == NULL) printf("Synth is null\n");
    switch(type) {
    case MIDI_TEXT:
        printf("Callback: Playing text event %s (length %d)\n",
               (char *) event->paramptr, event->param1);
        return  FLUID_OK;

    case MIDI_LYRIC:
        printf("Callback: Playing lyric event %d %s\n",
               event->param1, (char *) event->paramptr);
        return  FLUID_OK;
    }
    return fluid_synth_handle_midi_event( data, event);
}

/**
 * This is called whenever new data is loaded, such as a new file.
 * Here we extract the TEXT and LYRIC events and just print them
 * to stdout. They could e.g. be saved and displayed in a GUI
 * as the events are received by the event callback.
 */
int onload_callback(void *data, fluid_player_t *player) {
    printf("Load callback, tracks %d \n", player->ntracks);
    int n;
    for (n = 0; n < player->ntracks; n++) {
        fluid_track_t *track = player->track[n];
        printf("Track %d\n", n);
        fluid_midi_event_t *event = fluid_track_first_event(track);
        while (event != NULL) {
            switch (event->type) {
            case MIDI_TEXT:
            case MIDI_LYRIC:
                printf("Loaded event %s\n", (char *) event->paramptr);
            }
            event = fluid_track_next_event(track);
        }
    }
    return FLUID_OK;
}

int main(int argc, char** argv)
{
    int i;
    fluid_settings_t* settings;
    fluid_synth_t* synth;
    fluid_player_t* player;
    fluid_audio_driver_t* adriver;
    settings = new_fluid_settings();
    fluid_settings_setstr(settings, "audio.driver", "alsa");
    fluid_settings_setint(settings, "synth.polyphony", 64);
    synth = new_fluid_synth(settings);
    player = new_fluid_player(synth);

    /* Set the MIDI event callback to our own functions rather than the system default */
    fluid_player_set_playback_callback(player, event_callback, synth);

    /* Add an onload callback so we can get information from new data before it plays */
    fluid_player_set_onload_callback(player, onload_callback, NULL);

    adriver = new_fluid_audio_driver(settings, synth);
    /* process command line arguments */
    for (i = 1; i < argc; i++) {
        if (fluid_is_soundfont(argv[i])) {
            fluid_synth_sfload(synth, argv[1], 1);
        } else {
            fluid_player_add(player, argv[i]);
        }
    }
    /* play the midi files, if any */
    fluid_player_play(player);
    /* wait for playback termination */
    fluid_player_join(player);
    /* cleanup */
    delete_fluid_audio_driver(adriver);
    delete_fluid_player(player);
    delete_fluid_synth(synth);
    delete_fluid_settings(settings);
    return 0;
}

假设新的fluidsynth包在一个直接的子目录中,为了编译程序,您需要选择本地的 includes 和 libraries。

gcc -g -I fluidsynth-1.1.6/include/ -I fluidsynth-1.1.6/src/midi/ -I fluidsynth-1.1.6/src/utils/ -c -o karaoke_player.o karaoke_player.c

gcc karaoke_player.o -Lfluidsynth-1.1.6/src/.libs -l fluidsynth -o karaoke_player

要运行该程序,您还需要获取本地库和声音字体文件。

export LD_LIBRARY_PATH=./fluidsynth-1.1.6/src/.libs/
./karaoke_player /usr/share/soundfonts/FluidR3_GM.sf2 54154.mid

典型的KAR文件的输出如下:

Load callback, tracks 1
Track 0
Loaded event #
Loaded event 0
Loaded event 0
Loaded event 0
Loaded event 1
Loaded event

...

Callback: Playing lyric event 2 #
Callback: Playing lyric event 2 0
Callback: Playing lyric event 2 0
Callback: Playing lyric event 2 0
Callback: Playing lyric event 2 1
Callback: Playing lyric event 3

用 Gtk 显示和着色文本

虽然有许多显示 Karaoke 文本的方式,但一种常见的模式是显示两行文本:当前播放行和下一行。当前行逐渐突出显示,完成后由下一行替换。

在第二十五章中你做到了。但是 Java 库还没有完善,而且明显很慢,很笨重。在 Oracle 的 Java 开发计划中,它们似乎处于较低的优先级。因此,在这里您将看到一个替代的 GUI 并利用 FluidSynth 库。我选择 Gtk 库的原因在第十五章中有概述。

第一个任务是在加载文件时建立一个歌词行数组。你正在使用 KAR 格式的文件,这些文件带有关于标题的预先信息,等等,以@为前缀,换行符以\为前缀。

struct _lyric_t {
    gchar *lyric;
    long tick;
};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;
lyric_lines_t lyric_lines;

void build_lyric_lines() {
    int n;
    lyric_t *plyric;
    GString *line = g_string_new("");
    GArray *lines =  g_array_sized_new(FALSE, FALSE, sizeof(GString *), 64);

    lyric_lines.title = NULL;

    for (n = 0; n < lyrics->len; n++) {
        plyric = g_array_index(lyrics, lyric_t *, n);
        gchar *lyric = plyric->lyric;
        int tick = plyric->tick;

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'L')) {
            lyric_lines.language =  lyric + 2;
            continue;
        }

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'T')) {
            if (lyric_lines.title == NULL) {
                lyric_lines.title = lyric + 2;
            } else {
                lyric_lines.performer = lyric + 2;
            }
            continue;
        }

        if (lyric[0] == '@') {
            // some other stuff like @KMIDI KARAOKE FILE
            continue;
        }

        if ((lyric[0] == '/') || (lyric[0] == '\\')) {
            // start of a new line
            // add to lines
            g_array_append_val(lines, line);
            line = g_string_new(lyric + 1);
        }  else {
            line = g_string_append(line, lyric);
        }
    }
    lyric_lines.lines = lines;

    printf("Title is %s, performer is %s, language is %s\n",
           lyric_lines.title, lyric_lines.performer, lyric_lines.language);
    for (n = 0; n < lines->len; n++) {
        printf("Line is %s\n", g_array_index(lines, GString *, n)->str);
    }
}

这是从 onload 回调中调用的。

int onload_callback(void *data, fluid_player_t *player) {
    long ticks = 0L;
    lyric_t *plyric;

    printf("Load callback, tracks %d \n", player->ntracks);
    int n;
    for (n = 0; n < player->ntracks; n++) {
        fluid_track_t *track = player->track[n];
        printf("Track %d\n", n);
        fluid_midi_event_t *event = fluid_track_first_event(track);
        while (event != NULL) {
            switch (fluid_midi_event_get_type (event)) {
            case MIDI_TEXT:
            case MIDI_LYRIC:
                /* there's no fluid_midi_event_get_sysex()
                   or fluid_midi_event_get_time() so we
                   have to look inside the opaque struct
                */
                ticks += event->dtime;
                printf("Loaded event %s for time %d\n",
                       event->paramptr,
                       ticks);
                plyric = g_new(lyric_t, 1);
                plyric->lyric = g_strdup(event->paramptr);
                plyric->tick = ticks;
                g_array_append_val(lyrics, plyric);
            }
            event = fluid_track_next_event(track);
        }
    }

    printf("Saved %d lyric events\n", lyrics->len);
    for (n = 0; n < lyrics->len; n++) {
        plyric = g_array_index(lyrics, lyric_t *, n);
        printf("Saved lyric %s at %d\n", plyric->lyric, plyric->tick);
    }

    build_lyric_lines();
}

标准的 GUI 部分是构建一个由两个标签组成的界面,一个在另一个之上,用来保存歌词行。这只是普通的 Gtk。

最后一部分是处理来自音序器的歌词或文本事件。如果事件是一个\,那么在一个小的停顿之后,标签中的当前文本必须被替换为新的文本。否则,标签中的文本必须逐渐着色,以指示接下来要播放的内容。

在第十五章,我讨论了在 pixbufs 中使用 Cairo 画图,使用 Pango 构造文本。Gtk 标签直接理解 Pango,所以您只需使用 Pango 来格式化文本并将其显示在标签中。这包括构造一个 HTML 字符串,第一部分为红色,其余部分为黑色。这个可以在标签里设置,不需要用 Cairo。

节目是gtkkaraoke_player.c

Warning

当试图在 Gtk 代码中复制 Pango 属性列表以调整标签大小时,下面的程序经常崩溃。调试显示,Pango copy 函数在 Gtk 中的某个地方被设置为NULL,而不应该是这样。我还没有修复方法,也没有用足够简单的方法复制错误来记录错误报告。

#include <fluidsynth.h>
#include <fluid_midi.h>
#include <string.h>

#include <gtk/gtk.h>

/* GString stuff from https://developer.gnome.org/glib/2.31/glib-Strings.html
   Memory alloc from https://developer.gnome.org/glib/2.30/glib-Memory-Allocation.html
   Packing demo from https://developer.gnome.org/gtk-tutorial/2.90/x386.html
   Thread stuff from https://developer.gnome.org/gtk-faq/stable/x481.html
   GArrays from http://www.gtk.org/api/2.6/glib/glib-Arrays.html
   Pango attributes from http://www.ibm.com/developerworks/library/l-u-pango2/
   Timeouts at http://www.gtk.org/tutorial1.2/gtk_tut-17.html
 */

struct _lyric_t {
    gchar *lyric;
    long tick;

};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;

lyric_lines_t lyric_lines;

fluid_synth_t* synth;

GtkWidget *lyric_labels[2];

fluid_player_t* player;

int current_panel = -1;  // panel showing current lyric line
int current_line = 0;  // which line is the current lyric
gchar *current_lyric;   // currently playing lyric line
GString *front_of_lyric;  // part of lyric to be coloured red
GString *end_of_lyric;    // part of lyric to not be coloured

gchar *markup[] = {"<span foreground=\"red\">",
                   "</span><span foreground=\"black\">",
                   "</span>"};
gchar *markup_newline[] = {"<span foreground=\"black\">",
                   "</span>"};
GString *marked_up_label;

struct _reset_label_data {
    GtkLabel *label;
    gchar *text;
    PangoAttrList *attrs;
};

typedef struct _reset_label_data reset_label_data;

/**
 * redraw a label some time later
 */
gint reset_label_cb(gpointer data) {
    reset_label_data *rdata = ( reset_label_data *) data;

    if (rdata->label == NULL) {
        printf("Label is null, cant set its text \n");
        return FALSE;
    }

    printf("Resetting label callback to \"%s\"\n", rdata->text);

    gdk_threads_enter();

    gchar *str;
    str = g_strconcat(markup_newline[0], rdata->text, markup_newline[1], NULL);

    PangoAttrList *attrs;
    gchar *text;
    pango_parse_markup (str, -1,0, &attrs, &text, NULL, NULL);

    gtk_label_set_text(rdata->label, text);
    gtk_label_set_attributes(rdata->label, attrs);

    gdk_threads_leave();

    GtkAllocation* alloc = g_new(GtkAllocation, 1);
    gtk_widget_get_allocation((GtkWidget *) (rdata->label), alloc);
    printf("Set label text to \"%s\"\n", gtk_label_get_text(rdata->label));
    printf("Label has height %d width %d\n", alloc->height, alloc->width);
    printf("Set other label text to \"%s\"\n",
           gtk_label_get_text(rdata->label == lyric_labels[0] ?
                              lyric_labels[1] : lyric_labels[0]));
    gtk_widget_get_allocation((GtkWidget *) (rdata->label  == lyric_labels[0] ?
                              lyric_labels[1] : lyric_labels[0]), alloc);
    printf("Label has height %d width %d\n", alloc->height, alloc->width);

    return FALSE;
}

/**
 * This MIDI event callback filters out the TEXT and LYRIC events
 * and passes the rest to the default event handler.
 * Here we colour the text in a Gtk label
 */
int event_callback(void *data, fluid_midi_event_t *event) {
    fluid_synth_t* synth = (fluid_synth_t*) data;
    int type = fluid_midi_event_get_type(event);
    int chan = fluid_midi_event_get_channel(event);
    if (synth == NULL) printf("Synth is null\n");
    switch(type) {
    case MIDI_TEXT:
        printf("Callback: Playing text event %s (length %d)\n",
               (char *) event->paramptr, event->param1);

        if (((char *) event->paramptr)[0] == '\\') {
            // we've got a new line, change the label text on the NEXT panel
            int next_panel = current_panel; // really (current_panel+2)%2
            int next_line = current_line + 2;
            gchar *next_lyric;

            if (current_line + 2 >= lyric_lines.lines->len) {
                return FLUID_OK;
            }
            current_line += 1;
            current_panel = (current_panel + 1) % 2;

            // set up new line as current line
            char *lyric =  event->paramptr;

            // find the next line from lyric_lines array
            current_lyric = g_array_index(lyric_lines.lines, GString *, current_line)->str;

            // lyric is in 2 parts: front coloured, end uncoloured
            front_of_lyric = g_string_new(lyric+1); // lose \
            end_of_lyric = g_string_new(current_lyric);
            printf("New line. Setting front to %s end to \"%s\"\n", lyric+1, current_lyric);

            // update label for next line after this one
            char *str = g_array_index(lyric_lines.lines, GString *, next_line)->str;
            printf("Setting text in label %d to \"%s\"\n", next_panel, str);

            next_lyric = g_array_index(lyric_lines.lines, GString *, next_line)->str;

            gdk_threads_enter();

            // change the label after one second to avoid visual "jar"
            reset_label_data *label_data;
            label_data = g_new(reset_label_data, 1);
            label_data->label = (GtkLabel *) lyric_labels[next_panel];
            label_data->text = next_lyric;
            g_timeout_add(1000, reset_label_cb, label_data);

            // Dies if you try to flush at this point!
            // gdk_flush();

            gdk_threads_leave();
        } else {
            // change text colour as chars are played, using Pango attributes
            char *lyric =  event->paramptr;
            if ((front_of_lyric != NULL) && (lyric != NULL)) {
                // add the new lyric to the front of the existing coloured
                g_string_append(front_of_lyric, lyric);
                char *s = front_of_lyric->str;
                printf("Displaying \"%s\"\n", current_lyric);
                printf("  Colouring \"%s\"\n", s);
                printf("  Not colouring \"%s\"\n", current_lyric + strlen(s));

                // todo: avoid memory leak
                marked_up_label = g_string_new(markup[0]);
                g_string_append(marked_up_label, s);
                g_string_append(marked_up_label, markup[1]);
                g_string_append(marked_up_label, current_lyric + strlen(s));
                g_string_append(marked_up_label, markup[2]);
                printf("Marked up label \"%s\"\n", marked_up_label->str);

                /* Example from http://www.ibm.com/developerworks/library/l-u-pango2/
                 */
                PangoAttrList *attrs;
                gchar *text;
                gdk_threads_enter();
                pango_parse_markup (marked_up_label->str, -1,0, &attrs, &text, NULL, NULL);
                printf("Marked up label parsed ok\n");
                gtk_label_set_text((GtkLabel *) lyric_labels[current_panel],
                                   text);
                gtk_label_set_attributes(GTK_LABEL(lyric_labels[current_panel]), attrs);
                // Dies if you try to flush at this point!
                //gdk_flush();

                gdk_threads_leave();
            }
        }
        return  FLUID_OK;

    case MIDI_LYRIC:
        printf("Callback: Playing lyric event %d %s\n",
               event->param1, (char *) event->paramptr);
        return  FLUID_OK;

    case MIDI_EOT:
        printf("End of track\n");
        exit(0);
    }
    // default handler for all other events
    return fluid_synth_handle_midi_event( data, event);
}

/*
 * Build array of lyric lines from the MIDI file data
 */
void build_lyric_lines() {
    int n;
    lyric_t *plyric;
    GString *line = g_string_new("");
    GArray *lines =  g_array_sized_new(FALSE, FALSE, sizeof(GString *), 64);

    lyric_lines.title = NULL;

    for (n = 0; n < lyrics->len; n++) {
        plyric = g_array_index(lyrics, lyric_t *, n);
        gchar *lyric = plyric->lyric;
        int tick = plyric->tick;

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'L')) {
            lyric_lines.language =  lyric + 2;
            continue;
        }

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'T')) {
            if (lyric_lines.title == NULL) {
                lyric_lines.title = lyric + 2;
            } else {
                lyric_lines.performer = lyric + 2;
            }
            continue;
        }

        if (lyric[0] == '@') {
            // some other stuff like @KMIDI KARAOKE FILE
            continue;
        }

        if ((lyric[0] == '/') || (lyric[0] == '\\')) {
            // start of a new line
            // add to lines
            g_array_append_val(lines, line);
            line = g_string_new(lyric + 1);
        }  else {
            line = g_string_append(line, lyric);
        }
    }
    lyric_lines.lines = lines;

    printf("Title is %s, performer is %s, language is %s\n",
           lyric_lines.title, lyric_lines.performer, lyric_lines.language);
    for (n = 0; n < lines->len; n++) {
        printf("Line is %s\n", g_array_index(lines, GString *, n)->str);
    }

}

/**
 * This is called whenever new data is loaded, such as a new file.
 * Here we extract the TEXT and LYRIC events and save them
 * into an array
 */
int onload_callback(void *data, fluid_player_t *player) {
    long ticks = 0L;
    lyric_t *plyric;

    printf("Load callback, tracks %d \n", player->ntracks);
    int n;
    for (n = 0; n < player->ntracks; n++) {
        fluid_track_t *track = player->track[n];
        printf("Track %d\n", n);
        fluid_midi_event_t *event = fluid_track_first_event(track);
        while (event != NULL) {
            switch (fluid_midi_event_get_type (event)) {
            case MIDI_TEXT:
            case MIDI_LYRIC:
                /* there's no fluid_midi_event_get_sysex()
                   or fluid_midi_event_get_time() so we
                   have to look inside the opaque struct
                */
                ticks += event->dtime;
                printf("Loaded event %s for time %ld\n",
                       (char *) event->paramptr,
                       ticks);
                plyric = g_new(lyric_t, 1);
                plyric->lyric = g_strdup(event->paramptr);
                plyric->tick = ticks;
                g_array_append_val(lyrics, plyric);
            }
            event = fluid_track_next_event(track);
        }
    }

    printf("Saved %d lyric events\n", lyrics->len);
    for (n = 0; n < lyrics->len; n++) {
        plyric = g_array_index(lyrics, lyric_t *, n);
        printf("Saved lyric %s at %ld\n", plyric->lyric, plyric->tick);
    }

    build_lyric_lines();

    // stick the first two lines into the labels so we can see
    // what is coming
    gdk_threads_enter();
    char *str = g_array_index(lyric_lines.lines, GString *, 1)->str;
    gtk_label_set_text((GtkLabel *) lyric_labels[0], str);
    str = g_array_index(lyric_lines.lines, GString *, 2)->str;
    gtk_label_set_text((GtkLabel *) lyric_labels[1], str);
    // gdk_flush ();

    /* release GTK thread lock */
    gdk_threads_leave();

    return FLUID_OK;
}

/* Called when the windows are realized
 */
static void realize_cb (GtkWidget *widget, gpointer data) {
    /* now we can play the midi files, if any */
    fluid_player_play(player);
}

static gboolean delete_event( GtkWidget *widget,
                              GdkEvent  *event,
                              gpointer   data )
{
    /* If you return FALSE in the "delete-event" signal handler,
     * GTK will emit the "destroy" signal. Returning TRUE means
     * you don't want the window to be destroyed.
     * This is useful for popping up 'are you sure you want to quit?'
     * type dialogs. */

    g_print ("delete event occurred\n");

    /* Change TRUE to FALSE and the main window will be destroyed with
     * a "delete-event". */

    return TRUE;
}

/* Another callback */
static void destroy( GtkWidget *widget,
                     gpointer   data )
{
    gtk_main_quit ();
}

int main(int argc, char** argv)
{

    /* set up the fluidsynth stuff */
    int i;
    fluid_settings_t* settings;

    fluid_audio_driver_t* adriver;
    settings = new_fluid_settings();
    fluid_settings_setstr(settings, "audio.driver", "alsa");
    fluid_settings_setint(settings, "synth.polyphony", 64);
    fluid_settings_setint(settings, "synth.reverb.active", FALSE);
    fluid_settings_setint(settings, "synth.sample-rate", 22050);
    synth = new_fluid_synth(settings);
    player = new_fluid_player(synth);

    lyrics = g_array_sized_new(FALSE, FALSE, sizeof(lyric_t *), 1024);

    /* Set the MIDI event callback to our own functions rather than the system default */
    fluid_player_set_playback_callback(player, event_callback, synth);

    /* Add an onload callback so we can get information from new data before it plays */
    fluid_player_set_onload_callback(player, onload_callback, NULL);

    adriver = new_fluid_audio_driver(settings, synth);
    /* process command line arguments */
    for (i = 1; i < argc; i++) {
        if (fluid_is_soundfont(argv[i])) {
            fluid_synth_sfload(synth, argv[1], 1);
        } else {
            fluid_player_add(player, argv[i]);
        }
    }

    // Gtk stuff now

   /* GtkWidget is the storage type for widgets */
    GtkWidget *window;
    GtkWidget *button;
    GtkWidget *lyrics_box;

    /* This is called in all GTK applications. Arguments are parsed
     * from the command line and are returned to the application. */
    gtk_init (&argc, &argv);

    /* create a new window */
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

    /* When the window is given the "delete-event" signal (this is given
     * by the window manager, usually by the "close" option, or on the
     * titlebar), we ask it to call the delete_event () function
     * as defined above. The data passed to the callback
     * function is NULL and is ignored in the callback function. */
    g_signal_connect (window, "delete-event",
                      G_CALLBACK (delete_event), NULL);

    /* Here we connect the "destroy" event to a signal handler.
     * This event occurs when we call gtk_widget_destroy() on the window,
     * or if we return FALSE in the "delete-event" callback. */
    g_signal_connect (window, "destroy",
                      G_CALLBACK (destroy), NULL);

    g_signal_connect (window, "realize", G_CALLBACK (realize_cb), NULL);

    /* Sets the border width of the window. */
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);

    // Gtk 3.0 deprecates gtk_vbox_new in favour of gtk_grid
    // but that isn't in Gtk 2.0, so we ignore warnings for now
    lyrics_box = gtk_vbox_new(TRUE, 1);
    gtk_widget_show(lyrics_box);

    char *str = "  ";
    lyric_labels[0] = gtk_label_new(str);
    lyric_labels[1] = gtk_label_new(str);

    gtk_widget_show (lyric_labels[0]);
    gtk_widget_show (lyric_labels[1]);

    gtk_box_pack_start (GTK_BOX (lyrics_box), lyric_labels[0], TRUE, TRUE, 0);
    gtk_box_pack_start (GTK_BOX (lyrics_box), lyric_labels[1], TRUE, TRUE, 0);

    /* This packs the button into the window (a gtk container). */
    gtk_container_add (GTK_CONTAINER (window), lyrics_box);

    /* and the window */
    gtk_widget_show (window);

    /* All GTK applications must have a gtk_main(). Control ends here
     * and waits for an event to occur (like a key press or
     * mouse event). */
    gtk_main ();

    /* wait for playback termination */
    fluid_player_join(player);
    /* cleanup */
    delete_fluid_audio_driver(adriver);
    delete_fluid_player(player);
    delete_fluid_synth(synth);
    delete_fluid_settings(settings);
    return 0;
}

运行时,如图 27-1 所示。

A435426_1_En_27_Fig1_HTML.jpg

图 27-1。

Caption

用 Gtk 播放背景视频

第十五章展示了如何播放背景视频,包括图像(使用 pixbufs)、文本(使用 Cairo)和彩色文本(使用 Pango)。您可以通过添加动态文本显示来扩展这一功能,以便播放 Karaoke。

您可以在一个结构中捕获每一行歌词,该结构保留整行、已经唱过的部分、该行的 Pango 标记和 Pango 属性。

typedef struct _coloured_line_t {
    gchar *line;
    gchar *front_of_line;
    gchar *marked_up_line;
    PangoAttrList *attrs;
} coloured_line_t;

每次 MIDI 歌词事件发生时,都会在监听 FluidSynth 音序器的线程中进行更新。

一个单独的线程播放视频,并在每一帧上用当前和下一个歌词覆盖帧图像。这被设置到一个GdkImage中,由 Gtk 显示。

节目是gtkkaraoke_player_video_pango.c

#include <fluidsynth.h>
#include <fluid_midi.h>
#include <string.h>

#include <gtk/gtk.h>

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

// saving as pixbufs leaks memory
//#define USE_PIXBUF

/* run by
   gtkkaraoke_player_video_pango /usr/share/sounds/sf2/FluidR3_GM.sf2 /home/newmarch/Music/karaoke/sonken/songs/54154.kar
*/

/*
 * APIs:
 * GString: https://developer.gnome.org/glib/2.28/glib-Strings.html
 * Pango text attributes: https://developer.gnome.org/pango/stable/pango-Text-Attributes.html#pango-parse-markup
 * Pango layout: http://www.gtk.org/api/2.6/pango/pango-Layout-Objects.html
 * Cairo rendering: https://developer.gnome.org/pango/stable/pango-Cairo-Rendering.html#pango-cairo-create-layout
 * Cairo surface_t: http://cairographics.org/manual/cairo-cairo-surface-t.html
 * GTK+ 3 Reference Manual: https://developer.gnome.org/gtk3/3.0/
 * Gdk Pixbufs: https://developer.gnome.org/gdk/stable/gdk-Pixbufs.html
 */

struct _lyric_t {
    gchar *lyric;
    long tick;

};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;

lyric_lines_t lyric_lines;

typedef struct _coloured_line_t {
    gchar *line;
    gchar *front_of_line;
    gchar *marked_up_line;
    PangoAttrList *attrs;
#ifdef USE_PIXBUF
    GdkPixbuf *pixbuf;
#endif
} coloured_line_t;

coloured_line_t coloured_lines[2];

fluid_synth_t* synth;

GtkWidget *image;
#if GTK_MAJOR_VERSION == 2
GdkPixmap *dbuf_pixmap;
#endif

int height_lyric_pixbufs[] = {300, 400}; // vertical offset of lyric in video

fluid_player_t* player;

int current_panel = 1;  // panel showing current lyric line
int current_line = 0;  // which line is the current lyric
gchar *current_lyric;   // currently playing lyric line
GString *front_of_lyric;  // part of lyric to be coloured red
//GString *end_of_lyric;    // part of lyrci to not be coloured

// Colours seem to get mixed up when putting a pixbuf onto a pixbuf
#ifdef USE_PIXBUF
#define RED blue
#else
#define RED red
#endif

gchar *markup[] = {"<span font=\"28\" foreground=\"RED\">",
                   "</span><span font=\"28\" foreground=\"white\">",
                   "</span>"};
gchar *markup_newline[] = {"<span foreground=\"black\">",
                           "</span>"};
GString *marked_up_label;

/* FFMpeg vbls */
AVFormatContext *pFormatCtx = NULL;
AVCodecContext *pCodecCtx = NULL;
int videoStream;
struct SwsContext *sws_ctx = NULL;
AVCodec *pCodec = NULL;

void markup_line(coloured_line_t *line) {
    GString *str =  g_string_new(markup[0]);
    g_string_append(str, line->front_of_line);
    g_string_append(str, markup[1]);
    g_string_append(str, line->line + strlen(line->front_of_line));
    g_string_append(str, markup[2]);
    printf("Marked up label \"%s\"\n", str->str);

    line->marked_up_line = str->str;
    // we have to free line->marked_up_line

    pango_parse_markup(str->str, -1,0, &(line->attrs), NULL, NULL, NULL);
    g_string_free(str, FALSE);
}

#ifdef USE_PIXBUF
void update_line_pixbuf(coloured_line_t *line) {
    //return;
    cairo_surface_t *surface;
    cairo_t *cr;

    int lyric_width = 480;
    int lyric_height = 60;
    surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                          lyric_width, lyric_height);
    cr = cairo_create (surface);

    PangoLayout *layout;
    PangoFontDescription *desc;

    // draw the attributed text
    layout = pango_cairo_create_layout (cr);
    pango_layout_set_text (layout, line->line, -1);
    pango_layout_set_attributes(layout, line->attrs);

    // centre the image in the surface
    int width, height;
    pango_layout_get_pixel_size(layout,
                                &width,
                                &height);
    cairo_move_to(cr, (lyric_width-width)/2, 0);

    pango_cairo_update_layout (cr, layout);
    pango_cairo_show_layout (cr, layout);

    // pull the pixbuf out of the surface
    unsigned char *data = cairo_image_surface_get_data(surface);
    width = cairo_image_surface_get_width(surface);
    height = cairo_image_surface_get_height(surface);
    int stride = cairo_image_surface_get_stride(surface);
    printf("Text surface width %d height %d stride %d\n", width, height, stride);

    GdkPixbuf *old_pixbuf = line->pixbuf;
    line->pixbuf = gdk_pixbuf_new_from_data(data, GDK_COLORSPACE_RGB, 1, 8, width, height, stride, NULL, NULL);
    cairo_surface_destroy(surface);
    g_object_unref(old_pixbuf);
}
#endif

/**
 * This MIDI event callback filters out the TEXT and LYRIC events
 * and passes the rest to the default event handler.
  */
int event_callback(void *data, fluid_midi_event_t *event) {
    fluid_synth_t* synth = (fluid_synth_t*) data;
    int type = fluid_midi_event_get_type(event);
    int chan = fluid_midi_event_get_channel(event);
    if (synth == NULL) printf("Synth is null\n");

    //return 0;

    switch(type) {
    case MIDI_TEXT:
        printf("Callback: Playing text event %s (length %d)\n",
               (char *) event->paramptr, (int) event->param1);

        if (((char *) event->paramptr)[0] == '\\') {
            int next_panel = current_panel; // really (current_panel+2)%2
            int next_line = current_line + 2;
            gchar *next_lyric;

            if (current_line + 2 >= lyric_lines.lines->len) {
                return FLUID_OK;
            }
            current_line += 1;
            current_panel = (current_panel + 1) % 2;

            // set up new line as current line
            char *lyric =  event->paramptr;
            current_lyric = g_array_index(lyric_lines.lines, GString *, current_line)->str;
            front_of_lyric = g_string_new(lyric+1); // lose \
            printf("New line. Setting front to %s end to \"%s\"\n", lyric+1, current_lyric);

            coloured_lines[current_panel].line = current_lyric;
            coloured_lines[current_panel].front_of_line = lyric+1;
            markup_line(coloured_lines+current_panel);
#ifdef USE_PIXBUF
            update_line_pixbuf(coloured_lines+current_panel);
#endif
            // update label for next line after this one
            next_lyric = g_array_index(lyric_lines.lines, GString *, next_line)->str;

            marked_up_label = g_string_new(markup_newline[0]);

            g_string_append(marked_up_label, next_lyric);
            g_string_append(marked_up_label, markup_newline[1]);
            PangoAttrList *attrs;
            gchar *text;
            pango_parse_markup (marked_up_label->str, -1,0, &attrs, &text, NULL, NULL);

            coloured_lines[next_panel].line = next_lyric;
            coloured_lines[next_panel].front_of_line = "";
            markup_line(coloured_lines+next_panel);
#ifdef USE_PIXBUF
            update_line_pixbuf(coloured_lines+next_panel);
#endif
        } else {
            // change text colour as chars are played
            char *lyric =  event->paramptr;
            if ((front_of_lyric != NULL) && (lyric != NULL)) {
                g_string_append(front_of_lyric, lyric);
                char *s = front_of_lyric->str;
                coloured_lines[current_panel].front_of_line = s;
                markup_line(coloured_lines+current_panel);
#ifdef USE_PIXBUF
                update_line_pixbuf(coloured_lines+current_panel);
#endif
            }
        }
        return  FLUID_OK;

    case MIDI_LYRIC:
        printf("Callback: Playing lyric event %d %s\n", (int) event->param1, (char *) event->paramptr);
        return  FLUID_OK;

    case MIDI_EOT:
        printf("End of track\n");
        exit(0);
    }
    return fluid_synth_handle_midi_event( data, event);
}

void build_lyric_lines() {
    int n;
    lyric_t *plyric;
    GString *line = g_string_new("");
    GArray *lines =  g_array_sized_new(FALSE, FALSE, sizeof(GString *), 64);

    lyric_lines.title = NULL;

    for (n = 0; n < lyrics->len; n++) {
        plyric = g_array_index(lyrics, lyric_t *, n);
        gchar *lyric = plyric->lyric;
        int tick = plyric->tick;

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'L')) {
            lyric_lines.language =  lyric + 2;
            continue;
        }

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'T')) {
            if (lyric_lines.title == NULL) {
                lyric_lines.title = lyric + 2;
            } else {
                lyric_lines.performer = lyric + 2;
            }
            continue;
        }

        if (lyric[0] == '@') {
            // some other stuff like @KMIDI KARAOKE FILE
            continue;
        }

        if ((lyric[0] == '/') || (lyric[0] == '\\')) {
            // start of a new line
            // add to lines
            g_array_append_val(lines, line);
            line = g_string_new(lyric + 1);
        }  else {
            line = g_string_append(line, lyric);
        }
    }
    lyric_lines.lines = lines;

    printf("Title is %s, performer is %s, language is %s\n",
           lyric_lines.title, lyric_lines.performer, lyric_lines.language);
    for (n = 0; n < lines->len; n++) {
        printf("Line is %s\n", g_array_index(lines, GString *, n)->str);
    }

}

/**
 * This is called whenever new data is loaded, such as a new file.
 * Here we extract the TEXT and LYRIC events and just print them
 * to stdout. They could e.g. be saved and displayed in a GUI
 * as the events are received by the event callback.
 */
int onload_callback(void *data, fluid_player_t *player) {
    long ticks = 0L;
    lyric_t *plyric;

    printf("Load callback, tracks %d \n", player->ntracks);
    int n;
    for (n = 0; n < player->ntracks; n++) {
        fluid_track_t *track = player->track[n];
        printf("Track %d\n", n);
        fluid_midi_event_t *event = fluid_track_first_event(track);
        while (event != NULL) {
            switch (fluid_midi_event_get_type (event)) {
            case MIDI_TEXT:
            case MIDI_LYRIC:
                /* there's no fluid_midi_event_get_sysex()
                   or fluid_midi_event_get_time() so we
                   have to look inside the opaque struct
                */
                ticks += event->dtime;
                printf("Loaded event %s for time %ld\n",
                       (char *) event->paramptr,
                       ticks);
                plyric = g_new(lyric_t, 1);
                plyric->lyric = g_strdup(event->paramptr);
                plyric->tick = ticks;
                g_array_append_val(lyrics, plyric);
            }
            event = fluid_track_next_event(track);
        }
    }

    printf("Saved %d lyric events\n", lyrics->len);
    for (n = 0; n < lyrics->len; n++) {
        plyric = g_array_index(lyrics, lyric_t *, n);
        printf("Saved lyric %s at %ld\n", plyric->lyric, plyric->tick);
    }

    build_lyric_lines();

    return FLUID_OK;
}

static void overlay_lyric(cairo_t *cr,
                          coloured_line_t *line,
                          int ht) {
    PangoLayout *layout;
    int height, width;

    if (line->line == NULL) {
        return;
    }

    layout = pango_cairo_create_layout (cr);
    pango_layout_set_text (layout, line->line, -1);
    pango_layout_set_attributes(layout, line->attrs);
    pango_layout_get_pixel_size(layout,
                                &width,
                                &height);
    cairo_move_to(cr, (720-width)/2, ht);

    pango_cairo_update_layout (cr, layout);
    pango_cairo_show_layout (cr, layout);

    g_object_unref(layout);
}

static void pixmap_destroy_notify(guchar *pixels,
                                  gpointer data) {
    printf("Ddestroy pixmap\n");
}

static void *play_background(void *args) {
    /* based on code from
       http://www.cs.dartmouth.edu/∼xy/cs23/gtk.html
       http://cdry.wordpress.com/2009/09/09/using-custom-io-callbacks-with-ffmpeg/
    */

    int i;
    AVPacket packet;
    int frameFinished;
    AVFrame *pFrame = NULL;

    int oldSize;
    char *oldData;
    int bytesDecoded;
    GdkPixbuf *pixbuf;
    AVFrame *picture_RGB;
    char *buffer;

#if GTK_MAJOR_VERSION == 2
    GdkPixmap *pixmap;
    GdkBitmap *mask;
#endif

    pFrame=avcodec_alloc_frame();

    i=0;
    picture_RGB = avcodec_alloc_frame();
    buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, 720, 576));
    avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, 720, 576);

    while(av_read_frame(pFormatCtx, &packet)>=0) {
        if(packet.stream_index==videoStream) {
            //printf("Frame %d\n", i++);
            usleep(33670);  // 29.7 frames per second
            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
                                  &packet);
            int width = pCodecCtx->width;
            int height = pCodecCtx->height;

            sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);

            if (frameFinished) {
                printf("Frame %d\n", i++);

                sws_scale(sws_ctx,  (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, picture_RGB->data, picture_RGB->linesize);

                pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB, 0, 8, 720, 480, picture_RGB->linesize[0], pixmap_destroy_notify, NULL);

                /* start GTK thread lock for drawing */
                gdk_threads_enter();

#define SHOW_LYRIC
#ifdef SHOW_LYRIC
                // Create the destination surface
                cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                                       width, height);
                cairo_t *cr = cairo_create(surface);

                // draw the background image
                gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
                cairo_paint (cr);

#ifdef USE_PIXBUF
                // draw the lyric
                GdkPixbuf *lyric_pixbuf = coloured_lines[current_panel].pixbuf;
                if (lyric_pixbuf != NULL) {
                    int width = gdk_pixbuf_get_width(lyric_pixbuf);
                    gdk_cairo_set_source_pixbuf(cr,
                                                lyric_pixbuf,
                                                (720-width)/2,
                                                 height_lyric_pixbufs[current_panel]);
                    cairo_paint (cr);
                }

                int next_panel = (current_panel+1) % 2;
                lyric_pixbuf = coloured_lines[next_panel].pixbuf;
                if (lyric_pixbuf != NULL) {
                    int width = gdk_pixbuf_get_width(lyric_pixbuf);
                    gdk_cairo_set_source_pixbuf(cr,
                                                lyric_pixbuf,
                                                (720-width)/2,
                                                 height_lyric_pixbufs[next_panel]);
                    cairo_paint (cr);
                }
#else

                overlay_lyric(cr,
                              coloured_lines+current_panel,
                              height_lyric_pixbufs[current_panel]);

                int next_panel = (current_panel+1) % 2;
                overlay_lyric(cr,
                              coloured_lines+next_panel,
                              height_lyric_pixbufs[next_panel]);
#endif
                pixbuf = gdk_pixbuf_get_from_surface(surface,
                                                     0,
                                                     0,
                                                     width,
                                                     height);

                gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf);

                g_object_unref(pixbuf);         /* reclaim memory */
                //g_object_unref(layout);
                cairo_surface_destroy(surface);
                cairo_destroy(cr);
#else
        gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf);
#endif /* SHOW_LYRIC */

                /* release GTK thread lock */
                gdk_threads_leave();
            }
        }
        av_free_packet(&packet);
    }
    sws_freeContext(sws_ctx);

    printf("Video over!\n");
    exit(0);
}

static void *play_midi(void *args) {
    fluid_player_play(player);

    printf("Audio finished\n");
    //exit(0);
}

/* Called when the windows are realized
 */
static void realize_cb (GtkWidget *widget, gpointer data) {
    /* start the video playing in its own thread */
    pthread_t tid;
    pthread_create(&tid, NULL, play_background, NULL);

    /* start the MIDI file playing in its own thread */
    pthread_t tid_midi;
    pthread_create(&tid_midi, NULL, play_midi, NULL);
}

static gboolean delete_event( GtkWidget *widget,
                              GdkEvent  *event,
                              gpointer   data )
{
    /* If you return FALSE in the "delete-event" signal handler,
     * GTK will emit the "destroy" signal. Returning TRUE means
     * you don't want the window to be destroyed.
     * This is useful for popping up 'are you sure you want to quit?'
     * type dialogs. */

    g_print ("delete event occurred\n");

    /* Change TRUE to FALSE and the main window will be destroyed with
     * a "delete-event". */

    return TRUE;
}

/* Another callback */
static void destroy( GtkWidget *widget,
                     gpointer   data )
{
    gtk_main_quit ();
}

int main(int argc, char** argv)
{
    XInitThreads();

    int i;

    fluid_settings_t* settings;

    fluid_audio_driver_t* adriver;
    settings = new_fluid_settings();
    fluid_settings_setstr(settings, "audio.driver", "alsa");
    //fluid_settings_setint(settings, "lash.enable", 0);
    fluid_settings_setint(settings, "synth.polyphony", 64);
    fluid_settings_setint(settings, "synth.reverb.active", FALSE);
    fluid_settings_setint(settings, "synth.sample-rate", 22050);
    synth = new_fluid_synth(settings);
    player = new_fluid_player(synth);

    lyrics = g_array_sized_new(FALSE, FALSE, sizeof(lyric_t *), 1024);

    /* Set the MIDI event callback to our own functions rather than the system default */
    fluid_player_set_playback_callback(player, event_callback, synth);

    /* Add an onload callback so we can get information from new data before it plays */
    fluid_player_set_onload_callback(player, onload_callback, NULL);

    adriver = new_fluid_audio_driver(settings, synth);

    /* process command line arguments */
    for (i = 1; i < argc; i++) {
        if (fluid_is_soundfont(argv[i])) {
            fluid_synth_sfload(synth, argv[1], 1);
        } else {
            fluid_player_add(player, argv[i]);
        }
    }

    /* FFMpeg stuff */

    AVFrame *pFrame = NULL;
    AVPacket packet;

    AVDictionary *optionsDict = NULL;

    av_register_all();

    if(avformat_open_input(&pFormatCtx, "short.mpg", NULL, NULL)!=0) {
        printf("Couldn't open video file\n");
        return -1; // Couldn't open file
    }

    // Retrieve stream information
    if(avformat_find_stream_info(pFormatCtx, NULL)<0) {
        printf("Couldn't find stream information\n");
        return -1; // Couldn't find stream information
    }

    // Dump information about file onto standard error
    av_dump_format(pFormatCtx, 0, argv[1], 0);

    // Find the first video stream
    videoStream=-1;
    for(i=0; i<pFormatCtx->nb_streams; i++)
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
            videoStream=i;
            break;
        }
    if(videoStream==-1)
        return -1; // Didn't find a video stream

    for(i=0; i<pFormatCtx->nb_streams; i++)
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) {
            printf("Found an audio stream too\n");
            break;
        }

    // Get a pointer to the codec context for the video stream
    pCodecCtx=pFormatCtx->streams[videoStream]->codec;

    // Find the decoder for the video stream
    pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
    if(pCodec==NULL) {
        fprintf(stderr, "Unsupported codec!\n");
        return -1; // Codec not found
    }

    // Open codec
    if(avcodec_open2(pCodecCtx, pCodec, &optionsDict)<0) {
        printf("Could not open codec\n");
        return -1; // Could not open codec
    }

    sws_ctx =
        sws_getContext
        (
         pCodecCtx->width,
         pCodecCtx->height,
         pCodecCtx->pix_fmt,
         pCodecCtx->width,
         pCodecCtx->height,
         PIX_FMT_YUV420P,
         SWS_BILINEAR,
         NULL,
         NULL,
         NULL
         );

    /* GTK stuff now */

    /* GtkWidget is the storage type for widgets */
    GtkWidget *window;
    GtkWidget *button;
    GtkWidget *lyrics_box;

    /* This is called in all GTK applications. Arguments are parsed
     * from the command line and are returned to the application. */
    gtk_init (&argc, &argv);

    /* create a new window */
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

    /* When the window is given the "delete-event" signal (this is given
     * by the window manager, usually by the "close" option, or on the
     * titlebar), we ask it to call the delete_event () function
     * as defined above. The data passed to the callback
     * function is NULL and is ignored in the callback function. */
    g_signal_connect (window, "delete-event",
                      G_CALLBACK (delete_event), NULL);

    /* Here we connect the "destroy" event to a signal handler.
     * This event occurs when we call gtk_widget_destroy() on the window,
     * or if we return FALSE in the "delete-event" callback. */
    g_signal_connect (window, "destroy",
                      G_CALLBACK (destroy), NULL);

    g_signal_connect (window, "realize", G_CALLBACK (realize_cb), NULL);

    /* Sets the border width of the window. */
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);

    lyrics_box = gtk_vbox_new(TRUE, 1);
    gtk_widget_show(lyrics_box);

    /*
    char *str = "     ";
    lyric_labels[0] = gtk_label_new(str);
    str =  "World";
    lyric_labels[1] = gtk_label_new(str);
    */

    image = gtk_image_new();

    //image_drawable = gtk_drawing_area_new();
    //gtk_widget_set_size_request (canvas, 720, 480);
    //gtk_drawing_area_size((GtkDrawingArea *) image_drawable, 720, 480);

    //gtk_widget_show (lyric_labels[0]);
    //gtk_widget_show (lyric_labels[1]);

    gtk_widget_show (image);

    //gtk_box_pack_start (GTK_BOX (lyrics_box), lyric_labels[0], TRUE, TRUE, 0);
    //gtk_box_pack_start (GTK_BOX (lyrics_box), lyric_labels[1], TRUE, TRUE, 0);
    gtk_box_pack_start (GTK_BOX (lyrics_box), image, TRUE, TRUE, 0);
    //gtk_box_pack_start (GTK_BOX (lyrics_box), canvas, TRUE, TRUE, 0);
    //gtk_box_pack_start (GTK_BOX (lyrics_box), image_drawable, TRUE, TRUE, 0);

    /* This packs the button into the window (a gtk container). */
    gtk_container_add (GTK_CONTAINER (window), lyrics_box);

    /* and the window */
    gtk_widget_show (window);

    /* All GTK applications must have a gtk_main(). Control ends here
     * and waits for an event to occur (like a key press or
     * mouse event). */
    gtk_main ();

    return 0;

    /* wait for playback termination */
    fluid_player_join(player);
    /* cleanup */
    delete_fluid_audio_driver(adriver);
    delete_fluid_player(player);
    delete_fluid_synth(synth);
    delete_fluid_settings(settings);

    return 0;
}

应用如图 27-2 所示。

A435426_1_En_27_Fig2_HTML.jpg

图 27-2。

Caption

结论

通过扩展 FluidSynth,可以用各种方式做成 Karaoke 播放器。不过,它的 CPU 占用率相当高。在我的笔记本电脑上,最终版本运行在大约 100%的 CPU 上。

二十八、TiMidity 和 Karaoke

TiMidity 是 MIDI 播放器,不是 Karaoke 播放器。它被设计成一个具有特殊可扩展性的独立应用。开箱后,它可以播放 Karaoke,但不是很好。这一章着眼于如何与 TiMidity 建立一个 Karaoke 系统。

默认情况下,它只播放 MIDI 音乐,歌词打印出来。

$timidity ../54154.mid
Requested buffer size 32768, fragment size 8192
ALSA pcm 'default' set buffer size 32768, period size 8192 bytes
Playing ../54154.mid
MIDI file: ../54154.mid
Format: 1  Tracks: 1  Divisions: 30
No instrument mapped to tone bank 0, program 92 - this instrument will not be heard
#0001
@@00@12
@Here Comes The Sun
@
@@Beatles
Here comes the sun
doo doo doo doo
Here comes the sun
I said it's alright
Little
darling

但是它有许多可供选择的界面,提供不同的显示。如果您使用-h(帮助)选项运行timidity,它将显示一个类似这样的屏幕:

Available interfaces (-i, --interface option):
  -in          ncurses interface
  -ie          Emacs interface (invoked from `M-x timidity')
  -ia          XAW interface
  -id          dumb interface
  -ir          remote interface
  -iA          ALSA sequencer interface

默认的界面是“哑的”,但是如果你运行 Xaw 界面,你会得到如图 28-1 所示的显示。

A435426_1_En_28_Fig1_HTML.jpg

图 28-1。

TiMidity with Xaw interface

然而,有一个不幸的影响:歌词在播放之前就被显示了!要让歌词像应该唱的那样播放,您需要打开--trace选项。手册页中的“切换跟踪模式。在跟踪模式下,TiMidity++试图实时显示其当前状态。(您可能会发现文档和行为之间的联系不太明显。)

timidity --trace ../54154.mid

这对于 MIDI 文件来说已经很好了;歌词在该唱的时候显示。但是它不显示 KAR 文件的歌词。为此,您需要--trace-text-meta选项。

timidity --trace --trace-text-meta ../54154.kar

于是,到了这个阶段,TiMidity 对于 Karaoke 文件(以及带有歌词事件的 MIDI 文件)会将歌词实时显示在屏幕上。要对这个显示有自己的控制,你需要建立自己的 TiMidity 界面。

TiMidity 和 Jack

在第十七章中,我讨论了使用 Jack 播放 MIDI 文件。Jack 设计用于连接任意配置的音频源和接收器。例如,通过运行qjackctl,您可以将麦克风输出链接到扬声器输入。这是通过拖动capture_1playback_1来实现的,以此类推,看起来就像图 28-2 。

A435426_1_En_28_Fig2_HTML.jpg

图 28-2。

qjackctl showing microphone to speakers

如果 TiMidity,然后运行与插孔输出,你得到即时 Karaoke。您还可以使用--trace选项查看实时播放的歌词。

timidity -Oj --trace 54154.mid

连接如图 28-3 中的qjackctl所示。

A435426_1_En_28_Fig3_HTML.jpg

图 28-3。

qjackctl showing TiMidity

歌词显示很蹩脚,以后会改进。

TiMidity 界面

你需要从 SourceForge Timothy ++下载 TiMidity 源代码( http://sourceforge.net/projects/timidity/?source=dlp )。

在第二十一章中,我讨论了两种使用 TiMidity 来构建应用的方法。

  • 你可以 TiMidity 的搭建一个前端作为库后端。
  • 你可以使用带有定制接口的标准 TiMidity 作为 TiMidity 的后端。

这两种选择在这里都是可能的,但有一个问题:如果你想捕捉 MIDI 事件,那么你必须这样做作为 TiMidity 的后端,这需要你建立一个 TiMidity 的接口。

概括地说,TiMidity 的不同接口文件存储在目录interface中,并且包括像哑接口的dumb_c.c这样的文件。它们都围绕着一个在timidity/controls.h中定义的数据结构ControlMode

typedef struct {
  char *id_name, id_character;
  char *id_short_name;
  int verbosity, trace_playing, opened;

  int32 flags;

  int  (*open)(int using_stdin, int using_stdout);
  void (*close)(void);
  int (*pass_playing_list)(int number_of_files, char *list_of_files[]);
  int  (*read)(int32 *valp);
  int  (*write)(char *buf, int32 size);
  int  (*cmsg)(int type, int verbosity_level, char *fmt, ...);
  void (*event)(CtlEvent *ev);  /* Control events */
} ControlMode;

对于该结构中函数的最简单值,参见interface/dumb_c.c中哑接口的代码。

对于处理歌词,要设置的主要字段是函数event()。这将被传递一个指向在timidity/controls.h中定义的CtlEvent的指针。

typedef struct _CtlEvent {
    int type;           /* See above */
    ptr_size_t v1, v2, v3, v4;/* Event value */
} CtlEvent;

类型字段区分大量事件类型,如CTLE_NOW_LOADINGCTLE_PITCH_BEND。你感兴趣的类型是CTLE_LYRIC

处理这个问题的典型代码在interface/dumb_c.c中,它将事件信息打印到输出中。

static void ctl_event(CtlEvent *e)
{
    switch(e->type) {
      case CTLE_LYRIC:
        ctl_lyric((int)e->v1);
        break;
   }
}

static void ctl_lyric(int lyricid)
{
    char *lyric;

    lyric = event2string(lyricid);
    if(lyric != NULL)
    {
        if(lyric[0] == ME_KARAOKE_LYRIC)
        {
            if(lyric[1] == '/' || lyric[1] == '\\')
            {
                fprintf(outfp, "\n%s", lyric + 2);
                fflush(outfp);
            }
            else if(lyric[1] == '@')
            {
                if(lyric[2] == 'L')
                    fprintf(outfp, "\nLanguage: %s\n", lyric + 3);
                else if(lyric[2] == 'T')
                   fprintf(outfp, "Title: %s\n", lyric + 3);
                else
                    fprintf(outfp, "%s\n", lyric + 1);
            }
            else
            {
                fputs(lyric + 1, outfp);
                fflush(outfp);
            }
        }
        else
        {
            if(lyric[0] == ME_CHORUS_TEXT || lyric[0] == ME_INSERT_TEXT)
                fprintf(outfp, "\r");
            fputs(lyric + 1, outfp);
            fflush(outfp);
        }
    }
}

获取歌词列表

当前界面在 Karaoke 方面的缺点是,虽然它们可以在播放时显示歌词,但是它们不显示歌词线,并且在播放时逐渐突出显示它们。为此,你需要一套歌词。

TiMidity 实际上建立了一个歌词列表,并使它们易于理解。它有一个函数event2string(),接受从 1 开始的整数参数。对于每个值,它返回一个歌词或文本事件的字符串,最后返回列表末尾的NULL。返回的第一个字符是类型参数;剩下的就是字符串了。使用 GLib 函数,您可以使用以下内容为 KAR 文件构建一个行数组:

struct _lyric_t {
    gchar *lyric;
    long tick; // not used here
};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;
lyric_lines_t lyric_lines;

static void build_lyric_lines() {
    int n;
    lyric_t *plyric;
    GString *line = g_string_new("");
    GArray *lines =  g_array_sized_new(FALSE, FALSE, sizeof(GString *), 64);

    lyric_lines.title = NULL;

    n = 1;
    char *evt_str;
    while ((evt_str = event2string(n++)) != NULL) {
        gchar *lyric = evt_str+1;

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'L')) {
            lyric_lines.language =  lyric + 2;
            continue;
        }

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'T')) {
            if (lyric_lines.title == NULL) {
                lyric_lines.title = lyric + 2;
            } else {
                lyric_lines.performer = lyric + 2;
            }
            continue;
        }

        if (lyric[0] == '@') {
            // some other stuff like @KMIDI KARAOKE FILE
            continue;
        }

        if ((lyric[0] == '/') || (lyric[0] == '\\')) {
            // start of a new line
            // add to lines
            g_array_append_val(lines, line);
            line = g_string_new(lyric + 1);
        }  else {
            line = g_string_append(line, lyric);
        }
    }
    lyric_lines.lines = lines;

    printf("Title is %s, performer is %s, language is %s\n",
           lyric_lines.title, lyric_lines.performer, lyric_lines.language);
    for (n = 0; n < lines->len; n++) {
        printf("Line is %s\n", g_array_index(lines, GString *, n)->str);
    }
}

函数build_lyric_lines()应该从ctl_event()CTLE_LOADING_DONE分支调用。

TiMidity 选项

如果你选择使用 TiMidity 作为前端,那么你需要用合适的选项来运行它。这些包括打开跟踪和动态加载新接口。例如,对于当前目录中的“v”接口,这可以通过以下方式实现:

timidity -d. -iv --trace  --trace-text-meta ...
.

另一种方法是构建一个主程序,将 TiMidity 作为一个库。TiMidity 的命令行参数必须作为硬编码参数包含在应用中。一个简单的方法是:CtlMode有一个字段trace_playing,将它设置为 1 可以打开跟踪。将文本事件作为歌词事件需要更深入地挖掘 TiMidity,但只需要(在初始化库后不久)以下内容:

extern int opt_trace_text_meta_event;
opt_trace_text_meta_event = 1;

使用 Pango + Cairo + Xlib 播放歌词

我希望能够在 Raspberry Pi 和类似的片上系统(SOC)上播放我的 Karaoke 文件。不幸的是,Raspberry Pi 的 CPU 性能严重不足,所以我最终使用了 CubieBoard 2。

在这种 CPU 上,任何涉及大量图形的东西都是不可能的。所有的 MIDI 播放器都达到了接近(或超过)100%的 CPU 使用率。因此,下一节中讨论的显示背景视频的系统在不使用 GPU 的情况下在 Raspberry Pi 上是不可行的,这在我的书《Raspberry Pi GPU 音频视频编程》中讨论过。续集中讨论的程序可以在任何现有的笔记本电脑和台式机上正常运行。

在这一节中,你使用 TiMidity 作为 MIDI 播放器,使用最小的后端来显示播放的歌词。使用最低级别的 GUI 支持,即 Xlib。这可用于使用低级 Xlib 调用(如XDrawImageString)来绘制文本。这适用于 ASCII 语言,通过适当的字体选择,也适用于 ISO-8859 系列中的其他语言。

亚洲语言在标准 c 中更难处理。当使用像 UTF-8 这样的编码时,它们包含 1 或 2 字节的字符。要管理它们,最简单的方法是切换到专门处理它们的库,比如 Cairo。

Cairo 适合绘制简单的文本。例如,对于汉字,你必须找到一种能让你画出它们的字体。或者,你可以再跳一级到盘古。Pango 处理所有的字体问题,并生成发送到 X 服务器的字形。

下面的接口x_code.c采用了这种方法。

当然,前面的 naive interface 部分和本部分的 Xlib 接口之间的本质区别在于绘图。函数build_lyric_lines给你一组要渲染的线。Pango 和 Cairo 需要以下附加数据类型:

GArray *lyrics;
GString *lyrics_array[NUM_LINES];

lyric_lines_t lyric_lines;

typedef struct _coloured_line_t {
    gchar *line;
    gchar *front_of_line;
    gchar *marked_up_line;
    PangoAttrList *attrs;
} coloured_line_t;

int height_lyric_pixbufs[] = {100, 200, 300, 400}; // vertical offset of lyric in video
int coloured_text_offset;

// int current_panel = 1;  // panel showing current lyric line
int current_line = 0;  // which line is the current lyric
gchar *current_lyric;   // currently playing lyric line
GString *front_of_lyric;  // part of lyric to be coloured red
//GString *end_of_lyric;    // part of lyrci to not be coloured

gchar *markup[] = {"<span font=\"28\" foreground=\"RED\">",
                   "</span><span font=\"28\" foreground=\"white\">",
                   "</span>"};
gchar *markup_newline[] = {"<span foreground=\"black\">",
                           "</span>"};
GString *marked_up_label;

PangoFontDescription *font_description;

cairo_surface_t *surface;
cairo_t *cr;

标记字符串将用红色绘制已播放的文本,用白色绘制未播放的文本,而markup_newline将清除前一行。主要绘图功能如下:

static void paint_background() {
    cr = cairo_create(surface);
    cairo_set_source_rgb(cr, 0.0, 0.8, 0.0);
    cairo_paint(cr);
    cairo_destroy(cr);
}

static void set_font() {
    font_description = pango_font_description_new ();
    pango_font_description_set_family (font_description, "serif");
    pango_font_description_set_weight (font_description, PANGO_WEIGHT_BOLD);
    pango_font_description_set_absolute_size (font_description, 32 * PANGO_SCALE);
}

static int draw_text(char *text, float red, float green, float blue, int height, int offset) {
  // See http://cairographics.org/FAQ/
  PangoLayout *layout;
  int width, ht;
  cairo_text_extents_t extents;

  layout = pango_cairo_create_layout (cr);
  pango_layout_set_font_description (layout, font_description);
  pango_layout_set_text (layout, text, -1);

  if (offset == 0) {
      pango_layout_get_size(layout, &width, &ht);
      offset = (WIDTH - (width/PANGO_SCALE)) / 2;
  }

  cairo_set_source_rgb (cr, red, green, blue);
  cairo_move_to (cr, offset, height);
  pango_cairo_show_layout (cr, layout);

  g_object_unref (layout);
  return offset;
}

初始化 X 和 Cairo 的函数如下:

static void init_X() {
    int screenNumber;
    unsigned long foreground, background;
    int screen_width, screen_height;
    Screen *screen;
    XSizeHints hints;
    char **argv = NULL;
    XGCValues gcValues;
    Colormap colormap;
    XColor rgb_color, hw_color;
    Font font;
    //char *FNAME = "hanzigb24st";
    char *FNAME = "-misc-fixed-medium-r-normal--0-0-100-100-c-0-iso10646-1";

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Can't open dsplay\n");
        exit(1);
    }
    screenNumber = DefaultScreen(display);
    foreground = BlackPixel(display, screenNumber);
    background = WhitePixel(display, screenNumber);

    screen = DefaultScreenOfDisplay(display);
    screen_width = WidthOfScreen(screen);
    screen_height = HeightOfScreen(screen);

    hints.x = (screen_width - WIDTH) / 2;
    hints.y = (screen_height - HEIGHT) / 2;
    hints.width = WIDTH;
    hints.height = HEIGHT;
    hints.flags = PPosition | PSize;

    window = XCreateSimpleWindow(display,
                                 DefaultRootWindow(display),
                                 hints.x, hints.y, WIDTH, HEIGHT, 10,
                                 foreground, background);

    XSetStandardProperties(display, window,
                           "TiMidity", "TiMidity",
                           None,
                           argv, 0,
                           &hints);

    XMapWindow(display, window);

    set_font();
    surface = cairo_xlib_surface_create(display, window,
                                        DefaultVisual(display, 0), WIDTH, HEIGHT);
    cairo_xlib_surface_set_size(surface, WIDTH, HEIGHT);

    paint_background();

    /*
    cr = cairo_create(surface);
    draw_text(g_array_index(lyric_lines.lines, GString *, 0)->str,
              0.0, 0.0, 1.0, height_lyric_pixbufs[0]);
    draw_text(g_array_index(lyric_lines.lines, GString*, 1)->str,
              0.0, 0.0, 1.0, height_lyric_pixbufs[0]);
    cairo_destroy(cr);
    */
    XFlush(display);
}

关键函数是ctl_lyric,负责处理播放的歌词。如果歌词信号和行尾带有\/,那么它必须更新current_line。接下来的几行重新绘制每一行的文本,然后逐步遍历当前行,将第一部分涂成红色,其余部分涂成白色。

static void ctl_lyric(int lyricid)
{
    char *lyric;

    current_file_info = get_midi_file_info(current_file, 1);

    lyric = event2string(lyricid);
    if(lyric != NULL)
        lyric++;
    printf("Got a lyric %s\n", lyric);

    if ((*lyric == '\\') || (*lyric == '/')) {

        int next_line = current_line + NUM_LINES;
        gchar *next_lyric;

        if (current_line + NUM_LINES < lyric_lines.lines->len) {
            current_line += 1;

            // update label for next line after this one
            next_lyric = g_array_index(lyric_lines.lines, GString *, next_line)->str;

        } else {
            current_line += 1;
            lyrics_array[(next_line-1) % NUM_LINES] = NULL;
            next_lyric = "";
        }

        // set up new line as current line
        if (current_line < lyric_lines.lines->len) {
            GString *gstr = g_array_index(lyric_lines.lines, GString *, current_line);
            current_lyric = gstr->str;
            front_of_lyric = g_string_new(lyric+1); // lose     slosh
        }
        printf("New line. Setting front to %s end to \"%s\"\n", lyric+1, current_lyric);

        // Now draw stuff
        paint_background();

        cr = cairo_create(surface);

        int n;
        for (n = 0; n < NUM_LINES; n++) {

            if (lyrics_array[n] != NULL) {
                draw_text(lyrics_array[n]->str,
                          0.0, 0.0, 0.5, height_lyric_pixbufs[n], 0);
            }
        }
        // redraw current and next lines
        if (current_line < lyric_lines.lines->len) {
            if (current_line >= 2) {
                // redraw last line still in red
                GString *gstr = lyrics_array[(current_line-2) % NUM_LINES];
                if (gstr != NULL) {
                    draw_text(gstr->str,
                              1.0, 0.0, 00,
                              height_lyric_pixbufs[(current_line-2) % NUM_LINES],
                              0);
                }
            }
            // draw next line in brighter blue
            coloured_text_offset = draw_text(lyrics_array[(current_line-1) % NUM_LINES]->str,
                      0.0, 0.0, 1.0, height_lyric_pixbufs[(current_line-1) % NUM_LINES], 0);
            printf("coloured text offset %d\n", coloured_text_offset);
        }

        if (next_line < lyric_lines.lines->len) {
            lyrics_array[(next_line-1) % NUM_LINES] =
                g_array_index(lyric_lines.lines, GString *, next_line);
        }

        cairo_destroy(cr);
        XFlush(display);

    } else {
        // change text colour as chars are played
        if ((front_of_lyric != NULL) && (lyric != NULL)) {
            g_string_append(front_of_lyric, lyric);
            char *s = front_of_lyric->str;
            //coloured_lines[current_panel].front_of_line = s;

            cairo_t *cr = cairo_create(surface);

            // See http://cairographics.org/FAQ/
            draw_text(s, 1.0, 0.0, 0.0,
                      height_lyric_pixbufs[(current_line-1) % NUM_LINES],
                      coloured_text_offset);

            cairo_destroy(cr);
            XFlush(display);

        }
    }
}

文件x_code.c编译如下:

CFLAGS =   $(shell pkg-config --cflags gtk+-$(V).0 libavformat libavcodec libswscale libavutil )  -ITiMidity++-2.14.0/timidity/ -ITiMidity++-2.14.0/utils

LIBS =  -lasound -l glib-2.0 $(shell pkg-config --libs gtk+-$(V).0  libavformat libavcodec libavutil libswscale) -lpthread -lX11

gcc  -fPIC $(CFLAGS) -c -o x_code.o x_code.c $(LIBS)
 gcc -shared -o if_x.so x_code.o $(LIBS)

同样,这使用了 TiMidity 的本地编译和构建版本,因为 Ubuntu 版本崩溃了。它使用以下命令运行:

A435426_1_En_28_Figa_HTML.jpg

TiMidity++-2.14.0/timidity/timidity -d. -ix --trace --trace-text-meta <KAR file>

用 Gtk 播放背景视频

在第二十七章,我讨论了一个在电影上显示歌词的程序。除了前面的考虑之外,应用的其余部分与 FluidSynth 的情况类似:构建一组歌词行,使用 Pango over Gtk pixbufs 显示它们,当新的歌词事件发生时,更新歌词行中相应的颜色。

所有的动态动作都需要在 TiMidity 的后端发生,尤其是在函数ctl_event中。其他部分如初始化 FFmpeg 和 Gtk 在使用标准 TiMidity 时也必须发生在后端。如果 TiMidity 被用作一个库,这种初始化可能发生在前面或后面。为了简单起见,您只需将它们全部放在文件video_code.c的后面。

与上一节一样,您有一些初始数据结构和值,并将有一个两行coloured_line_t的数组。

struct _lyric_t {
    gchar *lyric;
    long tick;

};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;

lyric_lines_t lyric_lines;

typedef struct _coloured_line_t {
    gchar *line;
    gchar *front_of_line;
    gchar *marked_up_line;
    PangoAttrList *attrs;
#ifdef USE_PIXBUF
    GdkPixbuf *pixbuf;
#endif
} coloured_line_t;

coloured_line_t coloured_lines[2];

GtkWidget *image;

int height_lyric_pixbufs[] = {300, 400}; // vertical offset of lyric in video

int current_panel = 1;  // panel showing current lyric line
int current_line = 0;  // which line is the current lyric
gchar *current_lyric;   // currently playing lyric line
GString *front_of_lyric;  // part of lyric to be coloured red
//GString *end_of_lyric;    // part of lyrci to not be coloured

// Colours seem to get mixed up when putting a pixbuf onto a pixbuf
#ifdef USE_PIXBUF
#define RED blue
#else
#define RED red
#endif

gchar *markup[] = {"<span font=\"28\" foreground=\"RED\">",
                   "</span><span font=\"28\" foreground=\"white\">",
b                   "</span>"};
gchar *markup_newline[] = {"<span foreground=\"black\">",
                           "</span>"};
GString *marked_up_label;

现在基本上有两个代码块:一个用于在播放每个新歌词时保持彩色线条阵列的更新,另一个用于播放视频,彩色线条在顶部。第一个块有三个功能:markup_line用 HTML 标记准备一个字符串,update_line_pixbuf通过将 Pango 属性应用到标记行来创建一个新的 pixbuf,以及ctl_lyric,它在每个新的歌词事件时被触发。

前两个功能如下:

void markup_line(coloured_line_t *line) {
    GString *str =  g_string_new(markup[0]);
    g_string_append(str, line->front_of_line);
    g_string_append(str, markup[1]);
    g_string_append(str, line->line + strlen(line->front_of_line));
    g_string_append(str, markup[2]);
    printf("Marked up label \"%s\"\n", str->str);

    line->marked_up_line = str->str;
    // we have to free line->marked_up_line

    pango_parse_markup(str->str, -1,0, &(line->attrs), NULL, NULL, NULL);
    g_string_free(str, FALSE);
}

void update_line_pixbuf(coloured_line_t *line) {
    //return;
    cairo_surface_t *surface;
    cairo_t *cr;

    int lyric_width = 480;
    int lyric_height = 60;
    surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                          lyric_width, lyric_height);
    cr = cairo_create (surface);

    PangoLayout *layout;
    PangoFontDescription *desc;

    // draw the attributed text
    layout = pango_cairo_create_layout (cr);
    pango_layout_set_text (layout, line->line, -1);
    pango_layout_set_attributes(layout, line->attrs);

    // centre the image in the surface
    int width, height;
    pango_layout_get_pixel_size(layout,
                                &width,
                                &height);
    cairo_move_to(cr, (lyric_width-width)/2, 0);

    pango_cairo_update_layout (cr, layout);
    pango_cairo_show_layout (cr, layout);

    // pull the pixbuf out of the surface
    unsigned char *data = cairo_image_surface_get_data(surface);
    width = cairo_image_surface_get_width(surface);
    height = cairo_image_surface_get_height(surface);
    int stride = cairo_image_surface_get_stride(surface);
    printf("Text surface width %d height %d stride %d\n", width, height, stride);

    GdkPixbuf *old_pixbuf = line->pixbuf;
    line->pixbuf = gdk_pixbuf_new_from_data(data, GDK_COLORSPACE_RGB, 1, 8, width, height, stride, NULL, NULL);
    cairo_surface_destroy(surface);
    g_object_unref(old_pixbuf);
}

处理每个新歌词事件的函数需要确定是否发生了换行事件,即歌词是单个\字符时。然后,它需要更新索引current_line,并用新的一行替换先前的一行。一旦完成,对于所有事件,当前行被标记,并且它的位图被生成用于绘制。ctl_lyric功能如下:

static void ctl_lyric(int lyricid)
{
    char *lyric;

    current_file_info = get_midi_file_info(current_file, 1);

    lyric = event2string(lyricid);
    if(lyric != NULL)
        lyric++;
    printf("Got a lyric %s\n", lyric);
    if (*lyric == '\\') {
        int next_panel = current_panel; // really (current_panel+2)%2
        int next_line = current_line + 2;
        gchar *next_lyric;

        if (current_line + 2 >= lyric_lines.lines->len) {
            return;
        }
        current_line += 1;
        current_panel = (current_panel + 1) % 2;

        // set up new line as current line
        current_lyric = g_array_index(lyric_lines.lines, GString *, current_line)->str;
        front_of_lyric = g_string_new(lyric+1); // lose \
        printf("New line. Setting front to %s end to \"%s\"\n", lyric+1, current_lyric);

        coloured_lines[current_panel].line = current_lyric;
        coloured_lines[current_panel].front_of_line = lyric+1;
        markup_line(coloured_lines+current_panel);
#ifdef USE_PIXBUF
        update_line_pixbuf(coloured_lines+current_panel);
#endif
        // update label for next line after this one
        next_lyric = g_array_index(lyric_lines.lines, GString *, next_line)->str;

        marked_up_label = g_string_new(markup_newline[0]);

        g_string_append(marked_up_label, next_lyric);
        g_string_append(marked_up_label, markup_newline[1]);
        PangoAttrList *attrs;
        gchar *text;
        pango_parse_markup (marked_up_label->str, -1,0, &attrs, &text, NULL, NULL);

        coloured_lines[next_panel].line = next_lyric;
        coloured_lines[next_panel].front_of_line = "";
        markup_line(coloured_lines+next_panel);
        update_line_pixbuf(coloured_lines+next_panel);
    } else {
        // change text colour as chars are played
        if ((front_of_lyric != NULL) && (lyric != NULL)) {
            g_string_append(front_of_lyric, lyric);
            char *s = front_of_lyric->str;
            coloured_lines[current_panel].front_of_line = s;
            markup_line(coloured_lines+current_panel);
            update_line_pixbuf(coloured_lines+current_panel);
        }
    }
}

static gboolean draw_image(gpointer user_data) {
    GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;

    gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
    gtk_widget_queue_draw(image);
    g_object_unref(pixbuf);

    return G_SOURCE_REMOVE;
}

播放视频和覆盖彩色线条的功能没有什么本质上的新东西。它从视频中读取一帧,并将其放入 pixbuf。然后,对于每个歌词面板,它将彩色线绘制到 pixbuf 中。最后,它调用gdk_threads_add_idle以便 Gtk 可以在其主线程中绘制 pixbuf。功能play_background如下:

static void *play_background(void *args) {

    int i;
    AVPacket packet;
    int frameFinished;
    AVFrame *pFrame = NULL;

    int oldSize;
    char *oldData;
    int bytesDecoded;
    GdkPixbuf *pixbuf;
    AVFrame *picture_RGB;
    char *buffer;

    pFrame=av_frame_alloc();

    i=0;
    picture_RGB = avcodec_frame_alloc();
    buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, 720, 576));
    avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, 720, 576);

    int width = pCodecCtx->width;
    int height = pCodecCtx->height;

    sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                               pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB24,
                                               SWS_BICUBIC, NULL, NULL, NULL);

    while(av_read_frame(pFormatCtx, &packet)>=0) {
        if(packet.stream_index==videoStream) {
            //printf("Frame %d\n", i++);
            usleep(33670);  // 29.7 frames per second
            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
                                  &packet);

            if (frameFinished) {
                //printf("Frame %d\n", i++);

                sws_scale(sws_ctx,  (uint8_t const * const *) pFrame->data, pFrame->linesize, 0,
                                                  pCodecCtx->height, picture_RGB->data, picture_RGB->linesize);

                pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB, 0, 8,
                                                  width, height, picture_RGB->linesize[0],
                                                  pixmap_destroy_notify, NULL);

                // Create the destination surface
                cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                                       width, height);
                cairo_t *cr = cairo_create(surface);

                // draw the background image
                gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
                cairo_paint (cr);

                // draw the lyric
                GdkPixbuf *lyric_pixbuf = coloured_lines[current_panel].pixbuf;
                if (lyric_pixbuf != NULL) {
                    int width = gdk_pixbuf_get_width(lyric_pixbuf);
                    gdk_cairo_set_source_pixbuf(cr,
                                                lyric_pixbuf,
                                                (720-width)/2,
                                                height_lyric_pixbufs[current_panel]);
                    cairo_paint (cr);
                }

                int next_panel = (current_panel+1) % 2;
                lyric_pixbuf = coloured_lines[next_panel].pixbuf;
                if (lyric_pixbuf != NULL) {
                    int width = gdk_pixbuf_get_width(lyric_pixbuf);
                    gdk_cairo_set_source_pixbuf(cr,
                                                lyric_pixbuf,
                                                (720-width)/2,
                                                height_lyric_pixbufs[next_panel]);
                    cairo_paint (cr);
                }

                pixbuf = gdk_pixbuf_get_from_surface(surface,
                                                     0,
                                                     0,
                                                     width,
                                                     height);
                gdk_threads_add_idle(draw_image, pixbuf);

         /* reclaim memory */
                sws_freeContext(sws_ctx);
                g_object_unref(layout);
                cairo_surface_destroy(surface);
                cairo_destroy(cr);

            }
        }
        av_free_packet(&packet);
    }
    sws_freeContext(sws_ctx);

    printf("Video over!\n");
    exit(0);
}

它使用以下命令运行:

TiMidity++-2.14.0/timidity/timidity -d. -iv --trace --trace-text-meta <KAR file>

从外形上看,像图 28-4 。

A435426_1_En_28_Fig4_HTML.jpg

图 28-4。

Caption

以 TiMidity 为库的背景视频

其代码与第二十一章中的代码结构相同。它在文件gtkkaraoke_player_video_pango.c中。

#include <stdio.h>
#include <stdlib.h>

#include "sysdep.h"
#include "controls.h"

extern ControlMode  *video_ctl;
extern ControlMode  *ctl;

static void init_timidity() {
    int err;

    timidity_start_initialize();

    if ((err = timidity_pre_load_configuration()) != 0) {
        printf("couldn't pre-load configuration file\n");
        exit(1);
    }

    err += timidity_post_load_configuration();

    if (err) {
        printf("couldn't post-load configuration file\n");
        exit(1);
    }

    timidity_init_player();

    extern int opt_trace_text_meta_event;
    opt_trace_text_meta_event = 1;

    ctl = &video_ctl;
    //ctl->trace_playing = 1;
    //opt_trace_text_meta_event = 1;

}

#define MIDI_FILE "54154.kar"

static void *play_midi(void *args) {
    char *argv[1];
    argv[0] = MIDI_FILE;
    int argc = 1;

    timidity_play_main(argc, argv);

    printf("Audio finished\n");
    exit(0);
}

int main(int argc, char** argv)
{

    int i;

    /* TiMidity */
    init_timidity();
    play_midi(NULL);

    return 0;
}

以 TiMidity 为前端的背景视频

该接口需要构建为共享库,包含以下内容:

if_video.so: video_code.c
        gcc  -fPIC $(CFLAGS) -c -o video_code.o video_code.c $(LIBS)
        gcc -shared -o if_video.so video_code.o $(LIBS)

TiMidity 然后运行选项。

timidity -d. -iv --trace  --trace-text-meta

和以前一样,它崩溃了 Ubuntu 发行版的 TiMidity,但是在当前的 Linux 环境下,它可以很好地工作。

添加麦克风输入

在这个阶段,你有一个应用可以播放 MIDI 文件,播放背景电影,并在视频顶部显示突出显示的歌词。没有麦克风输入可以跟着唱。

跟着唱可以在这个应用中处理,也可以由外部进程处理。如果你想把它包含在当前的应用中,那么你必须为两个音频流构建一个混音器。Java 在 Java 声音包中实现了这一点,但在 C 语言中,您需要自己完成这一点。这可以在 ALSA 完成,但会涉及复杂的 ALSA 混频器代码。

Jack 可以轻松混合来自不同流程的音频。前面的部分展示了如何做到这一点。

一个长期的目标是包括得分,等等。然而,这将带你进入深度信号处理的领域(例如,使用 YIN 等算法识别唱的音符),这超出了本书的范围。

结论

这一章向你展示了如何在 Karaoke 系统中使用 MIDI 播放器。在我的笔记本电脑上,它使用了大约 35%的 Gtk 3.0 CPU。

二十九、Jack 和 Karaoke

插孔是为专业音响设计的。在这一章中,你将把前几章的技术应用到构建一个 Karaoke 系统中。

使用千斤顶架实现效果

Karaoke 从 MIDI 信号源和麦克风接收输入。这些混合在一起。一般来说,有一个整体音量控制,但通常也有一个麦克风音量控制。虽然 MIDI 源应该直接通过,但通常会对麦克风应用混响效果。

这些都是 LADSPA 模块可以提供的效果(参见第十四章)。Jack 应用jack-rack使这些插件可以被 Jack 应用访问,以便 LADSPA 效果可以被应用到 Jack 管道。

向会话中添加一个模块相当简单。点击+按钮,从巨大的效果菜单中选择。例如,从实用程序菜单中选择 Karaoke 插件,如图 29-1 所示。

A435426_1_En_29_Fig1_HTML.jpg

图 29-1。

Selecting Karaoke effect in Jack Rack

以下是一些可能相关的模块:

  • Karaoke(编号 1409),显示在工具菜单中。这将尝试从音乐轨道中移除中央人声。
  • 模拟器➤混响菜单中有许多混响模块。TAP 混响器似乎是功能最全的(但不保证实时)。
    • g 动词
    • 平板混响
    • 拍子混响器(来自拍子插件)The TAP Reverberator seems to be the most full featured (but is not guaranteed to be in real time).
  • 振幅➤放大器菜单中有许多放大器。

一个jack-rack应用可以应用多个模块,也可以运行多个应用。例如,将音量控制应用到麦克风,然后在将其发送到扬声器之前应用混响,可以通过添加抽头混响器和其中一个放大器来完成。这看起来像图 29-2 。

A435426_1_En_29_Fig2_HTML.jpg

图 29-2。

Jack Rack with reverb and amplifier plug-ins

我在 USB 声霸 TruStudioPro 上运行这个。这才 16 位,我好像找不到合适的插孔硬件配置。所以,我用一个插头设备手动运行 Jack,Jack 抱怨过这个设备,但不管怎样它还是工作了。

jackd -dalsa -dplughw:2 -r 48000

虽然gladish可以在它的插孔配置菜单下看到它,但我还没能让gladish接受声霸作为设置。到目前为止,我只能设法让 Jack 运行下作为一个插头设备,gladish不断交换回一个硬件设备。

qjackctl很好地保存和恢复会话,使用正确的插件和设置启动jack-rack,并将其链接到正确的捕获和回放端口。

播放 MIDI

主要合成器引擎 Timothy 和 FluidSynth 将输出到 ALSA 设备。为了让他们进入 Jack 世界,Jack 需要用-Xseq选项启动,或者需要运行a2jmidid

您可以尝试使用 Jack 会话管理器来管理连接(例如,qjackctl)。但这在使用 MIDI 合成器(如 TiMidity 或 FluidSynth)时遇到了障碍,因为它们假设 PulseAudio 输出而不是 Jack 输出。恢复会话无法恢复带有插孔输出的合成器。

您可以尝试使用 LADSPA 来管理连接。不幸的是,我至今无法使用gladish管理 Jack 服务器设置。因此,它使用默认的 ALSA 设置启动 Jack,而不使用-Xseq设置将 ALSA 端口映射到 Jack。你需要启动a2jmidid,它才能成功管理一个会话,比如timidityjack_keyboarda2jmidid

即使这样,连接图看起来还是一团乱(图 29-3 )。

A435426_1_En_29_Fig3_HTML.jpg

图 29-3。

LADISH playing MIDI

TiMidity 加上千斤顶架

在第二十八章中,你用一个插孔后端和一个 Xaw 接口的 TiMidity 给出了一个基本的 Karaoke 系统。你现在可以通过使用千斤顶效果来改善这一点。

  • 使用插孔输出和 Xaw 接口运行 TiMidity,并使用以下命令将歌词与声音同步:

    timidity -ia -B2,8 -Oj -EFverb=0 --trace --trace-text-meta
    
    
  • 运行安装了 TAP 混响器和音量控制的 Jack Rack。

  • 使用qjackctl连接端口。

最终的系统如图 29-4 所示。

A435426_1_En_29_Fig4_HTML.jpg

图 29-4。

TiMidity with Jack Rack

定制 TiMidity 构建

如果我试图动态加载另一个接口,Ubuntu 发行版的 TiMidity 版本会崩溃。随着代码被剥离,不可能找出原因。所以,要添加一个新的接口,你需要从源头上建立 TiMidity。

我现在使用的命令如下:

./configure --enable-audio=alsa,jack \
            --enable-interface=xaw,gtk \
            --enable-server \
            --enable-dynamic
make clean
make

一个带键的接口,比如说“k”,可以用如下的 Jack 输出来运行:

timidity -d. -ik -Oj --trace  --trace-text-meta 54154.mid

使用插孔架音高移位播放 MP3+G

播放器 VLC 将播放 MP3+G 文件。通常 MP3+G 是一个压缩文件,包含一个 MP3 文件和一个 CDG 文件,它们有相同的根目录。这必须解压缩,然后可以播放给 VLC 的 MP3 文件名。

vlc file.mp3

这将获得 CDG 文件并显示歌词。

VLC 可与带--aout jack选项的千斤顶一起使用。

vlc --aout jack file.mp3

对 VLC 的常见要求是具有“俯仰控制”机制。虽然应该有可能给 VLC 增加 LADPSA 俯仰控制,但还没有人着手去做。但是你仍然可以通过jack-rack添加 LADSPA 效果。

步骤如下:

  1. 您可能需要暂时停止 PulseAudio,例如使用pasuspender cat

  2. 使用以下命令启动照常运行的 Jack 守护进程:

    jackd -d alsa
    
    
  3. 开始qjackctl这样你就可以控制一些插孔连接。

  4. Start jack-rack. Using the + button, select Frequency ➤ Pitch shifters ➤ TAP Pitch Shifter. Don’t forget to enable it; it should look like Figure 29-5.

    A435426_1_En_29_Fig5_HTML.jpg

    图 29-5。

    Jack Rack with pitch shifter Note that in qjackctl, jack-rack shows as jack_rack (the minus has been replaced with an underscore), which is the proper Jack name of jack-rack. Connect the output of jack-rack to system.

  5. Now start vlc --aout jack so you can set up the correct configuration. Choose Tools ➤ Preferences, and in “Show settings” set the radio button to All. Then under Audio ➤ Output modules ➤ Jack, check “Automatically connect to writable clients” and connect to clients matching jack_rack (note the underscore). This should look like Figure 29-6.

    A435426_1_En_29_Fig6_HTML.jpg

    图 29-6。

    VLC selecting output client

  6. The next time you start vlc with, for example, vlc --aout jack BST.mp3, qjackctl should look like Figure 29-7.

    A435426_1_En_29_Fig7_HTML.jpg

    图 29-7。

    qjackctl with VLC connected to Jack Rack

音乐应该通过jack-rack播放,你可以调整音高。

图 29-8 显示了 VLC 通过音调滤波器播放 MP3 音频的结果,同时也显示了 CDG 的视频。

A435426_1_En_29_Fig8_HTML.jpg

图 29-8。

VLC playing through pitch shifter

结论

本章讨论了构建插孔管道以给 MIDI 和 MP3+G 文件添加效果的多种方法。

三十、流式音频

流式音频通常涉及将音频从网络上的一个节点发送到另一个节点。有许多方法可以做到这一点,使用许多格式。本章简要讨论了其中的一些。

超文本传送协议

HTTP 是网络的基础协议。该协议不知道它承载的内容。虽然它最初是为传输 HTML 文档而设计的,但现在它被用来传输图像文件、Postscript 文档、PowerPoint 文件和几乎任何其他文件。这包括媒体文件,这本书的主题。

超文本传输协议服务器

内容通过 HTTP 服务器从网站传送。其中最著名的是 Apache,但是在 Linux 世界中,Nginx 和 Lighttpd 也很常见。还有许多专有服务器。

HTTP 服务器可以传送存储在服务器上的静态文件,也可以从数据库连接动态构建内容。

HTTP 客户端

HTTP 流有许多客户端,通常称为用户代理。这些包括浏览器以及前面讨论的许多音频播放器。

HTTP 浏览器

将你的浏览器指向一个音频文件的 URL,它会将内容传递给一个助手,该助手将尝试播放该文件。浏览器将基于 URL 的文件扩展名或者基于从 HTTP 服务器的 HTTP 头中传送的文件的内容类型来选择助手。

MPlayer

MPlayer 支持 HTTP。你只需给出文件的 URL。

mplayer http://localhost/audio/enigma/audio_01.ogg

可见光通讯

VLC 也知道 HTTP。你只需给出文件的 URL。

vlc http://localhost/audio/enigma/audio_01.ogg

流媒体与下载

如果你从网上下载一个文件,那么你可以在下载完成后播放它。这意味着播放被延迟,直到整个文件被保存到本地文件系统。由于它现在是本地的,所以它可以玩而不用担心网络延迟。下面是一个简单的 shell 脚本来说明这一点:

wget -O tmp  http://localhost/audio/enigma/audio_01.ogg
mplayer tmp
rm tmp

另一种方法是从网上读取资源,并使用某种管道将收到的资源传递给播放器。只要管道足够大,能够缓冲足够的资源,以应对网络延迟,这就没问题。下面举例说明:

wget -O -  http://localhost/audio/enigma/audio_01.ogg | mplayer -

(是的,我知道,MPlayer 可以直接流 URLs 我只是想说明一点。)

HTML5

HTML5 是 HTML 的最新版本。HTML5 是一个“生活标准”。啊!这意味着它根本不是一个标准,而只是一个处于不断变化状态的规范的标签。现在有一个音频元素<audio>,被很多浏览器实现。

例如,下面的 HTML 将首先尝试 Ogg 文件,如果客户端无法播放它,它将尝试 MP3 文件,如果无法播放,它将显示失败消息:

      <audio controls="controls"<
        <source src="audio_01.ogg" type="audio/ogg"<
          <source src="audio_01.mp3" type="audio/mpeg"<
            Your browser does not support the audio element.
      </audio<

图 30-1 显示了它在浏览器中的样子。

A435426_1_En_30_Fig1_HTML.jpg

图 30-1。

Caption

dlna!dlna!dlna

数字生活网络联盟(DLNA)旨在共享家庭网络中的数字媒体,如照片、音频和视频。它建立在通用即插即用(UPnP)协议套件之上。这反过来又建立在一个更丑陋的互联网标准 SOAP 之上。为了处理媒体信息,UPnP 本身使用了只能被描述为糟糕透顶的黑客技术,从而加剧了基础技术的糟糕选择。由于其最复杂的数据类型是字符串,UPnP 将完整的 XML 文档嵌入到这些字符串中,以便一个 XML 文档包含另一个 XML 文档作为嵌入字符串。哦,天哪,质量更好的工程师肯定能想出比这更好的解决方案!

UPnP 是开放的,因为它可以描述许多不同的家庭网络设备和数据格式。DLNA 将其限制在少数“认可的”类型,然后将该规范设为私有,只有在付费后才能使用。

尽管如此,越来越多的设备“支持 DLNA ”,如电视、蓝光播放器等。看来 DLNA 要在这里呆下去了。

马修·潘顿在《媒体流的 DLNA——这一切意味着什么( http://news.cnet.com/8301-17938_105-10007069-1.html )指出了 DLNA 的一些进一步的问题,主要涉及到支持的文件格式。我最近购买的一台索尼 BDP-S390 蓝光播放器说明了他的评论的真实性。根据需要支持 LPCM ( .wav),但在可选的 MP3、WMA9、AC-3、AAC、ATRAC3plus 中,仅支持 MP3、AAC/HE-AAC ( .m4a)和 WMA9 标准(.wma)。当然,奥格不在任何 DLNA 榜单上。

网站 DLNA 开源项目( http://elinux.org/DLNA_Open_Source_Projects )列出了一批 Linux DLNA 玩家。我已经成功地使用了 CyberGarage Java 客户端和服务器以及 MediaTomb 服务器。

冰铸

Shoutcast 是一款用于互联网音频流的专有服务器软件,它为流设置了标准。Icecast 是开源软件的有力竞争者,它和开源软件一样质量好,更优秀,并且支持更多的格式。对于流的接收者来说,Icecast 只是一个 HTTP 服务器。后端是有趣的部分,因为 Icecast 使用 Shoutcast 协议从各种来源接收音频,如在线广播、麦克风或播放列表。

IceS 是 Icecast 获取音频流的一种方式,包含在发行版中。更多信息,请参见 IceS v2.0 文档( www.icecast.org/docs/ices-2.0.2/ )。

流体运动

从 Flumotion 网站( www.flumotion.net/ ),“Flumotion 流媒体软件允许广播公司和公司在一台服务器上以所有领先的格式实时点播内容。Flumotion 还提供流媒体平台和网络电视,通过覆盖整个流媒体价值链来减少工作流程和成本。这种端到端的模块化解决方案包括信号采集、编码、多格式转码、内容流和一流的接口设计。媒体后台支持高级内容管理,并通过富媒体广告实现最佳盈利。”

结论

本章简要概述了一些可用的流机制。HTML5 嵌入提供了一种将音频(和视频)包含到网页中的简单方法,而 Icecast 和 Flumotion 等系统可以用于广播电台等专业音频系统。

三十一、树莓派

Raspberry Pi (RPi)是一台低成本的 Linux 计算机,开发的目的是为进入大学计算机科学课程的学生提供一个良好、廉价的游戏环境。确实如此。我有一群已经步入中年的同事,他们都迫不及待地想和我一起玩。不过,到目前为止,他们的孩子还没有看过....

资源

以下是一些资源:

基础知识

以下部分涵盖了基础知识。

五金器具

Raspberry Pi (RPi) 3 型号 B 有 1Gb RAM,四个 USB 端口,WiFi 和蓝牙,以及一个以太网端口。它具有 HDMI 和模拟音频和视频输出。以下来自 FAQ ( www.raspberrypi.org/faqs ):

"Except for the 2B/3B raspberry pie, Broadcom BCM2835 is used in all versions and revisions of raspberry pie. It consists of a floating-point ARM1176JZFS running at 700MHz and a VideoCore 4 GPU. The GPU can play blue light with H.264 at a speed of 40 megabits per second. It has a fast 3D kernel, which can be accessed through OpenGL ES2.0 and OpenVG libraries provided. The model used in 2B is Broadcom BCM2836. It consists of a quad-core ARM Cortex-a7 processor with floating point and NEON, running at 900MHz, and the same VideoCore 4 GPU as other models of Raspberry Pi. The model 3B uses Broadcom BCM2837, which includes a quad-core ARM Cortex-A53 running at 1.2GHz. Its GPU capability is equivalent to Pi 2. "

RPi 通过 HDMI 端口和模拟 3.5 毫米音频输出端口提供音频输出。中没有音频。但是,有 USB 端口,可以插入 USB 声卡,这是 Linux 发行版可以识别的。

CPU 是 ARM CPU。您可以在“ARM 与 x86 处理器:有何不同?”中找到 ARM 和 Intel 指令集之间的差异的简单概述( www.brighthub.com/computing/hardware/articles/107133.aspx )。

替代单板计算机

单板电脑很多。维基百科有单板电脑列表(http://en.wikipedia.org/wiki/List_of_single_board_computers);它们都是 RPi 的潜在替代品。这里只是一个快速选择:

Gumstix ( http://en.wikipedia.org/wiki/Gumstix

  • 这是一台存在了很多年的单板电脑(我在 2004 年得到了一台)。它的功率不高,但也不应该如此。

Arduino ( http://en.wikipedia.org/wiki/Arduino

  • Arduino 是为电子项目设计的微控制器。它使用 ARM Cortec-M3 CPU,比 RPi 的规格还要低。

Udo(www.udoo.org/

  • UDOO 试图将 RPi 和 Arduino 的精华与两个 CPU 结合到一台单板计算机中。

ODroid ( http://odroid.com/ )

BeagleBone ( http://beagleboard.org/Products/BeagleBone%20Black

  • BeagleBone Black 的 CPU (ARM Cortex-A8)略好于 RPi,但价格稍贵。

分散注意力

Raspberry Pi 站点提供了几个 Linux 映像,其他映像正在其他地方开发。我使用的是基于 Debian 的镜像,它有两种形式:使用 Debian 的软浮点和使用 FPU 的硬浮点,称为 Raspbian。体面的声音处理需要硬浮点图像,这在很大程度上取决于浮点。在 www.memetic.org/raspbian-benchmarking-armel-vs-armhf/ 有一篇很好的对标文章。另一组基准在 http://elinux.org/RaspberryPiPerformance 。基本上,这些表明,如果您想要良好的浮点性能,应该使用硬浮点版本,这是音频处理所必需的。

ELinux.org 维护着一个 RPi 分布列表( http://elinux.org/RPi_Distributions )。这里包含了许多标准的 Linux 发行版,比如 Fedora、Debian、Arch、SUSE、Gentoo 等等。RPi 作为一个基于 XBMC 媒体中心的媒体中心获得了广泛的关注,它的代表是 Raspbmc 和 OpenElec 等发行版。

那么,它和目前讨论的各种音频工具相处的怎么样呢?这是一个混合的包。

没有声音

我用 HDMI 接口把我的电脑插入 29 英寸的优派显示器。最初,3.5 毫米模拟输出或 HDMI 显示器都没有声音。这在“为什么我的音频(声音)输出不起作用?”( http://raspberrypi.stackexchange.com/questions/44/why-is-my-audio-sound-output-not-working )。我编辑了文件/boot/config.txt,取消了对行"hdmi_drive=2"的注释。我还使用了下面的命令,其中n0 =自动,1 =耳机,2=hdmi 来切换输出:

sudo amixer cset numid=3 <n>

之后声音就没问题了。

驱动

Raspberry Pi 使用 ALSA 驱动snd_bcm2835,这可以管理 HDMI 输出。命令alsa-info不存在,但是因为这是一个 shell 脚本,所以它可以从其他地方复制并在 RPi 上运行。大型发行版中的一些常用配置文件和命令丢失了,但是它在 RPi2 的 Raspbian 发行版中显示出来(有许多遗漏)。

!!################################
!!ALSA Information Script v 0.4.64
!!################################

!!Script ran on: Sun Nov 13 11:13:36 UTC 2016

!!ALSA Version
!!------------

Driver version:     k4.7.2-v7+
Library version:    1.0.28
Utilities version:  1.0.28

!!Loaded ALSA modules
!!-------------------

snd_bcm2835

!!Soundcards recognised by ALSA
!!-----------------------------

 0 [ALSA           ]: bcm2835 - bcm2835 ALSA
                      bcm2835 ALSA

!!Aplay/Arecord output
!!--------------------

APLAY

**** List of PLAYBACK Hardware Devices ****
card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
  Subdevices: 8/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 0: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

ARECORD

**** List of CAPTURE Hardware Devices ****

!!Amixer output
!!-------------

!!-------Mixer controls for card 0 [ALSA]

Card hw:0 'ALSA'/'bcm2835 ALSA'
  Mixer name    : 'Broadcom Mixer'
  Components    : ''
  Controls      : 6
  Simple ctrls  : 1
Simple mixer control 'PCM',0
  Capabilities: pvolume pvolume-joined pswitch pswitch-joined
  Playback channels: Mono
  Limits: Playback -10239 - 400
  Mono: Playback -2000 [77%] [-20.00dB] [on]

音频播放器样本

前面的章节已经广泛使用了一些音频工具。RPi 仍然是一个 Linux 系统,所以您会期望音频工具在 RPi 上表现正常。但是值得确认!

MPlayer

MPlayer 在默认的 ALSA 模块上可以很好地播放 MP3、OGG 和 WAV 文件。

可见光通讯

VLC 试图播放 WAV 文件,但它在软浮动发行版上被打断了。CPU 使用率上升了 90%左右,这是非常不可玩的。由于这个原因,这个软件发行版已经不再使用了。硬浮动发行版可以播放 MP3,OGG 和 WAV 文件。

alsaplayer

该程序使用标准的硬浮动发行版播放 Ogg-Vorbis 和 MP3 格式的文件。

omxplayer

RPi 有一个 GPU,这个可以被omxplayer使用。它可以播放 Ogg-Vorbis 文件,只占用 12%的 CPU,看起来是音频和视频的良好候选。

是 X 在用 CPU 吗?

显然不只是 X: gnome-player 工作正常。

采样音频采集

RPi 没有音频输入或线路输入端口。我通过通电的 USB 集线器连接声霸卡。它与arecord -l一起显示如下:

**** List of CAPTURE Hardware Devices ****
card 1: Pro [SB X-Fi Surround 5.1 Pro], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

所以,对 ALSA 来说,它是装置hw:1,0

驱动

如果你得到正确的选项,标准程序arecord就会工作。

arecord -D hw:1,0 -f S16_LE -c 2 -r 48000 tmp.s16
Recording WAVE 'tmp.s16' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo

生成的文件可以通过以下方式播放:

aplay -D hw:1,1 -c 2 -r 48000 -f S16_LE tmp.s16

在第十章,我给出了一个名为alsa_capture.c的程序的源代码。当运行以下程序时:

alsa_capture hw:1,0 tmp.s16

它以 48,000Hz 的频率记录立体声 PCM 数据。

MIDI 播放器

虽然标准音频工具工作正常,但 MIDI 播放器的 CPU 负担很重。这一节着眼于自定义普通玩家玩 OK。

TiMidity

RPi 2 上 TiMidity 的 CPU 平均占 50 %, RPi 3 上 TiMidity 的 CPU 平均占 38%。如果其他应用(如 GUI 前端)也在使用,这可能会使它不可用。

为了提高 RPi 的可用性,在timidity.cfg文件中,取消对以下行的注释:

## If you have a slow CPU, uncomment these:
#opt EFresamp=d         #disable resampling
#opt EFvlpf=d           #disable VLPF
#opt EFreverb=d         #disable reverb
#opt EFchorus=d         #disable chorus
#opt EFdelay=d          #disable delay
#opt anti-alias=d       #disable sample anti-aliasing
#opt EWPVSETOZ          #disable all Midi Controls
#opt p32a               #default to 32 voices with auto reduction
#opt s32kHz             #default sample frequency to 32kHz
#opt fast-decay         #fast decay notes

这使得 RP2 上的 CPU 使用率下降到大约 30%。(感谢骑士精神不才, http://chivalrytimberz.wordpress.com/2012/12/03/pi-lights/ )。)

皮卡拉奥克

这仅使用了 40%的 CPU,即使使用 GUI 也运行良好。

流体合成/qssynth

在 RPi2 和 RPi3 上,CPU 使用率上升了大约 85%到 90%。

行程安排

有时 FluidSynth 会抱怨无法重置调度程序。Aere Greenway (

aere - rtprio 85
aere - memlock unlimited

确保用您的用户 ID 代替aere

非原因

以下被认为是问题的原因,但最终被忽略:

  • FluidSynth 可以配置为使用 doubles 或 floats。默认是双精度,这些在 ARM 芯片上很慢。切换到浮点并没有消除 CPU 使用中的问题峰值。
  • FluidSynth 使用声音字体文件,这些文件相当大。通常约为 40MB。切换到较小的字体没有帮助;内存使用不是问题。
  • FluidSynth 中的缓冲很小。可以使用-z参数使其变大。缓冲不是问题,改变它也没有帮助。
  • 众所周知,许多操作在 CPU 中开销很大。FluidSynth 支持许多插值算法,可以使用命令解释器设置这些算法,例如使用interp 0关闭插值。其他昂贵的操作包括混响、复调和合唱。孤立地摆弄这些东西被证明是徒劳的。

解决方法

我发现的两个解决方案是

  • polyphony=64reverb=false
  • rate=22050,这将 CPU 使用率降低到大约 55%

Java 声音

我安装了 OpenJDK 版本 8,这是目前默认的 Java 版本。第十章给出了程序DeviceInfo。RPi 上的输出如下:

Mixers:
   PulseAudio Mixer, version 0.02
    Mixer: org.classpath.icedtea.pulseaudio.PulseAudioMixer@144bcfa
      Source lines
        interface SourceDataLine supporting 42 audio formats, and buffers of 0 to 1000000 bytes
        interface Clip supporting 42 audio formats, and buffers of 0 to 1000000 bytes
      Target lines
        interface TargetDataLine supporting 42 audio formats, and buffers of 0 to 1000000 bytes
   ALSA [default], version 4.7.2-v7+
    Mixer: com.sun.media.sound.DirectAudioDevice@d3c617
      Source lines
        interface SourceDataLine supporting 84 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 84 audio formats, and buffers of at least 32 bytes
      Target lines
   ALSA [plughw:0,0], version 4.7.2-v7+
    Mixer: com.sun.media.sound.DirectAudioDevice@1c63996
      Source lines
        interface SourceDataLine supporting 8 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 8 audio formats, and buffers of at least 32 bytes
      Target lines
   ALSA [plughw:0,1], version 4.7.2-v7+
    Mixer: com.sun.media.sound.DirectAudioDevice@11210ee
      Source lines
        interface SourceDataLine supporting 8 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 8 audio formats, and buffers of at least 32 bytes
      Target lines
   Port ALSA [hw:0], version 4.7.2-v7+
    Mixer: com.sun.media.sound.PortMixer@40e464
      Source lines
      Target lines
        PCM target port

虽然这是使用 PulseAudio 混音器,但 PulseAudio 实际上并没有运行(在这个阶段)!所以,它只能使用 ALSA 接口。

第九章给出了程序PlayAudioFile。这个可以播放.wav文件 OK。但它不能播放 Ogg-Vorbis 或 MP3 文件,并会抛出一个UnsupportedAudioFileException

脉冲二极管(PulseAudio)

PulseAudio 从存储库中安装 OK,运行时没有任何问题。pulsedevlist的输出如下:

=======[ Output Device #1 ]=======
Description: bcm2835 ALSA Analog Stereo
Name: alsa_output.platform-bcm2835_AUD0.0.analog-stereo
Index: 0

=======[ Input Device #1 ]=======
Description: Monitor of bcm2835 ALSA Analog Stereo
Name: alsa_output.platform-bcm2835_AUD0.0.analog-stereo.monitor
Index: 0

Java MIDI 文件

openJDK 支持 Java MIDI 设备。程序DeviceInfo报告如下:

MIDI devices:
    Name: Gervill, Decription: Software MIDI Synthesizer, Vendor: OpenJDK
        Device is a synthesizer
        Open receivers:

        Default receiver: com.sun.media.sound.SoftReceiver@10655dd

        Open receivers now:
            com.sun.media.sound.SoftReceiver@10655dd

        Open transmitters:
        No default transmitter
    Name: Real Time Sequencer, Decription: Software sequencer, Vendor: Sun Microsystems
        Device is a sequencer
        Open receivers:

        Default receiver: com.sun.media.sound.RealTimeSequencer$SequencerReceiver@12f0999

        Open receivers now:
            com.sun.media.sound.RealTimeSequencer$SequencerReceiver@12f0999

        Open transmitters:

        Default transmitter: com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@65a77f

        Open transmitters now:
            com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@65a77f
Default system sequencer is Real Time Sequencer
Default system synthesizer is Gervill

像 DumpSequence 这样的程序工作正常。但是 SimpleMidiPlayer 达到了 80%的 CPU 使用率,是不可用的。因此,任何 Karaoke 播放器在 RPi 上使用 Java 的想法都是不好的。树莓派网站上有一个讨论声音问题的帖子( www.raspberrypi.org/phpBB3/viewtopic.php?f=38&t=11009 )。

最大 OpenMAX

可以使用 OpenMAX IL 工具包在 Raspberry Pi 上播放音频和视频。这是 Broadcom 为 RPi 使用的 GPU 实现的。这在第十三章中有部分介绍,在我的书《Raspberry Pi GPU 音频和视频编程》中有深入介绍。

结论

树莓派是一个令人兴奋的新玩具。这个市场上有许多竞争对手,但它仍然卖出了一千多万台设备。本章讲述了设备的一些音频方面。

三十二、总结

这是我最后的话。

我从哪里开始?

以下是这一切的起点:

  • 我有两台 Karaoke 机,各有不同的功能。
  • 我想用我的电脑建造一台“两全其美”的机器。
  • 我第一次尝试使用 Java Sound API 时工作正常,但是受到了延迟的影响,足以使它无法使用。
  • 试图将这个 Java 解决方案的任何部分移植到像 Raspberry Pi 这样的低功耗设备上的尝试都以惨败告终。

我到哪里了?

嗯,我已经走了大部分路了。我现在有一个系统在后台播放视频,并使用 TiMidity 合成器播放 Karaoke 文件。我没有让评分系统工作,但这涉及到对数字信号处理的进一步探索。

我实际上在树莓 Pi 上完成了所有工作,但这意味着深入研究树莓 Pi 的 GPU 来处理视频效果,我已经在另一本书中处理了它的 GPU 编程。

我是怎么到那里的?

很明显,我需要用声音来演奏。我从 Java 声音 API 开始,当它被证明有延迟问题时,我开始在 Linux 上搜索声音的所有方面。这就是为什么这本书有 ALSA、Jack、PulseAudio 等章节。我无法以足够清晰的方式找到我要找的信息,所以随着我发现的越来越多,我把它们都写了下来,结果就是这本书。

我希望你会发现它有普遍的价值,而不仅仅是让我特别着迷的东西。我在写这本书的过程中学到了很多,我相信如果你想在 Linux 下做任何关于声音的事情,这本书至少会给你一些答案。

问候,并祝你自己的项目好运!

三十三、解码 Sonken 卡拉 DVD 上的 DKD 文件

这一章是关于从我的 Sonken 卡拉 DVD 中获取信息,这样我就可以开始编写播放歌曲的程序。在 Linux 下不直接参与播放声音,可以跳过。

介绍

我有两台 Karaoke 机,一台 Sonken MD-388,一台 Malata MDVD-6619。在他们两个之间,他们拥有我认为我需要的 Karaoke 播放器的所有特征,包括以下:

  • 选曲和放曲子(当然!)
  • 大量的中文和英文歌曲(我妻子是中国人,我是英国人)
  • 中文歌曲有普通话和拼音,所以我也可以跟着唱
  • 旋律的音符与歌手实际唱的音符一起显示
  • 显示不同特征的评分系统

Malata 真的很好,因为它显示了旋律的音符,也显示了你正在唱的音符。但它的英文歌曲少得可怜,而且没有显示中文歌曲的拼音。Songen 在这两方面都有很好的选择,可以显示拼音,但不显示音符,并且有一个简单的评分系统。

所以,我想把歌曲从我的 Sonken DVD 上拿下来,在万利达或我的电脑上播放。在我的个人电脑上玩它们是首选,因为这样我就只受我能写的程序的限制,而不那么依赖于供应商的机器。所以,我的近期目标是把歌曲从 Sonken 的 DVD 上拿下来,开始用我想要的方式播放。

Sonken DVD 上的文件是 DKD 格式的。这是一种未记录的格式,可能代表数字 Karaoke 光盘。很多人都致力于这种格式,在卡拉 Engineering 等论坛上也有很多讨论。其中包括“了解加州电子 DVD 上的热狗文件”( http://old.nabble.com/Understanding-the-HOTDOG-files-on-DVD-of-California-electronics-td11359745.html ),“解码 JBK 6628 DVD Karaoke 碟片”( http://old.nabble.com/Decoding-JBK-6628-DVD-Karaoke-Disc-td12261269.html )(不过这两个环节似乎不再有任何内容),以及“Karaoke Huyndai 99”(http://board.midibuddy.net/showpost.php?p=533722&postcount=31)。

当我开始看我的光盘时,我的方向与这些论坛上的许多海报不同。此外,正如所预料的那样,论坛的结果是以一种临时的、常常令人困惑的方式提出的。所以,我最终重新发明了很多已经被发现的东西,也想出了一些新的东西。

事后看来,如果我对论坛上的言论给予足够的重视,我本可以节省数周的工作时间。因此,这个附录是我试图以一种简单而有逻辑的方式展示结果,以便试图用自己的光盘做类似事情的人可以很容易地找出什么适用于他们的情况,什么是不同的。

本章将涵盖以下内容:

  • 我的 DVD 上有什么文件
  • 每个文件包含的内容(概述)
  • 将歌曲标题与歌曲编号相匹配
  • 查找光盘上的歌曲数据
  • 提取歌曲数据
  • 解码歌曲数据

这个附录并不完整,因为还有更多有待发现。

格式转换

拷贝你的 DVD 不是违法的吗?它不在澳洲,在适当的条件下(见 www.ag.gov.au/Copyright/Issuesandreviews/Pages/CopyrightAmendmentAct2006FAQs.aspx 《版权修正法案 2006》常见问题)。

  • 我能把我的音乐收藏复制到我的 iPod 上吗?是的。您可以将自己的音乐格式转换到 MP3 播放器、Xbox 360 或电脑等设备上。

我只是把我合法购买的 Sonken DVD 上的音乐拷贝到我的电脑上供个人使用。这是在澳大利亚版权修正法案的范围内。你应该检查你的国家是否允许同样的权利。不要从我的 DVD 上索取任何文件的副本。那是违法的,我不会这么做的。

DVD 上的文件

我的 Sonken DVD 包含这些文件:

          BACK01.MPG
          DTSMUS00.DKD
          DTSMUS01.DKD
          DTSMUS02.DKD
          DTSMUS03.DKD
          DTSMUS04.DKD
          DTSMUS05.DKD
          DTSMUS06.DKD
          DTSMUS07.DKD
          DTSMUS10.DKD
          DTSMUS20.DKD

BACK01。每加仑行驶英里数

这是在后台播放的 MP3 文件。

dtsmus 00 . dkd 至 dtsmus 07 . dkd

这些是歌曲文件。这些的数量取决于 DVD 上有多少首歌曲。

dtsmus 10 . dkd

还没有人知道这个文件是干什么用的。

dtsmus 20 . dkd

该文件包含歌曲编号、歌曲标题和艺术家的列表,如歌曲书中所给。这个文件里的歌曲号比书里的歌曲号少一个。

解码 dtsmus 20 . dkd

我在 Linux 系统上,使用 Linux/Unix 实用程序和应用。Windows 和 Apple 等其他操作系统也有类似的版本。

歌曲信息

Unix 命令strings列出了一个文件中所有长度至少为四个字符的 ASCII 8 位编码字符串。在所有的 DVD 文件上运行这个命令会显示出DTSMUS20.DKD是唯一一个有很多英语字符串的文件,而这些字符串就是 DVD 上的歌曲标题。

简单的选择如下:

          Come To Me
          Come To Me Boy
          Condition Of My Heart
          Fly To The Sky
          Cool Love
          Count Down
          Cowboy
          Crazy

当然,光盘上显示的实际字符串取决于光盘上的歌曲。当然,你需要一些英文标题才能让它工作!

为了取得进一步的进展,您需要一个二进制编辑器。我使用的bvi. emacs也有二进制编辑模式。使用编辑器搜索光盘上已知的歌曲标题。例如,搜索甲壳虫乐队的“太阳来了”,会显示以下区块:

          000AA920  12 D3 88 48 65 72 65 20 43 6F 6D 65 73 20 54 68 ...Here Comes Th
          000AA930  65 20 52 61 69 6E 20 41 67 61 69 6E 00 45 75 72 e Rain Again.Eur
          000AA940  79 74 68 6D 69 63 73 00 1F 12 D3 89 48 65 72 65 ythmics.....Here
          000AA950  20 43 6F 6D 65 73 20 54 68 65 20 53 75 6E 00 42  Comes The Sun.B
          000AA960  65 61 74 6C 65 73 00 1B 12 D3 8A 48 65 72 65 20 eatles.....Here
          000AA970  46 6F 72 20 59 6F 75 00 46 69 72 65 68 6F 75 73 For You.Firehous

字符串“太阳来了”从 0xAA94C 开始,后跟一个空字节。接下来是以零结尾的“Beatles”紧接在这之前的是 4 个字节。这两个字符串(包括空字节)和 4 个字节的长度是 0x1F,这是前面四个字节的第一个。因此,该块由一个 4 字节的头、一个以空结尾的歌曲标题和一个以空结尾的艺术家组成。字节 1 是包括 4 字节标题的歌曲信息块的长度。

标题块的字节 2 是 0x12。jim75 在“解码 JBK 6628 DVD Karaoke 碟片”( http://old.nabble.com/Decoding-JBK-6628-DVD-Karaoke-Disc-td12261269.html )发现了文件JBK_Manual%5B1%5D.doc。其中有一个国家代码列表,如下所示:

          00 : KOREAN
          01 : CHINESE( reserved )
          02 : CHINESE
          03 : TAIWANESE
          04 : JAPANESE
          05 : RUSSIAN
          06 : THAI
          07 : TAIWANESE( reserved )
          08 : CHINESE( reserved )
          09 : CANTONESE
          12 : ENGLISH
          13 : VIETNAMESE
          14 : PHILIPPINE
          15 : TURKEY
          16 : SPANISH
          17 : INDONESIAN
          18 : MALAYSIAN
          19 : PORTUGUESE
          20 : FRENCH
          21 : INDIAN
          22 : BRASIL

甲壳虫乐队的歌曲在头的字节 2 中有 0x12,这与表中的国家代码相匹配。通过查看其他语言文件可以确认这一点。

我后来发现 WMA 的档案有他们自己的密码。到目前为止,我看到了以下内容:

          83 : CHINESE WMA
          92 : ENGLISH WMA
          94 : PHILIPPINE WMA

我想你可以从早期的照片中看出一种模式!

头的字节 3 和 4 是 0xD389,十进制是 54153。这比书里的歌号(54154)少了一个。所以,字节 3 和 4 是一个 16 位的短整数,比书中的歌曲索引少 1。

这种模式在整个文件中重复出现,所以每个记录都是这种格式。

数据的开始/结束

文件开头附近有一长串字节:“01 01 01 01 ....”这在我的文件 0x9F23 处结束。通过比较索引号和我的歌曲簿中的索引号,我确认这是韩国歌曲的开始,也可能是所有歌曲的开始。我还没有找到任何表给我这个起始值。

检查了一些歌曲后,我得到了这个表格:

  • 英文歌曲从 60x9562D 开始(歌曲 24452,类型 0x12)
  • 粤语 0x8F5D2(宋 13701,3 型)
  • 朝鲜语 at 0x9F23(歌曲 37847,类型 0)
  • 印度尼西亚文 at 0x11F942(宋 42002,类型 0x17)
  • 位于 0x134227 的印地语(歌曲 45058,类型 0x21)
  • 菲律宾 at 0xD5D20(宋 62775,类型 0x14)
  • 俄语 at 0x110428(歌曲 41012,类型 5)
  • 0xF5145 处的西班牙语(歌曲 26487,类型 0x16)
  • 0x413BE 处的普通话(1 个字符)(宋 1388,类型 3)

不过,我找不到越南歌曲。我的光盘上好像没有。我的歌本在说谎!我猜在某个地方有一些表格给出了这些起点,但是我还没有找到。这些都是看我的歌本然后在档案里找到的。

0x136C92 上的“FF FF FF FF …”序列表示模块结束。

但是在歌曲信息块之前和之后都有很多东西。我不知道这是什么意思。

中国歌曲

我书里的第一首英文歌是艾尔·维德的《阿甘正传》,歌曲编号 24452。在目录文件DTSMUS20.DK中,它位于 0x9562D (611885)。在此之前的条目是“20 03 3A 04 CE D2 B4 F2 C1 CB D2 BB CD A8 B2 BB CB B5 BB B0 B5 C4 B5 E7 BB B0 B8 F8 C4 E3 00 00。”歌曲代码是“3A 04”,换句话说就是 14852,也就是歌曲编号 14853(一个偏移量,记住!).当我在 Karaoke 机上播放这首歌时,我很幸运:这首歌的第一个字符是我,我把它认作是汉字“我”(拼音:wo3)。它在文件中的编码是“CE D2”我在电脑上安装了中文输入法,所以我可以搜索这个汉字。

Google 搜索 Unicode 值为我向我显示以下内容:

          [RESOLVED] Converting Unicode Character Literal to Uint16 variable ...
          www.codeguru.com › ... › C++ (Non Visual C++ Issues)
          5 posts - 2 authors - 1 Jul 2011

          I've determined that the unicode character '我' has a hex value of
          0x6211 by looking it up on the "GNOME Character Map 2.32.1"
          and if I do this....

然后在 Unicode 搜索上查找 0x 6211(www.khngai.com/chinese/tools/codeunicode.php)给出黄金。

          Unicode       6211 (25105)
          GB Code       CED2 (4650)
          Big 5 Code    A7DA
          CNS Code      1-4A3C

第二行中的 CED2 是 GB 代码。因此,字符集是 GB(可能是 EUC-CN 编码的 GB2312 ),代码为我作为 CED2。

只是为了确定,使用玛丽·安塞尔在 GB 代码表( www.ansell-uebersetzungen.com/gborder.html )中的表格,将字节“CE D2 B4 F2 C1 CB D2 BB CD A8 B2 BB CB B5 BB B0 B5 C4 B5 E7 BB B0 B8 F8 C4 E3”翻译成我 打 了 一 通…,确实是这首歌。

其他语言

我不熟悉其他语言编码,所以没有研究过泰语、越南语等等。韩国人好像是 EUC 人。

程序

其他人的早期研究已经产生了 C 或 C++程序。这些通常是独立的程序。我想建立一个可重用模块的集合,所以我选择 Java 作为实现语言。

Java 美食

Java 是一种很好的面向对象的语言,支持良好的设计。它包括一个 MIDI 播放器和 MIDI 类。它支持多种语言编码,所以很容易从 GB-2312 转换到 Unicode。它具有良好的跨平台 GUI 支持。

java 伙伴

Java 不支持无符号整数类型。这真的很糟糕,因为对于这些程序来说,很多数据类型都是无符号的。Java 中的偶数字节是有符号的。这里有一些窍门:

  • 使所有类型的大小增加:byte 到 int,int 到 long,long 到 long。只希望不需要无符号长整型。

  • 如果你需要一个无符号字节,你有一个 int,你需要它适合 8 位,转换成一个字节,希望它不要太大。

  • 到处进行类型转换以使编译器满意,例如当需要从 int,(byte) n中取出一个字节时。

  • 注意到处都是标志。如果要右移一个数字,运算符>>会保留符号扩展名,因此,例如,在二进制 1XYZ…中,会移至 1111XYZ…您需要使用>>>,结果为 0001XYZ。

  • 如果你想把一个无符号的字节赋给一个 int,再看一下 signs。您可能需要以下内容:

                  n = b ≥ 0 ? b : 256 - b
    
    
  • 要从两个无符号字节构建一个无符号 int,符号会再次填充你:n = (b1 << 8) + b2 会出错,如果 b1 或 b2 是-ve。而是用下面的:

                  n = ((b1 ≥ 0 ? b1 : 256 - b1) << 8) + (b2 ≥ 0 ? b2 : 256 - b2)
    
    

    (不开玩笑!)

班级

歌曲类SongInformation.java包含关于一首歌曲的信息,如下所示:

public class SongInformation {

    // Public fields of each song record
    /**
     *  Song number in the file, one less than in songbook
     */
    public long number;

    /**
     * song title in Unicode
     */
    public String title;

    /**
     * artist in Unicode
     */
    public String artist;

    /**
     * integer value of language code
     */
    public int language;

    public static final int  KOREAN = 0;
    public static final int  CHINESE1 = 1;
    public static final int  CHINESE2 = 2;
    public static final int  TAIWANESE3 = 3 ;
    public static final int  JAPANESE = 4;
    public static final int  RUSSIAN = 5;
    public static final int  THAI = 6;
    public static final int  TAIWANESE7 = 7;
    public static final int  CHINESE8 = 8;
    public static final int  CANTONESE = 9;
    public static final int  ENGLISH = 0x12;
    public static final int  VIETNAMESE = 0x13;
    public static final int  PHILIPPINE = 0x14;
    public static final int  TURKEY = 0x15;
    public static final int  SPANISH = 0x16;
    public static final int  INDONESIAN = 0x17;
    public static final int  MALAYSIAN = 0x18;
    public static final int  PORTUGUESE = 0x19;
    public static final int  FRENCH = 0x20;
    public static final int  INDIAN = 0x21;
    public static final int  BRASIL = 0x22;
    public static final int  CHINESE131 = 131;
    public static final int  ENGLISH146 = 146;
    public static final int  PHILIPPINE148 = 148;

    public SongInformation(long number,
                           String title,
                           String artist,
                           int language) {
        this.number = number;
        this.title = title;
        this.artist = artist;
        this.language = language;
    }

    public String toString() {
        return "" + (number+1) + " (" + language + ") \"" + title + "\" " + artist;
    }

    public boolean titleMatch(String pattern) {
        // System.out.println("Pattern: " + pattern);
        return title.matches("(?i).*" + pattern + ".*");
    }

    public boolean artistMatch(String pattern) {
        return artist.matches("(?i).*" + pattern + ".*");
    }

    public boolean numberMatch(String pattern) {
        Long n;
        try {
            n = Long.parseLong(pattern) - 1;
            //System.out.println("Long is " + n);
        } catch(Exception e) {
            //System.out.println(e.toString());
            return false;
        }
        return number == n;
    }

    public boolean languageMatch(int lang) {
        return language == lang;
    }
}

歌曲表类SongTable.java保存了歌曲信息对象的列表。

import java.util.Vector;
import java.io.FileInputStream;
import java.io.*;
import java.nio.charset.Charset;

// public class SongTable implements java.util.Iterator {
// public class SongTable extends  Vector<SongInformation> {
public class SongTable {

    private static final String SONG_INFO_FILE = "/home/newmarch/Music/karaoke/sonken/DTSMUS20.DKD";
    private static final long INFO_START = 0x9F23;

    public static final int ENGLISH = 0x12;

    private static Vector<SongInformation> allSongs;

    private Vector<SongInformation> songs =
        new Vector<SongInformation>  ();

    public static long[] langCount = new long[0x23];

    public SongTable(Vector<SongInformation> songs) {
        this.songs = songs;
    }

    public SongTable() throws java.io.IOException,
                              java.io.FileNotFoundException {
        FileInputStream fstream = new FileInputStream(SONG_INFO_FILE);
        fstream.skip(INFO_START);
        while (true) {
            int len;
            int lang;
            long number;

            len = fstream.read();
            lang = fstream.read();
            number = readShort(fstream);
            if (len == 0xFF && lang == 0xFF && number == 0xFFFFL) {
                break;
            }
            byte[] bytes = new byte[len - 4];
            fstream.read(bytes);
            int endTitle;
            // find null at end of title
            for (endTitle = 0; bytes[endTitle] != 0; endTitle++)
                ;
            byte[] titleBytes = new byte[endTitle];
            byte[] artistBytes = new byte[len - endTitle - 6];

            System.arraycopy(bytes, 0, titleBytes, 0, titleBytes.length);
            System.arraycopy(bytes, endTitle + 1,
                             artistBytes, 0, artistBytes.length);
            String title = toUnicode(lang, titleBytes);
            String artist = toUnicode(lang, artistBytes);
            // System.out.printf("artist: %s, title: %s, lang: %d, number %d\n", artist, title, lang, number);
            SongInformation info = new SongInformation(number,
                                                       title,
                                                       artist,
                                                       lang);
            songs.add(info);

            if (lang > 0x22) {
                //System.out.println("Illegal lang value " + lang + " at song " + number);
            } else {
                langCount[lang]++;
            }
        }
        allSongs = songs;
    }

    public void dumpTable() {
        for (SongInformation song: songs) {
            System.out.println("" + (song.number+1) + " - " +
                               song.artist + " - " +
                               song.title);
        }
    }

    public java.util.Iterator<SongInformation> iterator() {
        return songs.iterator();
    }

    private int readShort(FileInputStream f)  throws java.io.IOException {
        int n1 = f.read();
        int n2 = f.read();
        return (n1 << 8) + n2;
    }

    private String toUnicode(int lang, byte[] bytes) {
        switch (lang) {
        case SongInformation.ENGLISH:
        case SongInformation.ENGLISH146:
        case SongInformation.PHILIPPINE:
        case SongInformation.PHILIPPINE148:
            // case SongInformation.HINDI:
        case SongInformation.INDONESIAN:
        case SongInformation.SPANISH:
            return new String(bytes);

        case SongInformation.CHINESE1:
        case SongInformation.CHINESE2:
        case SongInformation.CHINESE8:
        case SongInformation.CHINESE131:
        case SongInformation.TAIWANESE3:
        case SongInformation.TAIWANESE7:
        case SongInformation.CANTONESE:
            Charset charset = Charset.forName("gb2312");
            return new String(bytes, charset);

        case SongInformation.KOREAN:
            charset = Charset.forName("euckr");
            return new String(bytes, charset);

        default:
            return "";
        }
    }

    public SongInformation getNumber(long number) {
        for (SongInformation info: songs) {
            if (info.number == number) {
                return info;
            }
        }
        return null;
    }

    public SongTable titleMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.titleMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

     public SongTable artistMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.artistMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

      public SongTable numberMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.numberMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

    public String toString() {
        StringBuffer buf = new StringBuffer();
        for (SongInformation song: songs) {
            buf.append(song.toString() + "\n");
        }
        return buf.toString();
    }

    public static void main(String[] args) {
        // for testing
        SongTable songs = null;
        try {
            songs = new SongTable();
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }
        songs.dumpTable();
        System.exit(0);

        // Should print "54151 Help Yourself Tom Jones"
        System.out.println(songs.getNumber(54150).toString());

        // Should print "18062 伦巴(恋歌) 伦巴"
        System.out.println(songs.getNumber(18061).toString());

        System.out.println(songs.artistMatches("Tom Jones").toString());
        /* Prints
54151 Help Yourself Tom Jones
50213 Daughter Of Darkness Tom Jones
23914 DELILAH Tom Jones
52834 Funny Familiar Forgotten Feelings Tom Jones
54114 Green green grass of home Tom Jones
54151 Help Yourself Tom Jones
55365 I (WHO HAVE NOTHING) TOM JONES
52768 I Believe Tom Jones
55509 I WHO HAVE NOTHING TOM JONES
55594 I'll Never Fall Inlove Again Tom Jones
55609 I'm Coming Home Tom Jones
51435 It's Not Unusual Tom Jones
55817 KISS Tom Jones
52842 Little Green Apples Tom Jones
51439 Love Me Tonight Tom Jones
56212 My Elusive Dream TOM JONES
56386 ONE DAY SOON Tom Jones
22862 THAT WONDERFUL SOUND Tom Jones
57170 THE GREEN GREEN GRASS OF HOME TOM JONES
57294 The Wonderful Sound Tom Jones
23819 TILL Tom Jones
51759 What's New Pussycat Tom Jones
52862 With These Hands Tom Jones
57715 Without Love Tom Jones
57836 You're My World Tom Jones
        */

        for (int n = 1; n < langCount.length; n++) {
            if (langCount[n] != 0) {
                System.out.println("Count: " + langCount[n] + " of lang " + n);
            }
        }

        // Check Russian, etc
        System.out.println("Russian " + '\u0411');
        System.out.println("Korean " + '\u0411');
        System.exit(0);
    }
}

您可能需要调整基于文件的构造函数中的常量值,这样才能正常工作。

使用 Swing 来显示和搜索歌曲标题的 Java 程序是SongTableSwing.java

import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.util.regex.*;
import java.io.*;

public class SongTableSwing extends JPanel {
   private DefaultListModel model = new DefaultListModel();
    private JList list;
    private static SongTable allSongs;

    private JTextField numberField;
    private JTextField langField;
    private JTextField titleField;
    private JTextField artistField;

    // This font displays Asian and European characters.
    // It should be in your distro.
    // Fonts displaying all Unicode are zysong.ttf and Cyberbit.ttf
    // See http://unicode.org/resources/fonts.html
    private Font font = new Font("WenQuanYi Zen Hei", Font.PLAIN, 16);
    // font = new Font("Bitstream Cyberbit", Font.PLAIN, 16);

    private int findIndex = -1;

    /**
     * Describe <code>main</code> method here.
     *
     * @param args a <code>String</code> value
     */
    public static final void main(final String[] args) {
        allSongs = null;
        try {
            allSongs = new SongTable();
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }

        JFrame frame = new JFrame();
        frame.setTitle("Song Table");
        frame.setSize(1000, 800);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        SongTableSwing panel = new SongTableSwing(allSongs);
        frame.getContentPane().add(panel);

        frame.setVisible(true);
    }

    public SongTableSwing(SongTable songs) {

        if (font == null) {
            System.err.println("Can't fnd font");
        }

        int n = 0;
        java.util.Iterator<SongInformation> iter = songs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
            // model.add(n++, iter.next().toString());
        }

        BorderLayout mgr = new BorderLayout();

        list = new JList(model);
        // list = new JList(songs);
        list.setFont(font);
        JScrollPane scrollPane = new JScrollPane(list);

        setLayout(mgr);
        add(scrollPane, BorderLayout.CENTER);

        JPanel bottomPanel = new JPanel();
        bottomPanel.setLayout(new GridLayout(2, 1));
        add(bottomPanel, BorderLayout.SOUTH);

        JPanel searchPanel = new JPanel();
        bottomPanel.add(searchPanel);
        searchPanel.setLayout(new FlowLayout());

        JLabel numberLabel = new JLabel("Number");
        numberField = new JTextField(5);

        JLabel langLabel = new JLabel("Language");
        langField = new JTextField(8);

        JLabel titleLabel = new JLabel("Title");
        titleField = new JTextField(20);
        titleField.setFont(font);

        JLabel artistLabel = new JLabel("Artist");
        artistField = new JTextField(10);
        artistField.setFont(font);

        searchPanel.add(numberLabel);
        searchPanel.add(numberField);
        // searchPanel.add(langLabel);
        // searchPanel.add(langField);
        searchPanel.add(titleLabel);
        searchPanel.add(titleField);
        searchPanel.add(artistLabel);
        searchPanel.add(artistField);

        titleField.getDocument().addDocumentListener(new DocumentListener() {
                public void changedUpdate(DocumentEvent e) {
                    // rest find to -1 to restart any find searches
                    findIndex = -1;
                    // System.out.println("reset find index");
                }
                public void insertUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void removeUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset remove find index");
                }
            }
            );
        artistField.getDocument().addDocumentListener(new DocumentListener() {
                public void changedUpdate(DocumentEvent e) {
                    // rest find to -1 to restart any find searches
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void insertUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void removeUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
            }
            );

        titleField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});
        artistField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});
        numberField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});

        JPanel buttonPanel = new JPanel();
        bottomPanel.add(buttonPanel);
        buttonPanel.setLayout(new FlowLayout());

        JButton find = new JButton("Find");
        JButton filter = new JButton("Filter");
        JButton reset = new JButton("Reset");
        JButton play = new JButton("Play");
        buttonPanel.add(find);
        buttonPanel.add(filter);
        buttonPanel.add(reset);
        buttonPanel.add(play);

        find.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    findSong();
                }
            });

        filter.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    filterSongs();
                }
            });

        reset.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    resetSongs();
                }
            });

        play.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    playSong();
                }
            });

     }

    public void findSong() {
        String number = numberField.getText();
        String language = langField.getText();
        String title = titleField.getText();
        String artist = artistField.getText();

        if (number.length() != 0) {
            try {

                long num = Integer.parseInt(number) - 1;
                for (int n = 0; n < model.getSize(); n++) {
                    SongInformation info = (SongInformation) model.getElementAt(n);
                    if (info.number == num) {
                        list.setSelectedIndex(n);
                        list.ensureIndexIsVisible(n);
                        return;
                    }
                }
            } catch(Exception e) {
                System.err.println("Not a number");
                numberField.setText("");
            }

            return;
        }

        /*
        System.out.println("Title " + title + title.length() +
                           "artist " + artist + artist.length() +
                           " find start " + findIndex +
                           " model size " + model.getSize());
        if (title.length() == 0 && artist.length() == 0) {
            System.err.println("no search terms");
            return;
        }
        */

        //System.out.println("Search " + searchStr + " from index " + findIndex);
        for (int n = findIndex + 1; n < model.getSize(); n++) {
            SongInformation info = (SongInformation) model.getElementAt(n);
            //System.out.println(info.toString());

            if ((title.length() != 0) && (artist.length() != 0)) {
                if (info.titleMatch(title) && info.artistMatch(artist)) {
                    // System.out.println("Found " + info.toString());
                        findIndex = n;
                        list.setSelectedIndex(n);
                        list.ensureIndexIsVisible(n);
                        break;
                }
            } else {
                if ((title.length() != 0) && info.titleMatch(title)) {
                    // System.out.println("Found " + info.toString());
                    findIndex = n;
                    list.setSelectedIndex(n);
                    list.ensureIndexIsVisible(n);
                    break;
                } else if ((artist.length() != 0) && info.artistMatch(artist)) {
                    // System.out.println("Found " + info.toString());
                    findIndex = n;
                    list.setSelectedIndex(n);
                    list.ensureIndexIsVisible(n);
                    break;

                }
            }

        }
    }

    public void filterSongs() {
        String title = titleField.getText();
        String artist = artistField.getText();
        String number = numberField.getText();
        SongTable filteredSongs = allSongs;

        if (allSongs == null) {
            // System.err.println("Songs is null");
            return;
        }

        if (title.length() != 0) {
            filteredSongs = filteredSongs.titleMatches(title);
        }
        if (artist.length() != 0) {
            filteredSongs = filteredSongs.artistMatches(artist);
        }
        if (number.length() != 0) {
            filteredSongs = filteredSongs.numberMatches(number);
        }

        model.clear();
        int n = 0;
        java.util.Iterator<SongInformation> iter = filteredSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }
    }

    public void resetSongs() {
        artistField.setText("");
        titleField.setText("");
        numberField.setText("");
        model.clear();
        int n = 0;
        java.util.Iterator<SongInformation> iter = allSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }
    }
    /**
     * "play" a song by printing its id to standard out.
     * Can be used in a pipeline this way
     */
    public void playSong() {
        SongInformation song = (SongInformation) list.getSelectedValue();
        if (song == null) {
            return;
        }
        long number = song.number + 1;

        System.out.println("" + number);
    }

    class SongInformationRenderer extends JLabel implements ListCellRenderer {

        public Component getListCellRendererComponent(
                                                      JList list,
                                                      Object value,
                                                      int index,
                                                      boolean isSelected,
                                                      boolean cellHasFocus) {
            setText(value.toString());
            return this;
        }
    }
}

当选择 Play 时,它会将歌曲 ID 打印到标准输出,以便在管道中使用。

数据文件

以下部分将介绍数据文件。

一般

文件DTSMUS00.DKDDTSMUS07.DKD包含音乐文件。音乐有两种格式:微软 WMA 文件和 MIDI 文件。在我的歌本里,有些歌是标注有歌手的。这些是 WMA 的档案。没有歌手的是 MIDI 文件。

WMA 的文件就是这样。MIDI 文件被略微压缩,在播放之前必须解码。

每个歌曲块在开头都有一个包含歌词的部分。这些是压缩的,必须解码。

一首歌曲的数据形成一个连续字节的记录。这些记录被收集成块,也是连续的。这两块是分开的。有一个指向这些块的指针“超级块”。歌曲号的一部分是超级块的索引,选择该块。歌曲号的其余部分是块中记录的索引。

我的路线

我回到了这一点,并在一段时间后才理解了其他人的成就。所以,为了帮助其他人,这是我的路线。

我用 Unix 命令strings发现了DTSMUS10.DKD中的歌曲信息。在其他文件中,它似乎没有产生太多。但是这些文件中有 ASCII 字符串,有些是重复的。所以,我写了一个 shell 管道来对这些字符串进行排序和计数。一个文件的管道如下:

          strings DTSMUS05.DKD | sort |uniq -c | sort -n -r |less

这产生了以下结果:

          1229 :^y|
          1018 j?wK
          843 ]/<
          756  Seh
          747  Ser
          747 _\D+P
          674 :^yt
          234 IRI$

结果并不令人鼓舞。但是,当我查看文件内部以查看“Ser”出现的位置时,我还看到了以下内容:

          q03C3E230  F6 01 00 00 00 02 00 16 00 57 00 69 00 6E 00 64 .........W.i.n.d
          03C3E240  00 6F 00 77 00 73 00 20 00 4D 00 65 00 64 00 69 .o.w.s. .M.e.d.i
          03C3E250  00 61 00 20 00 41 00 75 00 64 00 69 00 6F 00 20 .a. .A.u.d.i.o.
          03C3E260  00 39 00 00 00 24 00 20 00 34 00 38 00 20 00 6B .9...$. .4.8\. .k
          03C3E270  00 62 00 70 00 73 00 2C 00 20 00 34 00 34 00 20 .b.p.s.,. .4.4.
          03C3E280  00 6B 00 48 00 7A 00 2C 00 20 00 73 00 74 00 65 .k.H.z.,. .s.t.e
          03C3E290  00 72 00 65 00 6F 00 20 00 31 00 2D 00 70 00 61 .r.e.o. .1.-.p.a
          03C3E2A0  00 73 00 73 00 20 00 43 00 42 00 52 00 00 00 02 .s.s. .C.B.R....
          03C3E2B0  00 61 01 91 07 DC B7 B7 A9 CF 11 8E E6 00 C0 0C .a..............
          03C3E2C0  20 53 65 72 00 00 00 00 00 00 00 40 9E 69 F8 4D  Ser.......@.i.M

哇哦!双字节字符!

strings命令有选项可以查看,例如,2 字节的大端字符串。命令

          strings -e b DTSMUS05.DKD

发现了这个:

          IsVBR
          DeviceConformanceTemplate
          WM/WMADRCPeakReference
          WM/WMADRCAverageReference
          WMFSDKVersion
          9.00.00.2980
          WMFSDKNeeded
          0.0.0.0000

这些都是 WMA 格式的一部分。

根据加里·凯斯勒的文件签名表( www.garykessler.net/library/file_sigs.html ),WMA 文件的签名由下面所示的标题给出:

          30 26 B2 75 8E 66 CF 11
          A6 D9 00 AA 00 62 CE 6C

这种模式确实会出现,之前的字符串会在一段时间后出现。

ASF/WMA 文件格式的规范在 www.microsoft.com/download/en/details.aspx?displaylang=en&id=14995 ,尽管建议你不要阅读它,以防你想对这样的文件做任何开源的事情。

因此,在此基础上,我可以确定 WMA 文件的开始。每个 WMA 文件前面的四个字节是文件的长度。从那里我可以找到文件的结尾,它原来是下一个包含一些内容的记录的开始,然后是下一个 WMA 文件。

在这些记录中,我可以看到我无法理解的模式,但从字节 36 开始,我可以看到类似这样的字符串:

          AIN'T IT FUNNY HOW TIME SLIPS AWAY, Str length: 34

          00000000  10 50 41 10 50 49 10 50 4E 10 50 27 10 50 54 10 .PA.PI.PN.P'.PT.
          00000010  50 20 11 F1 25 12 71 05 04 61 05 05 51 21 13 01 P ..%.q..a..Q!..
          00000020  02 05 91 2B 10 20 48 10 50 4F 10 50 57 13 40 00 ...+. H.PO.PW.@.
          00000030  12 61 02 12 01 02 04 D1 05 04 51 3B 05 31 05 04 .a........Q;.1..
          00000040  C1 29 10 20 50 10 51 45 10 21 28 10 21 1E 10 21 .). P.QE.!(.!..!
          00000050  3A 14 F1 05 13 31 02 10 C1 0E 11 A1 58 15 A0 00 :....1......X...
          00000060  15 70 00 13 A0 A9                               .p....

能看到AIN'T(作为.PA.PI.PN.P'.PT)吗?

但是我不知道编码是什么,也不知道如何找到歌曲开始的表格。那时,我准备看看早期的东西,并了解它如何适用于我。(参见《了解加州电子 DVD 上的热狗文件》( http://old.nabble.com/Understanding-the-HOTDOG-files-on-DVD-of-California-electronics-td11359745.html )、《解码 JBK 6628 DVD Karaoke 碟》( http://old.nabble.com/Decoding-JBK-6628-DVD-Karaoke-Disc-td12261269.html )、《Karaoke Huyndai 99》(http://board.midibuddy.net/showpost.php?p=533722&postcount=31)。

超级街区

文件DTSMUS00.DKD以一串空值开始。在 0x200 处,它开始输入数据。这被认为是“表的表”的开始,换句话说,是一个超级块。这个超级块中的每个条目都是一个 4 字节的整数,它是数据文件中表的索引。超级块由一系列空值终止(对我来说是 0x5F4),表中的索引少于 256 个。

这些超级块条目的值似乎在不同的版本中发生了变化。在 JBK 光盘和我的光盘中,这些值必须乘以 0x800 才能在数据文件中给出一个“虚拟偏移量”。

为了说明这一点,在我的光盘 0x200 上有以下内容:

          00000200  00 00 00 01 00 00 08 6C 00 00 0F C1 00 00 17 7A
          00000210  00 00 1E 81 00 00 25 21 00 00 2B 8D 00 00 32 B7

因此,表值为 0x1、0x86C、0xFC1、0x177A、....“虚拟地址”是 0x800、0x436000 (0x86C * 0x800)等等。如果你去这些地址,你会看到地址前是一堆空值,在那个地址是数据。

我称它们为虚拟地址,因为在我的 DVD 上有八个数据文件,并且大多数地址比任何一个文件都大。我这里的文件(除了最后一个)都是 1065353216L 字节。“显而易见”的解决方案是可行的:文件号是地址/文件大小,文件中的偏移量是地址百分比文件大小。您可以通过查找每个块地址前的空值来检查这一点。

歌曲开始表

从超级块索引的每个表都是歌曲索引表。每个表包含 4 字节的索引。每个表最多有 0x100 个条目,或者以零索引结束。每个索引是从歌曲条目开始的表格开始的偏移。

从歌曲编号定位歌曲条目

给定一个歌曲编号,比如 54154,“太阳来了”,您现在可以找到歌曲条目。将歌曲编号减少 1 至 54153。它是一个 16 位的数字。最高 8 位是超级块中歌曲索引表的索引。底部的 8 位是歌曲索引表中歌曲条目的索引。

下面是伪代码:

          songNumber = get number for song from DTSMUS20.DKD
          superBlockIdx = songNumber >>
          indexTableIdx = songNumber & 0xFF

          seek(DTSMUS00.DKD, superBlockIdx)
          superBlockValue = read 4-byte int from DTSMUS00.DKD

          locationIndexTable = superBlockValue * 0x800
          fileNumber = locationIndexTable / fileSize
          indexTableStart = locationIndexTable % fileSize
          entryLocation = indexTableStart + indexTableIdx

          seek(fileNumber, entryLocation)
          read song entry

歌曲条目

每个歌曲条目都有一个标题,后面是两个块,我称之为信息块和歌曲数据块。每个标题块有一个 2 字节的类型码和一个 2 字节的整数长度。类型代码为 0x0800 或 0x0000。代码表示歌曲数据的编码:0x0800 是 WMA 文件,而 0x0000 是 MIDI 文件。

如果类型码是 0x0 比如披头士的“救命!”(歌曲号 51765),则信息块具有标题块中的长度,并从 12 个字节开始。歌曲数据块紧随其后。

如果类型代码是 0x8000,则信息块从 4 个字节开始,长度为报头中给定的长度。歌曲块从信息块末尾的下一个 16 字节边界开始。

歌曲块以一个 4 字节的头开始,这是所有类型的歌曲数据的长度。

歌曲数据

如果歌曲类型是 0x8000,则歌曲数据是 WMA 文件。所有歌曲都有一个歌手包含在这个文件中。

如果歌曲类型是 0x0,那么(从书中)在所查看的歌曲中没有歌手。该文件被编码和解码为 MIDI 文件。

解码 MIDI 文件

所有文件都有一个歌词块,后跟一个音乐块。歌词块被压缩,并且已经发现这是 LZW 压缩。这会解压缩成一组 4 字节的块。前两个字节是歌词的字符。对于 1 字节编码,如英语或越南语,第一个字节是一个字符,第二个字节是零或另一个字符(两个字节,如\r\n)。对于双字节编码,如 GB-2312,两个字节构成一个字符。

接下来的两个字节是字符串播放的时间长度。

歌词块

每个歌词块都以""#0001 @@00@12 @Help Yourself @ @@Tom Jones"这样的字符串开头。这里的语言代码是@00@NN中的NN。歌名,作词人,歌手都很清楚。(注意:这些字符都是相隔 4 个字节的!)对于英语,是 12 等等。

每个块的字节 0 和 1 是歌词中的一个字符。字节 2 和 3 是每个字符的持续时间。要将它们转换成 MIDI 数据,必须将持续时间转换成每个字符的开始/停止。

我做这个的 Java 程序是SongExtracter.java

import java.io.*;
import javax.sound.midi.*;
import java.nio.charset.Charset;

public class SongExtracter {
    private static final boolean DEBUG = false;

    private String[] dataFiles = new String[] {
        "DTSMUS00.DKD", "DTSMUS01.DKD", "DTSMUS02.DKD",
        "DTSMUS03.DKD", "DTSMUS04.DKD", "DTSMUS05.DKD",
        "DTSMUS06.DKD", "DTSMUS07.DKD"};
    private String superBlockFileName = dataFiles[0];
    private static final String DATADIR = "/home/newmarch/Music/karaoke/sonken/";
    private static final String SONGDIR ="/home/newmarch/Music/karaoke/sonken/songs/";
    //private static final String SONGDIR ="/server/KARAOKE/KARAOKE/Sonken/";
    private static final long SUPERBLOCK_OFFSET = 0x200;
    private static final long BLOCK_MULTIPLIER = 0x800;
    private static final long FILE_SIZE = 0x3F800000L;

    private static final int SIZE_UINT = 4;

    private static final int SIZE_USHORT = 2;

    private static final int ENGLISH = 12;

    public RawSong getRawSong(int songNumber)
        throws java.io.IOException,
               java.io.FileNotFoundException {
        if (songNumber < 1) {
            throw new FileNotFoundException();
        }

        // song number in files is one less than song number in books, so
        songNumber--;

        long locationIndexTable = getTableIndexFromSuperblock(songNumber);
        debug("Index table at %X\n", locationIndexTable);

        long locationSongDataBlock = getSongIndex(songNumber, locationIndexTable);

        // Now we are at the start of the data block
        return readRawSongData(locationSongDataBlock);

        //debug("Data block at %X\n", songStart);
    }

    private long getTableIndexFromSuperblock(int songNumber)
        throws java.io.IOException,
               java.io.FileNotFoundException {
        // index into superblock of table of song offsets
        int superBlockIdx = songNumber >> 8;

        debug("Superblock index %X\n", superBlockIdx);

        File superBlockFile = new File(DATADIR + superBlockFileName);

        FileInputStream fstream = new FileInputStream(superBlockFile);

        fstream.skip(SUPERBLOCK_OFFSET + superBlockIdx * SIZE_UINT);
        debug("Skipping to %X\n", SUPERBLOCK_OFFSET + superBlockIdx*4);
        long superBlockValue = readUInt(fstream);

        // virtual address of the index table for this song
        long locationIndexTable = superBlockValue * BLOCK_MULTIPLIER;

        return locationIndexTable;
    }

    /*
     * Virtual address of song data block
     */
    private long getSongIndex(int songNumber, long locationIndexTable)
        throws java.io.IOException,
               java.io.FileNotFoundException {
        // index of song into table of song ofsets
        int indexTableIdx = songNumber & 0xFF;
        debug("Index into index table %X\n", indexTableIdx);

        // translate virtual address to physical address
        int whichFile = (int) (locationIndexTable / FILE_SIZE);
        long indexTableStart =  locationIndexTable % FILE_SIZE;
        debug("Which file %d index into file %X\n", whichFile, indexTableStart);

        File songDataFile = new File(DATADIR + dataFiles[whichFile]);
        FileInputStream dataStream = new FileInputStream(songDataFile);
        dataStream.skip(indexTableStart + indexTableIdx * SIZE_UINT);
        debug("Song data index is at %X\n", indexTableStart + indexTableIdx*SIZE_UINT);

        long songStart = readUInt(dataStream) + indexTableStart;

        return songStart + whichFile * FILE_SIZE;
    }

    private RawSong readRawSongData(long locationSongDataBlock)
        throws java.io.IOException {
        int whichFile = (int) (locationSongDataBlock / FILE_SIZE);
        long dataStart =  locationSongDataBlock % FILE_SIZE;
        debug("Which song file %d  into file %X\n", whichFile, dataStart);

        File songDataFile = new File(DATADIR + dataFiles[whichFile]);
        FileInputStream dataStream = new FileInputStream(songDataFile);
        dataStream.skip(dataStart);

        RawSong rs = new RawSong();
        rs.type = readUShort(dataStream);
        rs.compressedLyricLength = readUShort(dataStream);
        // discard next short
        readUShort(dataStream);
        rs.uncompressedLyricLength = readUShort(dataStream);
        debug("Type %X, cLength %X uLength %X\n", rs.type, rs.compressedLyricLength, rs.uncompressedLyricLength);

        // don't know what the next word is for, skip it
        //dataStream.skip(4);
        readUInt(dataStream);

        // get the compressed lyric
        rs.lyric = new byte[rs.compressedLyricLength];
        dataStream.read(rs.lyric);

        long toBoundary = 0;
        long songLength = 0;
        long uncompressedSongLength = 0;

        // get the song data

        if (rs.type == 0) {
            // Midi file starts in 4 bytes time
            songLength = readUInt(dataStream);
            uncompressedSongLength = readUInt(dataStream);
            System.out.printf("Song data length %d, uncompressed %d\n",
                              songLength, uncompressedSongLength);
            rs.uncompressedSongLength = uncompressedSongLength;

            // next word is language again?
            //toBoundary = 4;
            //dataStream.skip(toBoundary);
            readUInt(dataStream);
        } else {
            // WMA starts on next 16-byte boundary
            if( (dataStart + rs.compressedLyricLength + 12) % 16 != 0) {
                // dataStart already on 16-byte boundary, so just need extra since then
                toBoundary = 16 - ((rs.compressedLyricLength + 12) % 16);
                debug("Read lyric data to %X\n", dataStart + rs.compressedLyricLength + 12);
                debug("Length %X to boundary %X\n", rs.compressedLyricLength, toBoundary);
                dataStream.skip(toBoundary);
            }
            songLength = readUInt(dataStream);
        }

        rs.music = new byte[(int) songLength];
        dataStream.read(rs.music);

        return rs;
    }

    private long readUInt(InputStream is) throws IOException {
        long val = 0;
        for (int n = 0; n < SIZE_UINT; n++) {
            int c = is.read();
            val = (val << 8) + c;
        }
        debug("ReadUInt %X\n", val);

        return val;
    }

    private int readUShort(InputStream is) throws IOException {
        int val = 0;
        for (int n = 0; n < SIZE_USHORT; n++) {
            int c = is.read();
            val = (val << 8) + c;
        }
        debug("ReadUShort %X\n", val);
        return val;
    }

    void debug(String f, Object ...args) {
        if (DEBUG) {
            System.out.printf("Debug: " + f, args);
        }
    }

    public Song getSong(RawSong rs) {
        Song song;
        if (rs.type == 0x8000) {
            song = new WMASong(rs);
        } else {
            song = new MidiSong(rs);
        }
        return song;
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java SongExtractor <song numnber>");
            System.exit(1);
        }

        SongExtracter se = new SongExtracter();
        try {
            RawSong rs = se.getRawSong(Integer.parseInt(args[0]));

            rs.dumpToFile(args[0]);

            Song song = se.getSong(rs);
            song.dumpToFile(args[0]);
            song.dumpLyric();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    private class RawSong {
        /**
         * type == 0x0 is Midi
         * type == 0x8000 is WMA
         */
        public int type;
        public int compressedLyricLength;
        public int uncompressedLyricLength;
        public long uncompressedSongLength; // only needed for compressed Midi
        public byte[] lyric;
        public byte[] music;

        public void dumpToFile(String fileName) throws IOException {
            FileOutputStream fout = new FileOutputStream(SONGDIR + fileName + ".lyric");
            fout.write(lyric);
            fout.close();

            fout = new FileOutputStream(SONGDIR + fileName + ".music");
            fout.write(music);
            fout.close();
        }
    }

    private class Song {
        public int type;
        public byte[] lyric;
        public byte[] music;
        protected Sequence sequence;
        protected int language = -1;

        public Song(RawSong rs) {

            type = rs.type;
            lyric = decodeLyric(rs.lyric,
                                rs.uncompressedLyricLength);
        }

        /**
         * Raw lyric is LZW compressed. Decompress it
         */
        public byte[] decodeLyric(byte[] compressedLyric, long uncompressedLength) {
            // uclen is short by at least 2 - other code adds 10 so we do too
            // TODO: change LZW to use a Vector to build result so we don't have to guess at length
            byte[] result = new byte[(int) uncompressedLength + 10];
            LZW lzw = new LZW();
            int len = lzw.expand(compressedLyric, compressedLyric.length, result);
            System.out.printf("uncompressedLength %d, actual %d\n", uncompressedLength, len);
            lyric = new byte[len];
            System.arraycopy(result, 0, lyric, 0, (int) uncompressedLength);
            return lyric;
        }

        public void dumpToFile(String fileName) throws IOException {
            FileOutputStream fout = new FileOutputStream(SONGDIR + fileName + ".decodedlyric");
            fout.write(lyric);
            fout.close();

            fout = new FileOutputStream(SONGDIR + fileName + ".decodedmusic");
            fout.write(music);

            fout.close();

            fout = new FileOutputStream(SONGDIR + fileName + ".mid");
            if (sequence == null)  {
                System.out.println("Seq is null");
            } else {
                // type is MIDI type 0
                MidiSystem.write(sequence, 0, fout);
            }
        }

        public void dumpLyric() {
            for (int n = 0; n < lyric.length; n += 4) {
                if (lyric[n] == '\r') {
                    System.out.println();
                } else {
                    System.out.printf("%c", lyric[n] & 0xFF);
                }
            }
            System.out.println();
            System.out.printf("Language is %X\n", getLanguageCode());
        }

        /**
         * Lyric contains the language code as string @00@NN in header section
         */
        public int getLanguageCode() {
            int lang = 0;

            // Look for @00@NN and return NN
            for (int n = 0; n < lyric.length-20; n += 4) {
                if (lyric[n] == (byte) '@' &&
                    lyric[n+4] == (byte) '0' &&
                    lyric[n+8] == (byte) '0' &&
                    lyric[n+12] == (byte) '@') {
                    lang = ((lyric[n+16]-'0') << 4) + lyric[n+20]-'0';
                    break;

                }
            }
            return lang;
        }

        /**
         * Lyric is in a language specific encoding. Translate to Unicode UTF-8.
         * Not all languages are handled because I don't have a full set of examples
         */
        public byte[] lyricToUnicode(byte[] bytes) {
            if (language == -1) {
                language = getLanguageCode();
            }
            switch (language) {
            case SongInformation.ENGLISH:
                return bytes;

            case SongInformation.KOREAN: {
                Charset charset = Charset.forName("gb2312");
                String str = new String(bytes, charset);
                bytes = str.getBytes();
                System.out.println(str);
                return bytes;
            }

            case SongInformation.CHINESE1:
            case SongInformation.CHINESE2:
            case SongInformation.CHINESE8:
            case SongInformation.CHINESE131:
            case SongInformation.TAIWANESE3:
            case SongInformation.TAIWANESE7:
            case SongInformation.CANTONESE:
                Charset charset = Charset.forName("gb2312");
                String str = new String(bytes, charset);
                bytes = str.getBytes();
                System.out.println(str);
                return bytes;
            }
            // language not handled
            return bytes;

        }

        public void durationToOnOff() {

        }

        public Track createSequence() {
            Track track;

            try {
                sequence = new Sequence(Sequence.PPQ, 30);
            } catch(InvalidMidiDataException e) {
                // help!!!
            }
            track = sequence.createTrack();
            addLyricToTrack(track);
            return track;
        }

        public void addMsgToTrack(MidiMessage msg, Track track, long tick) {
            MidiEvent midiEvent = new MidiEvent(msg, tick);

            // No need to sort or delay insertion. From the Java API
            // "The list of events is kept in time order, meaning that this
            // event inserted at the appropriate place in the list"
            track.add(midiEvent);
        }

        /**
         * return byte as int, converting to unsigned if needed
         */
        protected int ub2i(byte b) {
            return  b >= 0 ? b : 256 + b;
        }

        public void addLyricToTrack(Track track) {
            long lastDelay = 0;
            int offset = 0;

            int data0;
            int data1;
            final int LYRIC = 0x05;
            MetaMessage msg;

            while (offset < lyric.length-4) {
                int data3 = ub2i(lyric[offset+3]);
                int data2 = ub2i(lyric[offset+2]);
                data0 = ub2i(lyric[offset]);
                data1 = ub2i(lyric[offset+1]);

                long delay = (data3 << 8) + data2;

                offset += 4;
                byte[] data;
                int len;
                long tick;

                //System.out.printf("Lyric offset %X char %X after %d with delay %d made of %d %d\n", offset, data0, lastDelay, delay, lyric[offset-1], lyric[offset-2]);

                if (data1 == 0) {
                    data = new byte[] {(byte) data0}; //, (byte) MetaMessage.META};
                } else {
                    data = new byte[] {(byte) data0, (byte) data1}; // , (byte) MetaMessage.META};
                }
                data = lyricToUnicode(data);

                msg = new MetaMessage();

                if (delay > 0) {
                    tick = delay;
                    lastDelay = delay;
                } else {
                    tick = lastDelay;
                }

                try {
                    msg.setMessage(LYRIC, data, data.length);
                } catch(InvalidMidiDataException e) {
                    e.printStackTrace();
                    continue;

                }
                addMsgToTrack(msg, track, tick);
            }
        }

    }

    private class WMASong extends Song {

        public WMASong(RawSong rs) {
            // We want to decode the lyric, but just copy the music data
            super(rs);
            music = rs.music;
            createSequence();
        }

        public void dumpToFile(String fileName) throws IOException {
            System.out.println("Dumping WMA to " + fileName + ".wma");
            super.dumpToFile(fileName);
            FileOutputStream fout = new FileOutputStream(fileName + ".wma");
            fout.write(music);
            fout.close();
        }

    }

    private class MidiSong extends Song {

        private String[] keyNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};

        public MidiSong(RawSong rs) {
            // We want the decoded lyric plus also need to decode the music
            // and then turn it into a Midi sequence
            super(rs);
            decodeMusic(rs);
            createSequence();

        }

        public void dumpToFile(String fileName) throws IOException {
            System.out.println("Dumping Midi to " + fileName);
            super.dumpToFile(fileName);
        }

        public String getKeyName(int nKeyNumber)
        {
            if (nKeyNumber > 127)
                {
                    return "illegal value";
                }
            else
                {
                    int     nNote = nKeyNumber % 12;
                    int     nOctave = nKeyNumber / 12;
                    return keyNames[nNote] + (nOctave - 1);
                }
        }

        public byte[] decodeMusic(RawSong rs) {
            byte[]  compressedMusic = rs.music;
            long uncompressedSongLength = rs.uncompressedSongLength;

            // TODO: change LZW to use a Vector to build result so we don't have to guess at length
            byte[] expanded = new byte[(int) uncompressedSongLength + 20];
            LZW lzw = new LZW();
            int len = lzw.expand(compressedMusic, compressedMusic.length, expanded);
            System.out.printf("Uncompressed %d, Actual %d\n", compressedMusic.length, len);
            music = new byte[len];
            System.arraycopy(expanded, 0, music, 0, (int) len);

            return music;

        }

        public Track createSequence() {
            Track track = super.createSequence();
            addMusicToTrack(track);
            return track;
        }

        public void addMusicToTrack(Track track) {
            int timeLine = 0;
            int offset = 0;
            int midiChannelNumber = 1;

            /* From http://board.midibuddy.net/showpost.php?p=533722&postcount=31
               Block of 5 bytes :
               xx xx xx xx xx
               1st byte = Delay Time
               2nd byte = Delay Time when the velocity will be 0,
               this one will generate another midi event
               with velocity 0 (see above).
               3nd byte = Event, for example 9x : Note On for channel x+1,
               cx for PrCh, bx for Par, ex for Pitch Bend....
               4th byte = Note
               5th byte = Velocity
            */
            System.out.println("Adding music to track");
            while (offset < music.length - 5) {

                int startDelayTime = ub2i(music[offset++]);
                int endDelayTime = ub2i(music[offset++]);
                int event = ub2i(music[offset++]);
                int data1 = ub2i(music[offset++]);
                int data2 = ub2i(music[offset++]);

                int tick = timeLine + startDelayTime;
                System.out.printf("Offset %X event %X timeline %d\n", offset, event & 0xFF, tick);

                ShortMessage msg = new ShortMessage();
                ShortMessage msg2 = null;

                try {
                    // For Midi event types see http://www.midi.org/techspecs/midimessages.php
                    switch (event & 0xF0) {
                    case ShortMessage.CONTROL_CHANGE:  // Control Change 0xB0
                    case ShortMessage.PITCH_BEND:  // Pitch Wheel Change 0xE0
                        msg.setMessage(event, data1, data2);
                        /*
                          writeChannel(midiChannelNumber, chunk[2], false);
                          writeChannel(midiChannelNumber, chunk[3], false);
                          writeChannel(midiChannelNumber, chunk[4], false);
                        */
                        break;

                    case ShortMessage.PROGRAM_CHANGE: // Program Change 0xC0
                    case ShortMessage.CHANNEL_PRESSURE: // Channel Pressure (After-touch) 0xD0
                        msg.setMessage(event, data1, 0);
                        break;

                    case 0x00:
                        // case 0x90:
                        // Note on
                        int note = data1;
                        int velocity = data2;

                        /* We have to generate a pair of note on/note off.
                           The C code manages getting the order of events

                           done correctly by keeping a list of note off events
                           and sticking them into the Midi sequence when appropriate.
                           The Java add() looks after timing for us, so we'll
                           generate a note off first and add it, and then do the note on
                        */
                        System.out.printf("Note on %s at %d, off at %d at offset %X channel %d\n",
                                          getKeyName(note),
                                          tick, tick + endDelayTime, offset, (event &0xF)+1);
                        // ON
                        msg.setMessage(ShortMessage.NOTE_ON | (event & 0xF),
                                       note, velocity);

                        // OFF
                        msg2 = new ShortMessage();
                        msg2.setMessage(ShortMessage.NOTE_OFF  | (event & 0xF),
                                        note, velocity);

                        break;

                    case 0xF0: // System Exclusive
                        // We'll write the data as is to the buffer
                        offset -= 3;
                        // msg = SysexMessage();
                        while (music[offset] != (byte) 0xF7) // bytes only go upto 127 GRRRR!!!
                            {
                                //writeChannel(midiChannelNumber, midiData[midiOffset], false);
                                System.out.printf("sysex: %x\n", music[offset]);
                                offset++;
                                if (offset >= music.length) {
                                    System.err.println("Run off end of array while processing Sysex");
                                    break;
                                }

                            }
                        //writeChannel(midiChannelNumber, midiData[midiOffset], false);
                        offset++;
                        System.out.printf("Ignoring sysex %02X\n", event);

                        // ignore the message for now
                        continue;
                        // break;

                    default:
                        System.out.printf("Unrecognized code %02X\n", event);
                        continue;
                    }
                } catch(InvalidMidiDataException e) {
                    e.printStackTrace();
                }

                addMsgToTrack(msg, track, tick);
                if (msg2 != null ) {
                    if (endDelayTime <= 0) System.out.println("Start and end at same time");
                    addMsgToTrack(msg2, track, tick + endDelayTime);
                    msg2 = null;
                }

                timeLine = tick;
            }
        }
    }
}

支持类在LZW.java里。

/**
 * Based on code by Mark Nelson
 * http://marknelson.us/1989/10/01/lzw-data-compression/
 */

public class LZW {

    private final int BITS = 12;                   /* Setting the number of bits to 12, 13*/
    private final int HASHING_SHIFT = (BITS-8);    /* or 14 affects several constants.    */
    private final int MAX_VALUE = (1 << BITS) - 1; /* Note that MS-DOS machines need to   */
    private final int MAX_CODE = MAX_VALUE - 1;    /* compile their code in large model if*/
    /* 14 bits are selected.               */

    private final int TABLE_SIZE = 5021;           /* The string table size needs to be a */
    /* prime number that is somewhat larger*/
    /* than 2**BITS.                       */
    private final int NEXT_CODE = 257;

    private long[] prefix_code = new long[TABLE_SIZE];;        /* This array holds the prefix codes   */
    private int[] append_character = new int[TABLE_SIZE];      /* This array holds the appended chars */
    private int[] decode_stack; /* This array holds the decoded string */

    private int input_bit_count=0;
    private long input_bit_buffer=0; // must be 32 bits
    private int offset = 0;

    /*
    ** This routine simply decodes a string from the string table, storing
    ** it in a buffer.  The buffer can then be output in reverse order by
    ** the expansion program.
    */
    /* JN: returns size of buffer used
     */
    private int decode_string(int idx, long code)
    {
        int i;

        i=0;
        while (code > (NEXT_CODE - 1))
            {
                decode_stack[idx++] = append_character[(int) code];
                code=prefix_code[(int) code];
                if (i++>=MAX_CODE)
                    {
                        System.err.printf("Fatal error during code expansion.\n");
                        return 0;
                    }
            }

        decode_stack[idx]= (int) code;

        return idx;
    }

    /*
    ** The following two routines are used to output variable length
    ** codes.  They are written strictly for clarity, and are not
    ** particularyl efficient.
    */

    long input_code(byte[] inputBuffer, int inputLength, int dummy_offset, boolean firstTime)
    {
        long return_value;

        //int pOffsetIdx = 0;
        if (firstTime)

            {
                input_bit_count = 0;
                input_bit_buffer = 0;
            }

        while (input_bit_count <= 24 && offset < inputLength)
            {
                /*
                input_bit_buffer |= (long) inputBuffer[offset++] << (24 - input_bit_count);
                input_bit_buffer &= 0xFFFFFFFFL;
                System.out.printf("input buffer %d\n", (long) inputBuffer[offset]);
                */
                // Java doesn't have unsigned types. Have to play stupid games when mixing
                // shifts and type coercions
                long val = inputBuffer[offset++];
                if (val < 0) {
                    val = 256 + val;
                }
                // System.out.printf("input buffer: %d\n", val);
                //if ( ((long) inpu) < 0) System.out.println("Byte is -ve???");
                input_bit_buffer |= (((long) val) << (24 - input_bit_count)) & 0xFFFFFFFFL;
                //input_bit_buffer &= 0xFFFFFFFFL;
                // System.out.printf("input bit buffer %d\n", input_bit_buffer);

                /*
                if (input_bit_buffer < 0) {
                    System.err.println("Negative!!!");
                }
                */

                input_bit_count  += 8;
            }

        if (offset >= inputLength && input_bit_count < 12)
            return MAX_VALUE;

        return_value       = input_bit_buffer >>> (32 - BITS);
        input_bit_buffer <<= BITS;
        input_bit_buffer &= 0xFFFFFFFFL;
        input_bit_count   -= BITS;

        return return_value;
    }

    void dumpLyric(int data)
    {
        System.out.printf("LZW: %d\n", data);
        if (data == 0xd)
            System.out.printf("\n");
    }

    /*
    **  This is the expansion routine.  It takes an LZW format file, and expands
    **  it to an output file.  The code here should be a fairly close match to
    **  the algorithm in the accompanying article.
    */

    public int expand(byte[] intputBuffer, int inputBufferSize, byte[] outBuffer)
    {
        long next_code = NEXT_CODE;/* This is the next available code to define */
        long new_code;
        long old_code;
        int character;
        int string_idx;

        int offsetOut = 0;

        prefix_code      = new long[TABLE_SIZE];
        append_character = new int[TABLE_SIZE];
        decode_stack     = new int[4000];

        old_code= input_code(intputBuffer, inputBufferSize, offset, true);  /* Read in the first code, initialize the */
        character = (int) old_code;          /* character variable, and send the first */
        outBuffer[offsetOut++] = (byte) old_code;       /* code to the output file                */
        //outTest(output, old_code);
        // dumpLyric((int) old_code);

        /*
        **  This is the main expansion loop.  It reads in characters from the LZW file
        **  until it sees the special code used to inidicate the end of the data.
        */
        while ((new_code=input_code(intputBuffer, inputBufferSize, offset, false)) != (MAX_VALUE))
            {
                // dumpLyric((int)new_code);
                /*
                ** This code checks for the special STRING+CHARACTER+STRING+CHARACTER+STRING
                ** case which generates an undefined code.  It handles it by decoding
                ** the last code, and adding a single character to the end of the decode string.
                */

                if (new_code>=next_code)
                    {
                        if (new_code > next_code)
                            {
                                System.err.printf("Invalid code: offset:%X new:%X next:%X\n", offset, new_code, next_code);
                                break;
                            }

                        decode_stack[0]= (int) character;
                        string_idx=decode_string(1, old_code);
                    }
                else
                    {
                        /*
                        ** Otherwise we do a straight decode of the new code.
                        */

                        string_idx=decode_string(0,new_code);
                    }

                /*
                ** Now we output the decoded string in reverse order.
                */
                character=decode_stack[string_idx];
                while (string_idx >= 0)
                    {
                        int data = decode_stack[string_idx--];
                        outBuffer[offsetOut] = (byte) data;
                        //outTest(output, *string--);

                        if (offsetOut % 4 == 0) {
                            //dumpLyric(data);
                        }

                        offsetOut++;
                    }

                /*
                ** Finally, if possible, add a new code to the string table.
                */
                if (next_code > 0xfff)
                    {
                        next_code = NEXT_CODE;
                        System.err.printf("*");
                    }

                // test code
                if (next_code > 0xff0 || next_code < 0x10f)
                    {
                        Debug.printf("%02X ", new_code);
                    }

                prefix_code[(int) next_code]=old_code;
                append_character[(int) next_code] = (int) character;
                next_code++;

                old_code=new_code;
            }
        Debug.printf("offset out is %d\n", offsetOut);
        return offsetOut;

    }
}

这里是SongInformation.java:

public class SongInformation {

    // Public fields of each song record
    /**
     *  Song number in the file, one less than in songbook
     */
    public long number;

    /**
     * song title in Unicode
     */
    public String title;

    /**
     * artist in Unicode
     */
    public String artist;

    /**
     * integer value of language code
     */
    public int language;

    public static final int  KOREAN = 0;
    public static final int  CHINESE1 = 1;
    public static final int  CHINESE2 = 2;
    public static final int  TAIWANESE3 = 3 ;
    public static final int  JAPANESE = 4;
    public static final int  RUSSIAN = 5;
    public static final int  THAI = 6;
    public static final int  TAIWANESE7 = 7;
    public static final int  CHINESE8 = 8;
    public static final int  CANTONESE = 9;
    public static final int  ENGLISH = 0x12;
    public static final int  VIETNAMESE = 0x13;
    public static final int  PHILIPPINE = 0x14;
    public static final int  TURKEY = 0x15;
    public static final int  SPANISH = 0x16;
    public static final int  INDONESIAN = 0x17;
    public static final int  MALAYSIAN = 0x18;
    public static final int  PORTUGUESE = 0x19;
    public static final int  FRENCH = 0x20;
    public static final int  INDIAN = 0x21;
    public static final int  BRASIL = 0x22;
    public static final int  CHINESE131 = 131;
    public static final int  ENGLISH146 = 146;
    public static final int  PHILIPPINE148 = 148;

    public SongInformation(long number,
                           String title,
                           String artist,
                           int language) {
        this.number = number;
        this.title = title;
        this.artist = artist;
        this.language = language;
    }

    public String toString() {
        return "" + (number+1) + " (" + language + ") \"" + title + "\" " + artist;
    }

    public boolean titleMatch(String pattern) {
        // System.out.println("Pattern: " + pattern);
        return title.matches("(?i).*" + pattern + ".*");
    }

    public boolean artistMatch(String pattern) {
        return artist.matches("(?i).*" + pattern + ".*");
    }

    public boolean numberMatch(String pattern) {
        Long n;
        try {
            n = Long.parseLong(pattern) - 1;
            //System.out.println("Long is " + n);
        } catch(Exception e) {
            //System.out.println(e.toString());
            return false;
        }
        return number == n;
    }

    public boolean languageMatch(int lang) {
        return language == lang;
    }
}

这里是Debug.java:

public class Debug {

    public static final boolean DEBUG = false;

    public static void println(String str) {
        if (DEBUG) {
            System.out.println(str);
        }
    }

    public static void printf(String format, Object... args) {
        if (DEBUG) {
            System.out.printf(format, args);
        }
    }
}

要编译这些代码,请运行以下命令:

    javac SongExtracter.java LZW.java Debug.java SongInformation.java

使用以下命令运行:

java SongExtracter <song number >

把这些 MIDI 文件转换成卡拉 KAR 文件的程序是KARConverter.java

      /*
 * KARConverter.java
 *
 * The output from decodnig the Sonken data is not in
 * the format required by the KAR "standard".
 * e.g. we need @T for the title,
 * and LYRIC events need to be changed to TEXT events
 * Tempo has to be changed too
 *
 */

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.sound.midi.MidiSystem;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Receiver;

public class KARConverter {
    private static int LYRIC = 5;
    private static int TEXT = 1;

    private static boolean firstLyricEvent = true;

    public static void main(String[] args) {
        if (args.length != 1) {
            out("KARConverter: usage:");
            out("\tjava KARConverter <file>");
            System.exit(1);
        }
        /*
         *      args[0] is the common prefix of the two files
         */
        File    inFile = new File(args[0] + ".mid");
        File    outFile = new File(args[0] + ".kar");

        /*
         *      We try to get a Sequence object, which the content
         *      of the MIDI file.
         */
        Sequence        inSequence = null;
        Sequence        outSequence = null;
        try {
            inSequence = MidiSystem.getSequence(inFile);
        } catch (InvalidMidiDataException e) {
            e.printStackTrace();
            System.exit(1);
        } catch (IOException e) {

            e.printStackTrace();
            System.exit(1);
        }

        if (inSequence == null) {
            out("Cannot retrieve Sequence.");
        } else {
            try {
                outSequence = new Sequence(inSequence.getDivisionType(),
                                           inSequence.getResolution());
            } catch(InvalidMidiDataException e) {
                e.printStackTrace();
                System.exit(1);
            }

            createFirstTrack(outSequence);
            Track[]     tracks = inSequence.getTracks();
            fixTrack(tracks[0], outSequence);
        }
        FileOutputStream outStream = null;
        try {
            outStream = new FileOutputStream(outFile);
            MidiSystem.write(outSequence, 1, outStream);
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public static void fixTrack(Track oldTrack, Sequence seq) {
        Track lyricTrack = seq.createTrack();
        Track dataTrack = seq.createTrack();

        int nEvent = fixHeader(oldTrack, lyricTrack);
        System.out.println("nEvent " + nEvent);
        for ( ; nEvent < oldTrack.size(); nEvent++) {
            MidiEvent event = oldTrack.get(nEvent);
            if (isLyricEvent(event)) {
                event = convertLyricToText(event);
                lyricTrack.add(event);
            } else {
                dataTrack.add(event);
            }
        }

    }

    public static int fixHeader(Track oldTrack, Track lyricTrack) {
        int nEvent;

        // events at 0-10 are meaningless
        // events at 11, 12 should be the language code,
        // but maybe at 12, 13
        nEvent = 11;
        MetaMessage lang1 = (MetaMessage) (oldTrack.get(nEvent).getMessage());
        String val = new String(lang1.getData());
        if (val.equals("@")) {
            // try 12
            lang1 = (MetaMessage) (oldTrack.get(++nEvent).getMessage());
        }
        MetaMessage lang2 = (MetaMessage) (oldTrack.get(++nEvent).getMessage());
        String lang = new String(lang1.getData()) +
            new String(lang2.getData());
        System.out.println("Lang " + lang);
        byte[] karLang = getKARLang(lang);

        MetaMessage msg = new MetaMessage();
        try {
            msg.setMessage(TEXT, karLang, karLang.length);
            MidiEvent evt = new MidiEvent(msg, 0L);
            lyricTrack.add(evt);
        } catch(InvalidMidiDataException e) {
        }

        // song title is next
        StringBuffer titleBuff = new StringBuffer();
        for (nEvent = 15; nEvent < oldTrack.size(); nEvent++) {
            MidiEvent event = oldTrack.get(nEvent);
            msg = (MetaMessage) (event.getMessage());
            String contents = new String(msg.getData());
            if (contents.equals("@")) {
                break;

            }
            if (contents.equals("\r\n")) {
                continue;
            }
            titleBuff.append(contents);
        }
        String title = "@T" + titleBuff.toString();
        System.out.println("Title '" + title +"'");
        byte[] titleBytes = title.getBytes();

        msg = new MetaMessage();
        try {
            msg.setMessage(TEXT, titleBytes, titleBytes.length);
            MidiEvent evt = new MidiEvent(msg, 0L);
            lyricTrack.add(evt);
        } catch(InvalidMidiDataException e) {
        }

        // skip the next 2 @'s
        for (int skip = 0; skip < 2; skip++) {
            for (++nEvent; nEvent < oldTrack.size(); nEvent++) {
                MidiEvent event = oldTrack.get(nEvent);
                msg = (MetaMessage) (event.getMessage());
                String contents = new String(msg.getData());
                if (contents.equals("@")) {
                    break;
                }
            }
        }

        // then the singer
        StringBuffer singerBuff = new StringBuffer();
        for (++nEvent; nEvent < oldTrack.size(); nEvent++) {
            MidiEvent event = oldTrack.get(nEvent);
            if (event.getTick() != 0) {
                break;
            }
            if (! isLyricEvent(event)) {
                break;

            }

            msg = (MetaMessage) (event.getMessage());
            String contents = new String(msg.getData());
            if (contents.equals("\r\n")) {
                continue;
            }
            singerBuff.append(contents);
        }
        String singer = "@T" + singerBuff.toString();
        System.out.println("Singer '" + singer +"'");

        byte[] singerBytes = singer.getBytes();

        msg = new MetaMessage();
        try {
            msg.setMessage(1, singerBytes, singerBytes.length);
            MidiEvent evt = new MidiEvent(msg, 0L);
            lyricTrack.add(evt);
        } catch(InvalidMidiDataException e) {
        }

        return nEvent;
    }

    public static boolean isLyricEvent(MidiEvent event) {
        if (event.getMessage() instanceof MetaMessage) {
            MetaMessage msg = (MetaMessage) (event.getMessage());
            if (msg.getType() == LYRIC) {
                return true;
            }
        }
        return false;
    }

    public static MidiEvent convertLyricToText(MidiEvent event) {
        if (event.getMessage() instanceof MetaMessage) {
            MetaMessage msg = (MetaMessage) (event.getMessage());

            if (msg.getType() == LYRIC) {
                byte[] newMsgData = null;
                if (firstLyricEvent) {
                    // need to stick a \ at the front
                    newMsgData = new byte[msg.getData().length + 1];
                    System.arraycopy(msg.getData(), 0, newMsgData, 1, msg.getData().length);
                    newMsgData[0] = '\\';
                    firstLyricEvent = false;
                } else {
                    newMsgData = msg.getData();
                    if ((new String(newMsgData)).equals("\r\n")) {
                        newMsgData = "\\".getBytes();
                    }
                }
                try {
                    /*
                    msg.setMessage(TEXT,
                                   msg.getData(),
                                   msg.getData().length);
                    */
                    msg.setMessage(TEXT,
                                   newMsgData,
                                   newMsgData.length);
                } catch(InvalidMidiDataException e) {
                    e.printStackTrace();
                }
            }
        }
        return event;
    }

    public static byte[] getKARLang(String lang) {
        System.out.println("lang is " + lang);
        if (lang.equals("12")) {
            return "@LENG".getBytes();
        }

        // don't know any other language specs, so guess
        if (lang.equals("01")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("02")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("08")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("09")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("07")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("")) {
            return "@L".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }

        return ("@L" + lang).getBytes();
    }

    public static void copyNotesTrack(Track oldTrack, Sequence seq) {
        Track newTrack = seq.createTrack();

        for (int nEvent = 0; nEvent < oldTrack.size(); nEvent++)
            {
                MidiEvent event = oldTrack.get(nEvent);

                newTrack.add(event);
            }

    }

    public static void createFirstTrack(Sequence sequence) {
        Track track = sequence.createTrack();
        MetaMessage msg1 = new MetaMessage();
        MetaMessage msg2 = new MetaMessage();

        byte data[] = "Soft Karaoke".getBytes();
        try {
            msg1.setMessage(3, data, data.length);
        } catch(InvalidMidiDataException e) {
            e.printStackTrace();
            return;
        }
        MidiEvent event = new MidiEvent(msg1, 0L);
        track.add(event);

        data = "@KMIDI KARAOKE FILE".getBytes();
        try {
            msg2.setMessage(1, data, data.length);
        } catch(InvalidMidiDataException e) {
            e.printStackTrace();
            return;
        }
        MidiEvent event2 = new MidiEvent(msg2, 0L);
        track.add(event2);
    }

    public static void output(MidiEvent event)
    {
        MidiMessage     message = event.getMessage();
        long            lTicks = event.getTick();
    }

    private static void out(String strMessage)
    {
        System.out.println(strMessage);
    }

}

/*** KARConverter.java ***/

播放 MIDI 文件

从光盘中提取的 MIDI 文件可以使用标准的 MIDI 播放器播放,例如 Timothy。歌词包括在内,旋律线在 MIDI 通道 1。我已经用 Swing 和 Java Sound framework 编写了一批 Java 程序,它们可以播放 MIDI 文件并对其进行处理。在播放 MIDI 文件的同时,我还可以做一些很酷的 Karaoke 的事情比如显示歌词,显示应该播放的音符,通过歌词显示进度。

播放 WMA 文件

WMA 档案是“邪恶的”它们基于两种微软专有格式。第一种是高级系统格式(ASF)文件格式,它描述了音乐数据的“容器”。第二个是 Windows Media Audio 9 编解码器。

ASF 是首要问题。微软有一个公开的规范( www.microsoft.com/en-us/download/details.aspx?id=14995 ),强烈反对任何开源的东西。许可证规定,如果您基于该规范构建一个实现,那么您:

  • 无法分发源代码
  • 只能分发目标代码
  • 除非作为“解决方案”的一部分,否则不能分发目标代码(换句话说,库似乎是被禁止的)
  • 不能免费分发您的目标代码
  • 无法将您的许可证设置为允许衍生作品

更何况 2012 年 1 月 1 日之后不允许你开始任何新的执行,而且已经是 2017 年 1 月了!

只是说的更难听一点,微软有专利 6041345,“用于容纳多个媒体流的活动流格式”( www.google.com/patents/US6041345 ),是 1997 年申请的。该专利似乎覆盖了与当时存在的许多其他格式相同的领域,因此该专利的地位(如果受到质疑)尚不清楚。但是,它已经被用来阻止 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 的,否则不太可能有。

我从 DVD 中提取的 WMA 文件具有以下特征:

  • 每个文件有两个通道。
  • 每个声道传送一个单声道信号。
  • 右声道承载所有乐器、伴唱以及主唱。
  • 左声道承载所有乐器和伴唱,但不承载主唱。

如果没有人对着麦克风唱歌,Sonken player 会播放右声道,但一旦有人对着麦克风唱歌,就会切换到左声道(有效地静音主唱)。简单有效。

歌词仍然作为 MIDI 存在于音轨数据中,并且可以像以前一样被提取。它们可以由 MIDI 播放器播放。我还不知道如何同步播放 MIDI 和 WMA 文件。

KAR 格式

生成的 MIDI 文件不是 KAR 格式。这意味着 pykaraoke 等 Karaoke 播放器可能会在播放它们时出现问题。将文件转换成这种格式并不太难:在序列中循环,适当地编写或修改 MIDI 事件。这个程序不是很令人兴奋,但是可以作为 KARConverter 下载。

与 pykar 一起播放歌曲

播放卡拉 MIDI 文件最简单的方法之一是使用 pykar ( www.kibosh.org/pykaraoke/ )。遗憾的是,从 Sonken 光盘中翻录的歌曲无法正常播放。这是因为 pykar 中的错误和未提供的所需特性的混合。问题及其解决方案如下。

拍子

许多 MIDI 文件会使用元事件设定速度 0x51 来明确设定速度。这些文件通常不会。pykar 希望 MIDI 文件包含此事件,否则默认为每分钟零拍的速度。正如所料,这将丢弃 pykar 执行的所有计时计算。

正如 Sonic Spot ( www.sonicspot.com/guide/midifiles.html )解释的那样,“如果没有设定的速度事件,则假定每分钟 120 拍。”它给出了一个计算合适的速度值的公式,即 60000000/120。

这需要对一个 pykaraoke 文件进行一次更改:将pykar.py的第 190 行更改如下:

sele.Tempo = [(0, 0)]

对此:

self.Tempo = [(0, 500000)]

语言编码

文件pykdb.py声称cp1252是 Karaoke 文件的默认字符编码,并使用一种叫做DejaVuSans.t的字体,这种字体适合显示这样的字符。除了标准 ASCII 之外,这种编码还在一个字节的前 128 位中添加了各种欧洲符号,例如“”。

我不确定 pykaraoke 是从哪里得到这些信息的,但它肯定不适用于中国的 Karaoke。我不知道中文、日文、韩文等使用什么编码,但是我的代码将它们作为 Unicode UTF-8 转储。适合 Unicode 的字体是Cyberbit.ttf。(参见我在 http://jan.newmarch.name/i18n/ 的全球软件讲义中的“字体”一章)。)

文件pykdb.py需要以下几行:

        self.KarEncoding = 'cp1252'  # Default text encoding in karaoke files
        self.KarFont = FontData("DejaVuSans.ttf")

更改为以下内容:

        self.KarEncoding = 'utf-8'  # Default text encoding in karaoke files
        self.KarFont = FontData("Cyberbit.ttf")

并将Cyberbit.tt的副本复制到目录/usr/share/pykaraoke/fonts/中。

没有音符的歌曲

光盘上的一些歌曲没有 MIDI 音符,因为这些都在 WMA 文件中。MIDI 文件只有歌词。pykaraoke 只弹到最后一个音,也就是零音!所以,不放歌词。

结论

本章主要讨论了一个取证问题:当文件格式未知时,如何从 DVD 中获取信息。它与播放声音没有任何直接关系,尽管它确实给了我一个已经付费的文件的大来源。

posted @ 2024-08-02 19:34  绝不原创的飞龙  阅读(0)  评论(0编辑  收藏  举报