Audacity架构-PortAudio和wxWidgets

来源:互联网 发布:visio数据库图标在哪 编辑:程序博客网 时间:2024/05/22 08:38
原文地址:http://www.sand-tower.net/archives/86

Audacity是一个十分流行的声音录制和音频编辑软件。它是一个功能强大同时又保持了易用性的软件。Audacity的主要用户是使用的Windows系统,但是也有人在Linux和Mac上编译Audacity的源代码来使用该软件。

Dominic Mazzoni在1999年编写了该软件的原始版本,当时他还只是卡耐基梅隆大学(Carnegie Mellon University)的一个研究生。Dominic想要创建一个平台,在该平台上能够开发和调试音频处理的算法。这个软件逐渐成长为自身就可以以很多方式使用的软件。Audacity一作为开源软件方步,就吸引了其他的开发人员。一个小的、逐渐变化的爱好者团队在这些年对Audacity做了很多的工作,包括修改、维护、测试、更新、编写文档、提供对用户的支持、将Audacity的界面翻译成其他的语言。

Audacity的一个目标就是其界面应该是十分容易使用的:人们应该能够不使用手册就能够立刻开始使用Audacity,并逐步发现它的其他的功能。这条原则在Audacity提供出色的用户界面的一致性方面起了重要的作用。对于一个有很多人参与的项目来说,这种统一的原则比它最初看起来的作用要重要的多。

如果Audacity的架构也有同样类似的指导原则就好了。我们所拥有的最接近的指导原则就是“试图保存一致”。当添加新的代码的时候,开发人员试图跟随最近的代码的风格和习惯。但是在实际中,Audacity的代码混合了结构良好的代码和结构不那么良好的代码。从整体的架构上来看,用一个小城市来做比喻是最好的:有一些令人印象深刻的建筑,但是也有破败的像是贫民区的街区。

1. Audacity中的结构

Audacity按照层被划分为了好几个库。尽管Audacity代码中很多新的开发并不需要具体的这个代码会进入到哪个库,熟悉这些API和它们所做的事情还是很重要的。两个最重要的库就是PortAudio和wxWidgets。PortAudio以跨平台的方式提供了底层的音频接口,wxWidget则是以跨平台的方式提供了GUI组件。

阅读Audacity的代码的时候,应该意识到只有一部分的代码是必不可少的。这些库提供了很多可选的功能 — 当然,使用这些功能的用户可能不会认为这些功能是可选的。例如,除了Audacity内置的声音效果之外,Audacity还支持LADSPA(Linux Audio Developer’s Simple Plugin API)来支持可动态加载的音频效果插件。Audacity的VAMP API也做了同样的事情来支持音频分析的插件。没有这些API,Audacity的功能就没有这么丰富,但是这并不表明Audacity就完全依赖这些功能。

Audacity使用的其他的可选的库是libFLAC,libogg和libvorbis。这些库提供了很多不同的音频压缩格式。MP3格式是通过动态载入LAME或FFmpeg库来满足的。许可证限制使得我们无法将这个最流行的压缩格式放入内置的库中。

一些Audacity的库和结构的决定背后也是许可证的问题。例如,对于VST插件的支持没有作为内置的也是因为许可证的问题。我们也希望在我们的代码中使用十分有效的FFTW快速傅立叶变换代码。但是,我们只是为那些自己编译Audacity的人们提供了这个选项,在我们的普通的版本中,我们使用的是稍微慢一些的版本。由于Audacity可以接受插件,所以我们已经讨论过了Audacity是否能够使用FFTW。FFTW的作者不希望他的代码成为任何其他的代码的通用服务。因此,架构师的决定提供对插件的支持来作为我们可以提供的东西的折衷。这个决定使得LADSPA插件成为可能,但是使得我们无法在预先构建好的可执行版本中使用FFTW。

