Audacity 是一款流行的声音录制和音频编辑软件。它功能强大,同时易于使用。大多数用户都在 Windows 上,但相同的 Audacity 源代码也可以编译到 Linux 和 Mac 上运行。
多米尼克·马佐尼在 1999 年,还是卡内基梅隆大学的研究生时,编写了 Audacity 的第一个版本。多米尼克想要创建一个平台来开发和调试音频处理算法。该软件在许多其他方面也变得很有用。Audacity 发布为开源软件后,吸引了其他开发人员。多年来,一个由爱好者组成的规模较小、逐渐变化的团队一直在修改、维护、测试、更新、编写文档、帮助用户,以及将 Audacity 的界面翻译成其他语言。
目标之一是其用户界面应该是可发现的:人们应该能够无需手册,立即坐下来开始使用它,并逐渐发现它的功能。这一原则对于赋予 Audacity 比其他方式更一致的用户界面至关重要。对于一个多人参与的项目来说,这种统一原则比最初看起来更重要。
如果 Audacity 的架构有类似的指导原则,类似的可发现性,那就太好了。我们最接近这一点的是“尝试保持一致”。添加新代码时,开发人员会尝试遵循附近代码的风格和约定。然而,实际上,Audacity 的代码库是结构良好的代码和结构不太好的代码的混合。与其说是一个整体架构,不如说是一个小城市的类比更合适:有一些令人印象深刻的建筑,但您也会发现一些更像棚户区的破败社区。
Audacity 建立在几个库之上。虽然大多数 Audacity 代码中的新编程不需要详细了解这些库的内部工作原理,但熟悉它们的 API 和它们的功能非常重要。两个最重要的库是 PortAudio,它以跨平台的方式提供低级音频接口,以及 wxWidgets,它以跨平台的方式提供 GUI 组件。
在阅读 Audacity 的代码时,意识到只有少部分代码是必要的,这一点很有帮助。库贡献了许多可选功能,虽然使用这些功能的人可能不会认为它们是可选的。例如,除了拥有自己的内置音频效果外,Audacity 还支持 LADSPA(Linux 音频开发人员简单插件 API)用于动态加载插件音频效果。Audacity 中的 VAMP API 对分析音频的插件也做同样的事情。如果没有这些 API,Audacity 的功能会更少,但它并不完全依赖于这些功能。
Audacity 使用的其他可选库包括 libFLAC、libogg 和 libvorbis。这些提供了各种音频压缩格式。MP3 格式由动态加载 LAME 或 FFmpeg 库来支持。许可限制阻止这些非常流行的压缩库内置。
许可证是关于 Audacity 库和结构的一些其他决定的原因。例如,由于许可证限制,不支持 VST 插件。我们还想在我们的某些代码中使用非常高效的 FFTW 快速傅里叶变换代码。但是,我们只将其提供给自行编译 Audacity 的用户,并在我们的正常构建中使用速度稍慢的版本。只要 Audacity 接受插件,就可以并且一直在争论 Audacity 不能使用 FFTW。FFTW 的作者不希望他们的代码作为通用服务提供给任意其他代码。因此,支持插件的架构决策导致了我们在提供内容方面的权衡。它使 LADSPA 插件成为可能,但阻止我们在预编译的可执行文件中使用 FFTW。
架构也受到如何最好地利用我们稀缺的开发人员时间的考虑。由于开发人员团队规模较小,我们没有资源进行例如对 Firefox 和 Thunderbird 团队所做的安全漏洞的深入分析。但是,我们不希望 Audacity 提供绕过防火墙的途径,因此我们有一条规则,即 Audacity 不允许与 TCP/IP 连接。避免 TCP/IP 消除了许多安全问题。意识到我们有限的资源使我们能够更好地设计。它帮助我们削减功能,这些功能会花费我们过多的开发人员时间,并专注于必不可少的功能。
类似的开发人员时间问题也适用于脚本语言。我们想要脚本,但实现这些语言的代码不需要在 Audacity 中。将每个脚本语言的副本编译到 Audacity 中以给用户提供他们可能想要的所有选择是没有意义的。1 我们改用一个插件模块和一个管道来实现脚本,我们将在后面讨论。
图 2.1:Audacity 中的层
图 2.1 显示了 Audacity 中的一些层和模块。该图突出了 wxWidgets 中三个重要的类,每个类在 Audacity 中都有一个反映。我们正在从相关的较低级抽象构建更高级的抽象。例如,BlockFile 系统是对 wxWidgets 的 wxFiles 的反映,并且建立在它之上。在某个阶段,将 BlockFiles、ShuttleGUI 和命令处理分离到一个独立的中间库中可能是合理的。这将鼓励我们使它们更通用。
在图的下方是“平台特定实现层”的狭窄条带。wxWidgets 和 PortAudio 都是操作系统抽象层。两者都包含条件代码,根据目标平台选择不同的实现。
“其他支持库”类别包括各种库。有趣的是,其中相当一部分依赖于动态加载的模块。这些动态模块对 wxWidgets 一无所知。
在 Windows 平台上,我们过去将 Audacity 编译为一个单一的整体可执行文件,将 wxWidgets 和 Audacity 应用程序代码放在同一个可执行文件中。在 2008 年,我们改为使用模块化结构,将 wxWidgets 作为独立的 DLL。这是为了允许在运行时加载其他可选 DLL,这些 DLL 直接使用 wxWidgets 的功能。在图中虚线以上接入的插件可以使用 wxWidgets。
将 wxWidgets 用于 DLL 的决定有其弊端。现在发行版更大,部分原因是 DLL 中提供了许多以前会被优化掉的不使用函数。Audacity 的启动时间也更长,因为每个 DLL 都单独加载。优点是相当可观的。我们预计模块对我们来说具有与 Apache 相似的优势。在我们看来,模块允许 Apache 的核心非常稳定,同时在模块中促进实验、特殊功能和新想法。模块在很大程度上抵消了将项目分叉以将其带到新方向的诱惑。我们认为这对我们来说是一个非常重要的架构变化。我们期待这些优势,但还没有看到它们。公开 wxWidgets 函数只是第一步,我们还有更多工作要做才能拥有一个灵活的模块化系统。
像 Audacity 这样的程序的结构显然不是预先设计的。它是随着时间推移而发展起来的。总体而言,我们现在拥有的架构对我们来说非常有效。当我们尝试添加影响许多源文件的特性时,我们会发现自己在与架构作斗争。例如,Audacity 目前以特殊的方式处理立体声和单声道轨道。如果您想修改 Audacity 以处理环绕声,则需要在 Audacity 的许多类中进行更改。
超越立体声:GetLink
故事
Audacity 从未有过通道数量的抽象。它使用的抽象是链接音频通道。有一个函数 GetLink
,如果有两个通道,它会返回对中的另一个音频通道;如果轨道是单声道,它会返回 NULL。使用 GetLink
的代码通常看起来就像最初是为单声道编写的,后来使用 (GetLink() != NULL)
测试来扩展该代码以处理立体声。我不确定它是否确实是那样编写的,但我怀疑是。没有使用 GetLink
在链接列表中遍历所有通道的循环。绘制、混合、读取和写入都包含对立体声情况的测试,而不是可以用于 n 个通道的通用代码,其中 n 最有可能为 1 或 2。要使用更通用的代码,您需要更改大约 100 个此类对 GetLink
函数的调用,修改至少 26 个文件。
搜索代码以查找 GetLink
调用很容易,所需的更改也不那么复杂,因此修复这个“问题”并不像乍看起来那么大。GetLink
故事不是关于难以修复的结构缺陷。相反,它说明了一个相对较小的缺陷如何在允许的情况下传播到大量代码中。
事后看来,将 GetLink
函数设为私有,并提供一个迭代器来遍历轨道中的所有通道会更好。这将避免许多针对立体声的特殊情况代码,同时使使用音频通道列表的代码独立于列表实现。
更模块化的设计可能会推动我们更好地隐藏内部结构。随着我们定义和扩展外部 API,我们将需要更仔细地查看我们提供的函数。这将吸引我们注意我们不想锁定到外部 API 的抽象。
对于 Audacity 用户界面程序员来说,最重要的单个库是 wxWidgets GUI 库,它提供了诸如按钮、滑块、复选框、窗口和对话框之类的东西。它提供了最明显的跨平台行为。wxWidgets 库有自己的字符串类 wxString
,它对线程、文件系统和字体有跨平台的抽象,以及用于本地化到其他语言的机制,我们都使用这些。我们建议 Audacity 开发新手先下载 wxWidgets,编译并尝试一些该库附带的示例。wxWidgets 是对操作系统提供的底层 GUI 对象的一个相对薄的层。
为了构建复杂的对话框,wxWidgets 不仅提供单个窗口小部件元素,还提供调整器,这些调整器控制元素的大小和位置。这比为图形元素指定绝对固定位置要好得多。如果窗口小部件的大小直接由用户调整,或者例如通过使用不同的字体大小进行调整,则对话框中元素的位置会以非常自然的方式更新。调整器对于跨平台应用程序很重要。没有它们,我们可能需要为每个平台创建自定义的对话框布局。
通常,这些对话框的设计都放在一个资源文件中,由程序读取。然而,在 Audacity 中,我们专门将对话框设计编译成程序中的 wxWidgets 函数调用序列。这提供了最大的灵活性:即对话框的具体内容和行为将由应用程序级别的代码决定。
曾经,你可以在 Audacity 中找到一些地方,那里用于创建 GUI 的初始代码明显是用图形对话框构建工具生成的。这些工具帮助我们得到了基本的设计。随着时间的推移,基本代码被修改以添加新功能,导致在许多地方,新的对话框都是通过复制和修改现有的、已经修改过的对话框代码来创建的。
经过多年的这种开发,我们发现 Audacity 源代码的大部分,特别是用于配置用户首选项的对话框,由混乱的重复代码组成。这些代码虽然功能简单,但令人惊讶地难以理解。部分问题在于对话框构建的顺序是相当任意的:较小的元素被组合成较大的元素,最终组合成完整的对话框,但代码创建元素的顺序并不(也不需要)类似于元素在屏幕上的布局顺序。代码也很冗长且重复。有用于将磁盘上存储的首选项数据传输到中间变量的 GUI 相关代码,有用于将数据从中间变量传输到显示的 GUI 的代码,有用于将数据从显示的 GUI 传输到中间变量的代码,以及有用于将数据从中间变量传输到存储的首选项的代码。代码中有一些注释,比如 `//this is a mess`,但直到很久之后才有人开始处理它。
解决所有这些代码混乱问题的方案是引入一个新的类,ShuttleGui,它极大地减少了指定对话框所需的代码行数,从而使代码更易读。ShuttleGui 是 wxWidgets 库和 Audacity 之间的一个额外层。它的作用是在两者之间传输信息。以下是一个示例,它将生成 图 2.2 中所示的 GUI 元素。
ShuttleGui S; // GUI Structure S.StartStatic("Some Title",…); { S.AddButton("Some Button",…); S.TieCheckbox("Some Checkbox",…); } S.EndStatic();
图 2.2:示例对话框
这段代码在对话框中定义了一个静态框,该框包含一个按钮和一个复选框。代码和对话框之间的对应关系应该是清晰的。`StartStatic` 和 `EndStatic` 是成对调用。其他类似的 `StartSomething`/`EndSomething` 对(必须匹配)用于控制对话框布局的其他方面。花括号和它们对应的缩进对于正确代码来说不是必需的。我们采用了添加它们来使结构,特别是成对调用的匹配变得明显的约定。在更大的示例中,它确实有助于可读性。
所示的源代码不仅仅创建了对话框。注释“`//GUI Structure`”之后的代码还可以用于将数据从对话框传输到存储用户首选项的地方,以及将数据传输回来。以前,许多重复代码都源于需要这样做。现在,这些代码只写一次,并隐藏在 `ShuttleGui` 类中。
Audacity 中还有其他对基本 wxWidgets 的扩展。Audacity 有自己的类来管理工具栏。为什么它不使用 wxWidgets 内置的工具栏类呢?原因是历史性的:Audacity 的工具栏是在 wxWidgets 提供工具栏类之前编写的。
Audacity 中显示音频波形的mainPanel 是 TrackPanel。这是一个由 Audacity 绘制的自定义控件。它由多个组件组成,例如带有轨迹信息的较小面板、时间轴尺、振幅尺以及可能显示波形、频谱或文本标签的轨迹。轨迹可以通过拖动进行调整大小和移动。包含文本标签的轨迹使用我们自己的可编辑文本框重新实现,而不是使用内置的文本框。你可能会认为这些面板、轨迹和尺子应该各自是一个 wxWidgets 组件,但事实并非如此。
图 2.3:带标签的 TrackPanel 元素的 Audacity 界面
图 2.3 中显示的屏幕截图显示了 Audacity 用户界面。所有被标记的组件都是 Audacity 的自定义组件。就 wxWidgets 而言,TrackPanel 只有一个 wxWidget 组件。Audacity 代码,而不是 wxWidgets,负责在其中进行定位和重绘。
所有这些组件如何组合在一起形成 TrackPanel,这真是太糟糕了。(是代码很糟糕;用户看到的结果看起来很好。)GUI 和特定于应用程序的代码都混在一起,没有干净地分离。在良好的设计中,只有我们特定于应用程序的代码应该了解左右声道、分贝、静音和独奏。GUI 元素应该是特定于应用程序的元素,可以在非音频应用程序中重复使用。即使是 TrackPanel 的纯粹 GUI 部分也是特殊情况代码的拼凑,具有绝对位置和大小,并且没有足够的抽象。如果这些特殊组件是自包含的 GUI 元素,并且如果它们使用与 wxWidgets 相同类型的接口的 sizer,那将好得多、更干净、更一致。
为了得到这样的 TrackPanel,我们需要一个新的 wxWidgets sizer,它可以移动和调整轨迹的大小,或者实际上可以调整任何其他小部件的大小。wxWidgets sizer 还不能做到那么灵活。作为额外的好处,我们可以将该 sizer 用在其他地方。我们可以在包含按钮的工具栏中使用它,从而可以通过拖动轻松地自定义工具栏中按钮的顺序。
已经进行了一些探索性工作来创建和使用这种 sizer,但还不够。一些关于使 GUI 组件成为成熟的 wxWidgets 的实验遇到了一个问题:这样做会减少我们对小部件重绘的控制,从而导致在调整大小和移动组件时出现闪烁。我们需要对 wxWidgets 进行大量修改才能实现无闪烁的重绘,并更好地将调整大小步骤与重绘步骤分开。
谨慎使用这种 TrackPanel 方法的第二个原因是,我们已经知道当有大量小部件时,wxWidgets 的运行速度会变得非常慢。这主要是在 wxWidgets 的控制范围之外。每个 wxWidget、按钮和文本输入框都使用窗口系统的资源。每个都有一个句柄来访问它。处理大量这些句柄需要时间。即使大多数小部件被隐藏或处于屏幕之外,处理速度也很慢。我们希望能够在我们的轨道上使用许多小部件。
最佳解决方案是使用享元模式,即我们自己绘制的轻量级小部件,它们没有对应于消耗窗口系统资源或句柄的对象。我们将使用类似 wxWidgets 的 sizer 和组件小部件的结构,并为组件提供类似的 API,但实际上不从 wxWidgets 类派生。我们将重构我们现有的 TrackPanel 代码,使其结构变得更加清晰。如果这是一个简单的解决方案,它早就已经完成了,但关于我们最终想要得到什么的确切意见分歧破坏了早期的尝试。将我们目前的临时方法推广到一般情况需要大量的设计工作和编码。有一种强烈的诱惑,就是将已经足够好用的复杂代码保留下来。
PortAudio 是一个音频库,它使 Audacity 能够以跨平台的方式播放和录制音频。没有它,Audacity 就无法使用它运行的设备的声卡。PortAudio 提供环形缓冲区,在播放/录制时进行采样率转换,并且最重要的是,提供了一个 API,该 API 隐藏了 Mac、Linux 和 Windows 上音频之间的差异。在 PortAudio 中,有替代的实现文件来支持每个平台的此 API。
我从未需要深入研究 PortAudio 来了解其内部工作原理。但是,了解我们如何与 PortAudio 交互很有用。Audacity 从 PortAudio 接收数据包(录制)并向 PortAudio 发送数据包(播放)。值得关注的是发送和接收是如何发生的,以及它如何与读写磁盘和屏幕更新相吻合。
几个不同的进程同时进行。一些进程频繁发生,传输少量数据,并且必须快速响应。其他进程发生频率较低,传输大量数据,其发生的确切时间并不那么关键。这是进程之间的阻抗不匹配,缓冲区用于解决这个问题。图片的第二部分是我们正在处理音频设备、硬盘驱动器和屏幕。我们没有深入到底层,因此必须使用我们得到的 API。虽然我们希望每个进程看起来都相似,例如,让每个进程都从一个 wxThread 运行,但我们没有这种奢侈(图 2.4)。
图 2.4:播放和录音中的线程和缓冲区
一个音频线程由 PortAudio 代码启动,并直接与音频设备交互。这就是驱动录制或播放的线程。此线程必须响应迅速,否则数据包将丢失。该线程在 PortAudio 代码的控制下,调用 `audacityAudioCallback`,该函数在录制时将新到达的小数据包添加到一个较大的(5 秒)捕获缓冲区中。在播放时,它从一个 5 秒的播放缓冲区中取出小块数据。PortAudio 库对 wxWidgets 一无所知,因此由 PortAudio 创建的此线程是一个 pthread。
第二个线程是由 Audacity 的 AudioIO 类中的代码启动的。在录制时,AudioIO 从捕获缓冲区获取数据,并将其附加到 Audacity 的轨道上,以便最终显示出来。此外,当添加了足够的数据时,AudioIO 将数据写入磁盘。同一个线程还执行音频播放的磁盘读取操作。`AudioIO::FillBuffers` 是这里的主要函数,它根据一些布尔变量的设置,在一个函数中处理录制和播放。一个函数同时处理这两个方向非常重要。当进行“软件播放”,即你对之前录制的内容进行叠录时,录制和播放部分都会同时使用。在 AudioIO 线程中,我们完全受制于操作系统磁盘 IO。我们可能会在读取或写入磁盘时停顿未知的时间。我们不能在 `audacityAudioCallback` 中执行这些读取或写入操作,因为需要在那里快速响应。
这两个线程之间的通信通过共享变量进行。因为我们控制哪些线程在何时写入这些变量,所以我们避免了对更昂贵的互斥锁的需求。
在播放和录制中,还有一个额外的要求:Audacity 也需要更新 GUI。这是最不重要的操作。更新发生在主 GUI 线程中,并且是由每秒滴答 20 次的周期性计时器引起的。此计时器的滴答导致调用 `TrackPanel::OnTimer`,如果发现需要更新 GUI,则应用这些更新。此主 GUI 线程是在 wxWidgets 内创建的,而不是由我们自己的代码创建的。它很特殊,因为其他线程不能直接更新 GUI。使用计时器让 GUI 线程检查是否需要更新屏幕,可以让我们将重绘次数减少到可接受的响应显示水平,并且不会对用于显示的处理器时间造成过大的负担。
使用一个音频设备线程、一个缓冲区/磁盘线程和一个带周期性计时器的 GUI 线程来处理这些音频数据传输,这是一种好的设计吗?使用这三个不同的线程,而这些线程不基于单个抽象基类,这在某种程度上是临时的。但是,这种临时性在很大程度上是由我们使用的库决定的。PortAudio 预计会自行创建线程。wxWidgets 框架会自动拥有一个 GUI 线程。我们对缓冲区填充线程的需求,是由我们对解决音频设备线程的频繁小数据包与磁盘驱动器的较不频繁的大数据包之间阻抗不匹配的需求所决定的。使用这些库有非常明显的优势。使用这些库的代价是,我们最终会使用它们提供的抽象。因此,我们比严格必要的情况更多地将数据从内存中复制到另一个地方。在我参与的快速数据交换中,我看到了用于处理这些阻抗不匹配的非常高效的代码,该代码是中断驱动的,根本不使用线程。指针会传递到缓冲区,而不是复制数据。只有在使用的库设计有更丰富的缓冲区抽象时,才能这样做。使用现有接口,我们被迫使用线程,也被迫复制数据。
Audacity 面临的一个挑战是支持对可能长达数小时的音频录音进行插入和删除操作。录音很容易长到无法放入可用内存中。如果音频录音存储在一个磁盘文件中,那么在该文件的开头附近插入音频,可能意味着移动大量数据以腾出空间。在磁盘上复制这些数据将非常耗时,这意味着 Audacity 无法快速响应简单的编辑操作。
Audacity 对此的解决方案是将音频文件分成许多块文件,每个块文件的大小约为 1 MB。这是 Audacity 拥有自己的音频文件格式的主要原因,该格式是一个具有 .aup
扩展名的主文件。它是一个 XML 文件,用于协调各个块。对长音频录音开头附近的更改可能只影响一个块和主 .aup
文件。
块文件平衡了两种相互冲突的力量。我们可以插入和删除音频,而无需过度复制,并且在播放过程中,我们保证每次向磁盘请求时都会获得相当大的音频块。块越小,获取相同数量的音频数据所需的磁盘请求次数就越多;块越大,插入和删除时的复制次数就越多。
Audacity 的块文件内部永远没有空闲空间,并且它们永远不会超过最大块大小。为了在插入或删除时保持这一点,我们最终可能需要复制最多一个块的数据。当我们不再需要块文件时,我们会将其删除。块文件是引用计数的,因此,如果我们删除一些音频,相关的块文件仍将保留以支持撤消机制,直到我们保存为止。我们永远不需要在 Audacity 块文件中回收空闲空间,这在单文件方法中是必要的。
合并和拆分更大的数据块是数据管理系统的基本操作,从 B 树到 Google 的 BigTable 存储区,再到展开的链接列表的管理,都是如此。图 2.5 显示了在 Audacity 中删除开头附近的一段音频时发生的情况。
图 2.5:删除之前,.aup
文件和块文件保存着序列 ABCDEFGHIJKLMNO。删除 FGHI 后,两个块文件合并。
块文件不仅用于音频本身。还有一些块文件用于缓存摘要信息。如果要求 Audacity 在屏幕上显示一个长达四个小时的录音,它不能每次重绘屏幕时都处理所有音频。相反,它使用摘要信息,该信息给出时间范围内音频的最大和最小振幅。放大时,Audacity 使用实际样本进行绘制。缩小时,Audacity 使用摘要信息进行绘制。
块文件系统的一个改进是,块不需要是 Audacity 创建的文件。它们可以是音频文件子部分的引用,例如存储在 .wav
格式中的音频的时间段。用户可以创建一个 Audacity 项目,从 .wav
文件导入音频,并混合多个音轨,而只为摘要信息创建块文件。这可以节省磁盘空间,并减少音频复制时间。总之,这是一个相当糟糕的主意。我们的很多用户都删除了原始的 .wav
文件,以为 Audacity 项目文件夹中会有一个完整的副本。事实并非如此,如果没有原始的 .wav
文件,音频项目就无法播放。如今,Audacity 的默认做法是始终复制导入的音频,并在过程中创建新的块文件。
块文件解决方案在 Windows 系统上遇到了问题,因为在 Windows 系统上,大量的块文件性能非常差。这似乎是因为 Windows 处理同一目录中的大量文件时速度要慢得多,与大量小部件导致速度变慢的问题类似。后来的一个补充是使用子目录的层次结构,每个子目录中的文件数量不超过一百个。
块文件结构的主要问题是它向最终用户公开。我们经常从用户那里听到,他们移动了 .aup
文件,但没有意识到还需要移动包含所有块文件的文件夹。如果 Audacity 项目是一个单个文件,并且 Audacity 负责如何使用文件内部的空间,情况会更好。如果有什么不同的话,这将提高性能,而不是降低性能。主要需要添加的代码是用于垃圾回收的代码。一个简单的做法是在保存时将块复制到一个新文件,如果超过一定比例的文件未被使用。
Audacity 有一款实验性的插件,支持多种脚本语言。它通过命名管道提供脚本接口。通过脚本公开的命令采用文本格式,响应也是如此。只要用户的脚本语言能够向命名管道写入文本并从命名管道读取文本,该脚本语言就可以驱动 Audacity。音频和其他大量数据不需要在管道上传输 (图 2.6)。
图 2.6:脚本插件通过命名管道提供脚本
插件本身不知道它承载的文本流量的内容。它只负责传递它。脚本插件用于插入 Audacity 的插件接口(或基本扩展点)已经以文本格式公开了 Audacity 命令。因此,脚本插件很小,它的主要内容是管道代码。
不幸的是,管道引入了与 TCP/IP 连接类似的安全风险——我们出于安全原因排除了 Audacity 上的 TCP/IP 连接。为了降低这种风险,插件是一个可选的 DLL。您必须做出一个有意识的决定来获取和使用它,并且它附带一个健康/安全警告。
在脚本功能启动后,我们 wiki 的功能请求页面出现了一个建议,建议我们考虑使用 KDE 的 D-Bus 标准来提供使用 TCP/IP 的进程间调用机制。我们已经开始走一条不同的路线,但将我们最终得到的接口改编为支持 D-Bus 仍然是有意义的。
脚本代码的起源
脚本功能源于爱好者对 Audacity 的改编,以满足特定的需求,这些需求正朝着分叉的方向发展。这些功能称为 CleanSpeech,用于将布道转换为 mp3 格式。CleanSpeech 添加了新的效果,例如截断静音——该效果会查找并剪切掉音频中的长静音——以及对一批音频录音应用固定序列的现有降噪效果、归一化和 mp3 转换的能力。我们想要这个功能中的一些优秀功能,但是它的编写方式对于 Audacity 来说过于特殊。将它引入主流 Audacity 使我们编写了灵活序列的代码,而不是固定序列。灵活序列可以使用任何效果,通过查找表查找命令名称,并使用 Shuttle
类将命令参数持久化为用户首选项中的文本格式。此功能称为批处理链。我们非常有意地没有添加条件或计算,以避免发明一种临时的脚本语言。
回顾起来,避免分叉的努力是值得的。Audacity 中仍然存在 CleanSpeech 模式,可以通过修改首选项来设置。它还会缩减用户界面,删除高级功能。人们要求为其他用途提供一个简化的 Audacity 版本,最引人注目的用途是在学校。问题是,每个人对哪些是高级功能,哪些是基本功能的看法都不一样。我们后来实现了一个简单的 hack,利用了翻译机制。当菜单项的翻译以“#”开头时,它将不再显示在菜单中。这样,想要减少菜单的人就可以自己做出选择,无需重新编译——这比 Audacity 中的 mCleanspeech
标志更通用,侵入性更低,随着时间的推移,我们最终可能完全将其删除。
CleanSpeech 的工作为我们带来了批处理链以及截断静音的功能。这两项功能都从核心团队之外吸引了额外的改进。批处理链直接导致了脚本功能。这反过来又开始了支持更通用的插件以适应 Audacity 的过程。
Audacity 没有实时效果,即按需计算的音频效果,这些效果在音频播放时计算。相反,在 Audacity 中,您需要应用效果,然后等待它完成。实时效果以及在用户界面保持响应的同时在后台渲染音频效果,是 Audacity 最常被要求的功能之一。
我们遇到的一个问题是,在一台机器上可能是实时效果的某些效果,在速度慢得多的机器上可能运行速度不够快,无法成为实时效果。Audacity 可以在各种机器上运行。我们希望有一个优雅的回退机制。在速度较慢的机器上,我们仍然希望能够请求将效果应用于整个音轨,然后在等待一小段时间后,在音轨的中间附近收听处理后的音频,同时 Audacity 知道要先处理该部分。在速度太慢而无法实时渲染效果的机器上,我们将能够收听音频,直到播放赶上渲染。要做到这一点,我们需要删除音频效果会挂起用户界面以及音频块的处理顺序严格从左到右的限制。
Audacity 中一个相对较新的功能称为按需加载,它具有我们实现实时效果所需的许多元素,尽管它与音频效果无关。当您将音频文件导入 Audacity 时,它现在可以在后台任务中创建摘要块文件。Audacity 将显示一个蓝色和灰色条纹的对角线占位符,用于表示尚未处理的音频,并在音频仍在加载时响应许多用户命令。块不需要按从左到右的顺序进行处理。一直以来的目标是,同一代码最终将用于实时效果。
按需加载为我们提供了一种逐步添加实时效果的方法。它是一个步骤,可以避免将效果本身设为实时效果的一些复杂性。实时效果还需要块之间的重叠,否则回声等效果将无法正确连接。我们还需要允许参数在音频播放时发生变化。通过首先执行按需加载,代码将在比其他情况更早的阶段使用。它将从实际使用中获得反馈和改进。
本章前面的部分说明了良好的结构如何促进程序的增长,或者缺乏良好的结构如何阻碍程序的增长。
你看得越多,就越明显地发现 Audacity 是一个社区共同努力的产物。该社区不仅包括直接贡献的人,因为它依赖于库,每个库都有自己的社区,拥有自己的领域专家。在阅读了关于 Audacity 结构组合的介绍之后,你可能不会感到意外,开发它的社区欢迎新的开发者,并且能够很好地处理各种技能水平。
对我来说,毫无疑问,Audacity 背后社区的性质反映在代码的优缺点上。一个更封闭的团体可以比我们更一致地编写高质量的代码,但要以更少的人员参与来匹配 Audacity 的功能范围会更加困难。