架构的选择还受到另外一个方面的影响,就是怎样使用稀缺的开发时间。Audacity的开发团队很小,没有很多的资源区做一些事情,例如深入的分析安全漏洞,就像Firefox和Thunderbird的团队所做的那样。但是,我们确实不希望Audacity会提供能够绕过防火墙的路由,因此我们有一个规则就是绝对不使用TCP/IP来连接到Audacity或者从Audacity连接出去。不使用TCP/IP减少了很多安全上的顾虑。意识到了资源的有限,使得我们只有追求更好的设计。这帮助我们减少了很多会花费我们很多时间的功能,使我们专注于最本质的东西。

在脚本语言上也有同样的开发人员的时间的考虑。我们希望使用脚本,但是实现脚本语言的代码不是Audacity所需要的。将每种用户可能希望使用的脚本语言的副本都编译到Audacity中时没有道理的。我们最终选择通过一个插件模块和pipe来实现脚本,关于这一点我们后面会谈到。

Audacity的分层结构

图1 Audacity中的分层结构

图1显示了Audacity中的层和一些模块。在这张图中高亮显示了wxWidgets中的3个重要的类,这些类在Audacity中都有相应的映射。我们从相关的底层的类来构造高层的抽象层。例如,BlockFiles系统是wxWidgets的wxFiles类的映射和抽象。在某种程度上来说,将BlockFiles、ShuttleGUI和命令处理划分出去形成一个自己的中间库是更加合理的。这也会鼓励开发人员将这些库变得更加通用。

再下面是一个狭长的“平台相关的实现层”。wxWidgets和PortAudio都是和操作系统无关的抽象层,它们都包含在依赖于目标平台的不同的实现间选择的条件代码。

图中的“其他的支撑库”类包括很广泛的一组库。有趣的是其中很多是依赖于动态载入的模块的。这些动态载入的模块对wxWidgets一无所知。

在Windows平台上,我们以前是将Audacity编译成一个整体的可执行文件,将wxWidgets和Audacity应用程序包含在同一个可执行程序中。在2008年,Audacity社区做出了改变,使用模块化的结构,将wxWidgets作为一个单独的DLL。这样做是为了允许在运行时载入那些直接使用了wxWidgets的功能的附加的可选的DLL。插入到图中的虚线之上部分的插件是可以使用wxWidgets的。

决定将wxWidgets编译成DLL也有不好的一面。这样要分发的文件就大了很多,部分原因是因为很多以前被优化去掉了的没有使用的功能现在都在DLL中提供了。Audacity的启动时间也增长了,因为每个DLL都是单独载入的。但是带来的好处也很可观。我们期望模块化给我们带来的好处和模块化给Apache带来的好处一样。正如我们所看到的那样,模块化使得Apache的核心非常稳定,同时又可以方便在新的模块中使用试验性的、特殊的功能和新的想法。目前我们还没有看到这样的好处。公开wxwidgets的功能还只是第一步,要拥有一个灵魂的模块化的系统,我们还有很多事情要做。

很明显,一个Audacity这样的程序的结构不是事先就设计好的,而是随着时间发展而来的。我们现在拥有的这个大的架构工作的很好。但是我们发现,当我们需要添加新的会影响很多源文件的功能的时候,我们就必须和架构“战斗”。例如,Audacity现在是以一种很特殊的方式在处理立体声和单声道。如果想要修改Audacity来处理环绕声,就必须修改Audacity中的很多文件。

超越立体声:GetLink的故事

Audacity从来没有对声道的数量进行抽象,而是使用的链接声音声道。有一个函数GetLink,这个函数会返回一对声道中的另外一个,如果是单声道,就返回NULL。使用GetLink的代码通常看起来会给人的感觉是这段代码最开始是为单声道的情况编写的,后来才添加了(GetLink()!=NULL)的检测来将代码扩展来处理立体声。我不确定代码是不是真的是这样编写的,但是我很怀疑就是这样的。在Audacity的代码中,没有循环使用GetLink来遍历一个链表中的所有的声道的代码。绘制、混音、读和写的代码都包含一个是否是立体声的检测,而不是编写了一个对n声道通用的代码,尽管n多半是1或者2。如果要得到更加通用的代码,我们需要修改大概100个对GetLink函数的调用,至少分布在26个文件中。

很容易我们就能够搜索代码来找到所有对GetLink的调用,改变也不是十分复杂,因此修改这个问题并没有刚开始看起来的那么难。GetLink的故事并不是关于难以修复的结构缺陷的。但是这个故事演示了在允许的情况下,一个很小的缺陷是怎样扩散到很多代码中的。

现在看来,将GetLink函数设置为私有的,转而提供一个迭代器来迭代遍历音轨中所有的声道是更好的。这就会避免很多针对立体声的特殊的代码,同时使得具体的实现对使用音频声道列表的代码是完全不可知的。

更加的模块化的设计能够使得我们向更好的隐藏内部结构方向发展。当我们定义和扩展一个外部API的时候,我们也需要更加仔细的看看我们提供的功能。这能够将我们的注意力放在那些我们不希望放在外部API中的抽象上。

2. wxWidget GUI库

对于Audacity的用户界面程序员来说,wxWidget GUI库是最显著的一个库,这个库提供了如按钮、滑动条、选择框、窗口和对话框等东西。这个库提供了跨平台的可视行为。wxWidget库有自己的字符串类wxString,有自己的跨平台的线程、文件系统和字体的抽象,还有本地化到其他语言的机制,这些都是我们经常使用的。我们建议开发Audacity的新人先下载一个wxWidget,编译它并熟悉这个库所附带的一些例子。wxWidget是在操作系统提供的GUI对象之上的很薄的一层。

为了构建复杂的对话框,wxWidget提供的不仅仅是一个部件元素,还有用来控制元素的大小和位置的sizers。相比给定图形元素一个固定的绝对位置来说,这个方式好好得多。如果部件的大小发生了改变,无论是用户直接做出的改变还是因为字体变化间接引发的改变,对话框中的元素的位置都会很自然的发生变化。对于跨平台的应用程序来说,sizers是十分重要的。没有它们,我们可能需要为每个平台都提供客户化的对话框布局。

通常,这些对话框的设计都是在一个资源文件中,这些文件会被程序读取。但是在Audacity中,我们独有的将对话框设计作为一系列的对wxWidget的调用编译到程序中。这样我们就提供了最大的灵活性:也就是对话框的确切的内容和行为时由应用程序级的代码来决定的。

你应该会在Audacity中发现创建一个GUI的初始化的代码很明显是使用图形化的对话框构建工具自动生成的。这些工具帮助我们获得了一个基本的设计。随着时间的推移,为了添加新的功能,这些基本的代码会越来越乱,并且有很多的地方创建的新的对话框是通过拷贝和修改已有的、已经变乱了的对话框代码而来的。

经过很多年的这样的开发之后,我们发现在Audacity中有大段的代码,尤其是配置用户偏好的对话框,包含了很多混乱的重复的代码。这些代码尽管功能很简单,但是很难阅读。这个问题的一部分原因是因为构建这些对话框的顺序十分的随意:小的元素被组合成大的元素,直到组成整个对话框。但是这些元素被代码创建的顺序和这些元素在页面上的布局的顺序不一样(也不是必须一样)。这些代码也是冗长而重复的。其中有GUI相关的代码来将保存到硬盘的用户的偏好转换成中间变量,也有将中间变量转换成显示的GUI的代码,还有相反的功能的代码(GUI显示转换为中间变量,中间变量转换为要保存的偏好)。其中还有一些代码的注释是//this is a mess (这是一团乱麻)。但是在我们出来这个问题之前,这样的注释已经存在了很久。

 

前面说过了,在Audacity的GUI的代码中,有很多冗长、重复、有点混乱的代码。下面我们就来看看解决这个问题的方案。

3. ShuttleGui层

解决这些混乱的代码的方案是创建一个新的类,ShuttleGui,这个类能够有效的减少定制一个对话框所需的代码行数,使得代码的可读性更好。ShuttleGui是在wxWidget库和Audacity的额外的一层。这个类的工作就是在这两者之间传递信息。下面就是一个例子,这个例子会得到图2中的GUI。

 
1ShuttleGui S;
2// GUI Structure
3S.StartStatic("Some Title",…);
4{
5    S.AddButton("Some Button",…);
6    S.TieCheckbox("Some Checkbox",…);
7}
8S.EndStatic();

对话框例子

图2 对话框例子

这段代码在对话框上定义了一个静态的框,这个框内包含一个按钮和一个选择框。代码和对话框之间的对应关系十分清楚。StartStatic和EndStatic是成对调用的。其他的类似的StartXXX/EndXXX对也应该是成对调用的,是用来控制对话框布局中的其他的方面的。花括号和缩进并不是必须的,我们在上面的代码中使用它们只是为了代码的结构更加明显,尤其是使相互匹配的成对的调用看起来更加明显。对于大型的程序来说,这样可读性好很多。

上面显示的源代码不仅仅是创建了对话框。在注释//GUI Structure之后的代码还可以用来从对话框传输数据到保存的用户偏好中,同时也可以反向传输数据。在以前,很多重复的代码都是因为它们要各自自己实现这样的功能。现在,这些代码只需要写一次,就写在ShuttleGui类中。

在Audacity中还有其他的对基本的wxWidget的扩展。Audacity有自己类来管理工具栏。为什么不使用wxWidget内置的工具条类呢?这个是历史遗留问题:Audacity的工具条是先写的,而wxWidget提供工具条类是后面的事情。

4 TrackPanel

在Audacity中最主要的界面是TrackPanel,是用来显示波形图的。这是一个由Audacity绘制的客户控件。TrackPanel是由如小一点的面板这样的组件组成的,这些面板会显示音轨信息、时间线的标尺、波幅的标尺、要显示波形图的音轨、波谱或文本标签。可以拖动来移动这些音轨和改变它们的大小。音轨包含的文本标签是使用的我们自己重新实现的可编辑的文本框,而不是使用的内置的文本框。你可能认为这些标尺和面板每个都是wxWidget组件,但是不是。

TrackPanel界面

图3 TrackPanel界面

图1.3中显示的屏幕截图是Audacity的用户界面。所有那些标注了的组件都是Audacity客户化的组件,尽管wxWidgets有一个为TrackPanel提供的部件。Audacity的代码而不是wxWidgets在管理位置和重画的工作。

这些组件结合在一起来组成TrackPanel的方式真是十分的可怕。(是代码很可怕,用户看到的界面还是不错的。)GUI和应用程序相关的代码完全混在了一起,没有明确的分开。在一个好的设计中,应该只有我们的应用程序相关的代码才知道左声道和右声道、分贝、噪声抑制和独奏。GUI元素应该是对应用程序的毫无所知元素,能够重用在非音频的应用程序中。就算是TrackPanel中纯粹的GUI部分也是不同的特殊情况的代码拼凑起来的,使用了绝对位置和大小,不够抽象。如果这些特殊的组件都是自包含的GUI组件,并且和wxWidgets中相同的接口一样使用同样的sizers,那么我们的代码会更加整洁、一致性更好。

要得到一个这样的TrackPanel,我们需要一个新的wxWidget的sizer,才能够移动或重新设置track,或者其他的部件的大小。wxWidget的sizer还不够灵活。我们可以在其他的地方使用这个sizer,例如我们可以在持有按钮的工具条上使用这个sizer,使得工具条能够客户化,支持通过拖放来调整按钮的顺序。

已经做了一些探索性的工作来创建和使用这样的sizer,但是工作做的还不够。一些将GUI组件做成完全完善的wxWidget的实验碰到了一个问题:这样做降低了我们对部件重画的控制,导致的结果是当改变组件的大小或移动组件的时候造成闪烁。我们需要对wxWidgets进行很多修改来达到一个不闪烁的重画,将改变大小的步骤和重画的步骤分开来。

要小心使用wxWidget的另外一个原因是我们已经知道当有很多的部件的时候,wxWidgets会运行的很慢。大部分的原因是在wxWidget的控制范围之外的。灭国wxWidget,例如按钮、文本输入框,都是使用的窗口系统的资源,每个部件都有一个句柄来访问窗口,这些都需要消耗处理时间。即使是大部分的部件都是隐藏的或在屏幕之外的,处理仍然会很慢。我们的track窗口上就是有很多部件的。

最好的方法是使用轻量级模式(flyweight pattern),我们自己的轻量部件,这些部件没有相应的消耗窗口系统资源和句柄的对象。我们会使用一种类似wxWidget的sizer和组件部件,赋予这些组件类似的API,但是并不继承wxWidget类。我们还要重构我们的已有的TrackPanel代码,采用更加清晰的结构。如果这个方案很容易的话,我们早就做了。修改现有的零时代码很明显有大量的设计和编码的工作要做。而保留现在这个复杂的但是工作足够良好的代码也是一个很大的诱惑。

5 PortAudio库:录音和播放

PortAudio是一个音频库,它赋予了Audacity以跨平台的方式播放和录制音频的能力。没有这个库,Audacity就没有办法使用声卡。PortAudio提供了环状缓冲区、播放/录制的采样率转换器,还有最重要的是提供了隐藏了Mac、Linux和Windows的音频区别的API。在PortAudio中,对于每个平台都提供了支持这些平台的实现文件。

我们不需要深入的挖掘PortAudio内部做了些什么,但是知道Audacity和PortAudio是怎样的互动还是有帮助的。Audacity会接收来自PortAudio的数据包(录制)和发送数据包到PortAudio(播放)。我们要看看发送和接收时怎样发生的,这两个操作怎样和读/写磁盘以及更新屏幕结合在一起。

有几个不同的处理在同时进行。有一些处理进行的很频繁,每次传递小量的数据,并且这些处理必须很快的完成。其他的一些处理进行的就不那么频繁,每次传递的数据比较多,并且完成的时间不是那么关键。这两种类型的处理中存在“阻抗不匹配”,因此使用了缓冲区来解决这个问题。

录制和播放时的线程和缓冲区

图4 录制和播放时的线程和缓冲区

PortAudio代码会启动一个线程来直接和音频设备交互,这个线程会驱动录制和回放。这个线程必须有很快的响应速度,要不然就会丢失数据包。这个由PortAduio代码控制的线程叫做audacityAudioCallback,当录制的时候,这个线程将新收到的小数据包添加到一个大的(5秒钟的)录制缓冲区中。在播放的时候,这个线程会从一个5秒钟的播放缓冲区中读取小段的数据。PortAudio库根本就不知道exWidgets的存在,因此这个PortAudio创建的线程是一个pthread。

另外一个线程是Audacity的类AudioIO启动的。当录音的时候,AudioIO会从录制缓冲区中获取数据,并将数据添加到Audacity的track中,这样最终这个音频就会被播放。另外,当读取了足够多的数据之后,AudioIO会将数据写入硬盘中。在进行音频播放的时候,这个线程还负责读硬盘。函数AudioIO::Filebuffer是这里关键的函数,这个函数根据几个Boolean变量的设定来再同一个函数中处理录制和播放。用同一个函数来处理两个方向的功能是十分重要的。有时候,我们需要给以前录制的声音文件录制新的配音的时候,就需要同时使用录制和播放的功能。在AudioIO线程中,我们是完全听凭操作系统的磁盘IO来摆布的。在读取或写入磁盘的时候,我们可能被拖延不知道多长的时间。因此我们不能够在audacityAudioCallback中做这些读写操作,因为那个线程需要很快的应答速度。

这两个线程间的通信时通过共享变量来完成的。因为我们能够控制哪个线程在什么时候写这个变量,因此我们不需要更加昂贵的互斥机制。

在播放和录音中,都有一个附加的需求:Audacity还需要更新GUI。这个是对响应时间要求最低的操作。更新时发生在主要的GUI线程中的,是由一个定时器每秒钟20次的触发的。这个定时器会导致TrackPanel::OnTime被调用,如果发现需要就会更新GUI。这个主GUI线程是在wxWidget中创建的,而不是在我们自己的代码中创建的。在其他的线程中不能直接更新GUI。使用定时器来让GUI线程检查是否需要根性屏幕使得我们能够将重画的次数降低到一个可接受的水平,使得既能够得到满足要求的显示,又不会让显示占用太多的处理器时间。

这样3个线程分开的设计好不好?使用3个不是基于同一个抽象基本类的线程是有点特别。但是,这种特别决定大部分是因为我们使用的库。PortAudio自己会创建一个线程。而wxWidget框架自动的就会创建一个GUI线程。一个和频繁操作小片数据的音频设备线程,一个是相对不太频繁的操作大量数据的磁盘操作线程,我们之所以需要一个填充缓冲区的线程就是因为这两个线程之间的不匹配。使用这些库的好处四很明显的,代价是使用这些库我们就不得不使用它们提供的抽象。结果就是我们将数据在内存里从一个地方拷贝到另外一个地方,而这本来是不必要的。在我以前做过的数据交换的工作中,我曾经见过更加有效的处理这种“阻抗不匹配”的代码,使用的是中端驱动,压根不用线程。是通过传递缓冲区的指针而不是拷贝数据来完成。只有你使用的库的设计有更加丰富的缓冲区抽象才能够这样做。使用已有的接口,我们不得不使用线程并且拷贝数据。

6 BlockFile

Audacity所面临的挑战之一就是支持向一个可能有1小时长的录音中插入或删除数据。录音很容易就太长了而无法放入内存中。如果一个音频录音师在一个单独的磁盘文件中,那么在靠近文件开始的部分插入音频就可能意味着要移动很多数据来为新插入的数据让路。磁盘上的这种拷贝数据的操作将会很费时间,也就意味着Audacity不能够很快的响应简单的编辑。

Audacity对这个问题的解决方案就是将音频文件划分为多个BlockFile,每个大概1MB左右。这一生Audacity有自己的音频文件格式(主文件的扩展名为.aup)的主要原因。这个主文件是一个XML文件,用来协调不同的block。修改一个很长的音频录音的靠近开头的部分也许只会影响一个block和主aup文件。

BlockFile平衡了两股冲突的力量。我们能够不需要过多的拷贝就能够插入或者删除,同时在播放的时候,我们能够保证每次对磁盘的读请求都能够得到足够大的音频段。Block越小,为了获取同样数量的音频数据所需要的磁盘访问次数就越多;block越大,插入或者删除的时候需要拷贝的数据就越多。

Audacity的BlockFile从来没有内部的空闲空间,也从来没有增长的超过最大的block的大小。要在插入和删除数据的时候保证这两点,我们有时候需要拷贝一个block的数据。当一个BlockFile完全不需要了的时候,我们会删除这个文件。BlockFile是有引用计数的,因此当我们删除了一些音频数据,再我们保存之前,相关的BlockFile还是仍然存在,可以提供对撤销机制的支持。和采用将所有的数据放在单独的一个文件中的方案不同,在BlockFile内从来不需要垃圾回收空闲空间。

从B-tree到Google的BigTable表再到unrolled linked list,合并或者将大块的数据分割开来都是数据管理的系统的最基本的东西。图5显示了再Audacity删除一段靠近开头部分的音频的时候会发生的事情。

BlockFile

图5 在删除之前,.aup文件和BlockFile文件持有序列ABCDEFGHIJKLMN。在删除了FGHI之后,有两个BlockFile被合并了。

BlockFile并不是只有音频在使用,有些BlockFile被用来缓存摘要信息。如果Audacity被要求在屏幕上显示一个4个小时长的录音,那么让屏幕每次重画的时候都处理整个音频文件时不可想象的。Audacity的方法就是使用摘要信息,摘要信息只是提供了按时间段的最大和最小的音频幅度。当放大的时候,Audacity会使用真实的采样来重画。当缩小的时候,Audacity就是使用摘要信息来重画。

对BlockFile系统的一个完善是其中的block不是必须是由Audacity创建的文件。这些block可以指向另外一个音频文件的一段,例如一个以.wav格式保存的音频的时间段。用户可以创建一个Audcity工程,从.wav文件导入音频,并将多个track进行混音,最后只会为摘要信息生成BlockFile。这节省了磁盘空间,也节省了拷贝数据的时间。但是,总而言之这是一个糟糕的主意。很多Audacity的用户会删除原始的音频.wav文件,他们以为在Audacity工程的目录中应该有完全的拷贝。但是实际上是没有的,而且没有了原始的.wav文件,Audacity的工程也无法播放。现在在Audacity中默认的做法就是拷贝导入的音频,在处理的过程中创建新的BlockFile。

在Windows操作系统上,BlockFile这个解决方案遇到了问题:当有态度的BlockFile的时候,性能会很糟糕。这是因为当同一个目录中有太多的文件的时候,Windows处理文件就很慢,和前面提到的有太多的布局的时候wxWidget会很慢很类似的问题。后来的附加方案是使用层级的字目录结构,在每个子目录中不超过100个文件。

BlockFile结构的最大的问题就是这个结构暴露给了最终用户。经常有用户删除了.aup文件,但是忘记删除包含了BlockFile的目录。如果Audacity工程师一个单独的文件,Audacity复杂文件内的空间怎样使用就好了。如果是这样的话,性能会提升而不是下降。主要的需要添加的代码是垃圾回收。但是也有简单的解决方法,就是当文件中超过设定的百分比的空间没有被使用的时候,将block拷贝到一个新的文件中。

7 Scripting

Audacity有一个实验性的插件,能够支持多种脚本语言。它通过命名管道提供了一个脚本接口。通过脚本暴露出的命令式文本格式的,回应也是文本格式的。只要用户的脚本语言能够向命名管道中写入和读取文本,这个脚本语言就能够驱动Audacity。音频和其他的大数据量的数据不需要经过管道。(图1.6)

Scripting

图6 通过命名管道提供脚本功能的脚本插件

插件自身对其传输的文本内容一无所知。这个插件只负责传输文本。脚本插件使用的插件接口(或者叫基本的扩展点)已经将Audacity命令以文本格式暴露了出来。因此脚本插件很小,主要的内容都是和命名管道相关的编码。

但是不幸的是,管道或引入和TCP/IP连接类似的安全风险。因为安全的原因,我们已经将TCP/IP连接排除在了Audacity之外。为了降低风险,这个插件是一个可选的DLL。要使用该插件必须经过深思熟虑,并且承担随之而来的安全风险。

脚本功能之后,在我们的wiki的功能需求页面上浮出水面的另外一个建议是我们应该考虑使用KDE的D-Bus标准来提供一个进程间的通过TCP/IP的调用机制。我们已经开始了另外一种方式,但是将我们已有的接口做一个调整来支持D-Bus看起来也是很有道理的。

8 实时效果

Audacity并没有实时的效果,所谓实时的效果就是音频效果是在音频播放的时候根据需要计算的。在Audacity中,我们应用一个效果就必须等待该效果结束。实时效果和在后台渲染音频效果的同时保持用户界面的响应时最常提出的对Audacity的功能需求。

我们的问题是,在一个机器上的实时的效果在另外一个慢得多的机器上可能就不够快,不是实时的。Audacity在很多机器上运行,我们希望有一个优雅的后备方案。在一个有些慢的机器上,我们仍然能够要求将效果应用到整个track上,然后稍等片刻之后就能够听到接近track的中间部分的已经处理了的音频,因为Audacity知道先处理那一部分。在一个太慢而无法实时的渲染效果的机器上,直到播放被渲染追上之后,我们才能够听到音频。为了做到这一点,我们需要去除“音频效果要使用户界面停顿”和“对音频块的处理顺序必须严格从左到右”的限制。

Audacity中的一个名为on demaind loading的功能有着我们的实时效果所需要的类似的元素,尽管该功能和音频效果没有任何的关系。有了这个功能之后,当导入一个音频文件到Audacity中的时候,Audacity能够在后台创建一个摘要的BlockFile。Audacity会为还没有处理的音频显示一个蓝色和灰色斜条纹的占位符,并能够在载入音频的同时响应用户的命令。而且不需要按照从左到右的顺序处理block。在适当的时候,这些代码都能够被重用到实时效果功能中。

On demand loading为我们提供了一种渐进的方式来添加实时效果。这个步骤能够避免了一些使效果实时化的复杂性。此外,有些实时效果需要block之间的重叠,否则有些象回声这样的效果就无法正确的应用。另外,我们还需要允许在音频播放的时候参数是可变的。通过先做on demand load,代码可以在早期就被使用。我们能够得到一些反馈,并在实际的应用中对代码进行完善。

9 小结

通过对Audacity的分析,我们知道了一个好的结构怎样对程序的发展做出贡献,而缺乏好的结构会怎样妨碍程序的发展。

在实际的软件架构中,要对自行开发和引用第三方部件(开源的或商业的)做出决断。自行开发能够做到完全的掌控和按需的设计,使用第三方部件,尤其经过广泛验证的第三方部件能够帮助我们提升生产率,缩短开发时间。例如在Audacity中使用象PortAudio和wxWidget这样的第三方API有很大的好处。它们为Audacity的代码提供了隐藏了很多平台差异的抽象。但是使用这些第三方API的代价之一就是Audacity无法灵活的选择抽象。Audacity的播放和录制的代码不那么漂亮是因为Audacity不得不以3种不同的方式来处理线程。相比能够自己控制抽象的情况,Audacity还做了更多的原本不必要的代码拷贝的工作。

有些时候,我们需要在现成的第三方库的基础上进行二次开发,例如wxWidget给Audacity提供的API诱惑开发人员编写一些冗长的、难以维护的应用程序代码。Audacity对这个问题的解决方法是在wxWidget前面添加一个fasade,使得Audacity能够获得所希望的抽象和更简洁的应用程序代码。另外,在Audacity的TrackPanel中,功能和性能的需求超出了能够从已经存在的部件中很容易就得到的功能。结果是Audacity创建了自己的特殊的系统。这是一个简明的系统,包含widget和sizer,在逻辑上将应用程序基本的对象和TrackPanel中分离了出来。

在软件架构中的架构决定不仅仅是决定新的功能的架构。决定什么不应该包含到应用程序中也是很重要的。这个决定能够带来更简洁的、更安全的代码。能够使用类似Perl这样的脚本语言而不需要创建自己的脚本语言,这样还是很让人高兴的。这是Audacity所做出的架构决定,也是被未来的增长计划所驱动的。

从Audacity的架构和代码中能够清楚的看出Audacity是一个社区工作的结果。社区的范围远远大于那些直接的贡献者,因为Audacity要依赖于其他的库,每个库又有自己的社区,自己的领域专家。Audacity社区的特质是在代码的长处和缺点中被反映出来了的。一个更加紧密的团体会写出比这个社区更加高质量、高一致性的代码,但是很难使用更少的人来完成和Audacity所拥有的一样多的功能。


0 0