便携式实时应用程序
在当今数据处理的可视化世界中,许多人认为用计算机解决问题意味着实现图形用户界面 (GUI)。从这个角度来看,编写实时应用程序意味着编写 GUI,同时掌握确保可预测响应时间的系统相关函数,通常与神秘的硬件功能相结合。这种系统相关的 GUI 和硬件相关的实时功能的混合通常会导致复杂、昂贵且不可移植的应用程序。为了解决编写便携式实时应用程序的普遍问题,我将首先回顾 UNIX 操作系统的根源。然后,我将把学到的经验应用到一个简单的多媒体应用程序中,该应用程序在三个非常不同的平台上运行:Linux、IRIX 和 Win32 (MS Windows 95/98/NT/2000)。
大多数人都知道正弦波或方波听起来是什么样的。它们可以作为 PC 扬声器、电话或常见乐器(如长笛)的蜂鸣声或测试信号听到。我们将实现的便携式应用程序会产生这样的声音。正弦波、三角波和方波只是 Duffing 振荡器产生的更通用波形的特殊情况(请参阅“资源”)。根据参数(可以通过 GUI 中的两个滑块进行调整),该振荡器还能够发出混沌声音。(混沌,在这种意义上,其含义来自非线性动力学或混沌理论。)当启动应用程序时,您实际上可以通过聆听声音并观察 Duffing 振荡器的图形行为来测试一些研究结果。尽管如此,这样的应用程序必须连续产生声音,否则声音会被咔哒声甚至静音所扭曲。因此,此应用程序是实时应用程序的一个示例。
在开始我们的实现之前,我们必须考虑这种便携式应用程序的设计,暂时忘记我们想要用 GUI 实现我们的多媒体应用程序。大多数 UNIX 程序员都知道UNIX 哲学的含义。在用户界面都是文本而非图形的旧时代,Kernighan 和 Pike 在他们的著作《UNIX 编程环境》的后记中解释了这个概念。他们强调将问题分解为独立的子问题的重要性,子问题之间具有简单的接口,通常是管道。在进程之间放置管道意味着将一个进程的文本输出作为下一个进程的文本输入读取。然后,每个子问题都独立地逐步实现和测试,最好是通过应用现有工具。
这种设计理念允许编写便携式应用程序,并且与今天的开发环境形成鲜明对比。今天,许多程序员使用一些可视化开发环境来构建绑定到一个平台的单体应用程序。
关键问题是:基于 GUI 的实时应用程序可以像旧式的 UNIX 管道一样实现吗?它可以——你只需要选择合适的工具。一个基于 GUI 的应用程序,允许调整参数、发出声音和可视化结果,可以分解为进程管道:GUI-->声音生成-->声音输出-->图形。
阶段 1 (GUI) 提供了一种交互式调整数学模型参数的方法。这些参数可以用旋钮、滑块或二维平面中的一个点进行调整,以最直观的方式为准。它可以很容易地被替换,而不会影响其他阶段,只要它在其标准输出上产生相同类型的数据。
阶段 2(生成)对用户是不可见的,因此不需要 GUI。它从阶段 1 获取参数,并使用物理过程的数学模型处理它们。此阶段唯一的挑战是同时进行参数输入和连续计算,但速率不同步。
阶段 3(声音发射)读取生成的波形并将其交给声音系统。由于 Linux、IRIX 和 Win32 等平台上的声音系统实现存在显着差异,因此我们需要一些封装平台相关代码的经验。幸运的是,这是唯一为特定平台编写的阶段。
阶段 4(图形输出),就像阶段 1 一样,与用户接触,因此将作为 GUI 实现。就像阶段 1 一样,结果可以用许多不同的方式表示,而不会影响其他阶段。示例包括表格、简单的幅度图、相空间轨迹或 Poincaré 截面。
每个阶段也可以单独使用或作为完全不同应用程序中的构建块。阶段 3 是最有趣的构建块。
很明显,从阶段 2 到阶段 3 传输大量数据是系统的瓶颈。写入管道、再次读取和扫描每个样本比其他任何操作都消耗更多的 CPU 时间。因此,阶段 2 和 3 必须集成到一个程序中,因为传递数据(每秒 44,100 个值)会花费太多时间。您可能会认为将这些阶段集成到一个阶段中是一个设计缺陷——事实并非如此。我可以轻松地重新编号阶段并更改本文,但我更愿意向您展示实时生活对天真的软件设计师是多么残酷。
对于阶段 1,Tcl/Tk 是实现第一个子问题的自然选择工具。许多人已经忘记,Tcl/Tk 中的 GUI 进程也有一个文本标准输出,可以管道传输到第二个进程。
在阶段 2 和 3 中,声音生成器从 GUI 进程读取文本参数,并从中实时计算出适当的声音信号。因此,它应该用 C 语言编写,因为就实时约束而言,它是最关键的子问题。作为声音生成的副作用,图形化结果所需的数据将被写入标准输出并管道传输到应用程序的最后阶段。
由于阶段 4 输出结果的图形,因此它也非常适合像 Tcl/Tk 这样的工具。
用户只会注意到管道的阶段 1 和 4,因为他可以将它们中的每一个都看作一个窗口并与之交互。一个有趣的悖论是,看似重要的阶段 1 和 4 使用像 Tcl/Tk 这样的工具实现起来相当简单。阶段 2 和 3 虽然大多不引人注意,但由于实时同步概念(select 或线程)、由于连续声音发射而产生的实时约束以及声音系统的不同处理方式和所有平台依赖性,因此是最具挑战性的子问题。
我们已经提到,这种管道方法的一个优点是将开发分解为很大程度上独立的子任务,这允许一位程序员同时处理每个任务。同样重要的是,每个阶段也可以由不同的程序员以不同的方式实现。为了证明这一点,我们将查看阶段 1 任务的三个解决方案
命令行上的文本用户界面
带有 Tcl/Tk 的 GUI(列表 1 和 2)
带有 Netscape 浏览器和 GNU AWK 3.1 作为服务器的 GUI(列表 3,未打印,但包含在存档文件中)
此外,我们将查看阶段 4 的三种不同实现
文本输出到文件
使用 GNUPLOT 图形输出此数据(图 5)
使用 Tcl/Tk 图形输出(列表 5)
现在系统的设计已经明确,是时候更精确地了解我们想要产生的声音类型了。想象一下一个驱动的钢梁,底部和顶部用固定支架固定。当从侧面驱动梁时,固定支架会在有限的挠度处引起膜张力。这导致了适度大挠度的硬化非线性刚度,由三次项表示。在本世纪初,来自德国柏林的工程师 Georg Duffing 对来自振动机器部件的这种噪音感到恼火。这种噪音不仅是一种滋扰,还会缩短机器部件的预期寿命。Duffing 发现了一个简单的非线性微分方程,描述了机器部件在某些情况下的行为
x'' + kx' + x3 = Bcos(t)
该振荡器由方程右侧的正弦力(幅度为B)驱动,并由方程左侧的参数k阻尼。因此,在这个驱动振荡器中只有两个自由参数。

图 1. 更改参数k(阻尼)和B(驱动)将使振荡器进入或退出混沌状态(参见第 11 页,Thompson/Stewart)。
在图 3 中,您可以看到来自此类声音机器的短波形。与 Duffing 不同,您可以通过使用 GUI(如图 1 和图 2 所示)更改参数来模拟计算机的噪声产生。您应该期望更改图 1 右轴上的参数B只会影响噪声的音量。实际上,通过将B推到其最小位置 0,您可以实际关闭噪声。当将参数B推到最大值时,噪声不仅会变得更大,还会改变主频率,但不会以连续和单调的方式改变。这种频率随响度变化的奇怪行为不会在线性振荡器中发生。1980 年,Ueda 系统地研究了由图 1 中的参数B和k打开的平面中的点。通过计算机模拟,他发现了振荡器发出混沌声音的区域。这些结果总结在 Thompson 和 Stewart 的书中(请参阅“资源”)。
为什么计算这些波形如此困难?毕竟,对于每个时间瞬间,应该只需要评估一个公式;但是,没有这样的解析函数。当遇到麻烦时,工程师通常会求助于简单的近似值。我们将使用一种称为有限差分法的技术(请参阅“资源”)来做到这一点,该技术在每个时间瞬间为我们提供了一个单行计算(列表 4)。
使用 Tcl/Tk,实现阶段 1 非常简单,我们可以考虑两种不同的实现。当使用命令 wish -f duffing.tcl 执行列表 2 中的脚本时,两个独立的参数k和B被可视化为二维坐标系的轴。将圆圈拖动到地图上的任何位置,新坐标将打印到标准输出。但是,您可能更喜欢列表 1 中所示的更简单的实现,它将两个参数都显示为刻度。如果您在没有 GUI 的计算机上运行,您仍然可以使用此处提供的软件。在这种情况下,忘记阶段 1;阶段 2 将从命令行读取其输入。在运行时输入类似 k 0.05 或 B 7.5 的行,阶段 2 不会注意到差异。
出于效率原因,阶段 2 和 3 必须集成到一个程序中。将每个阶段实现为单个执行线程是很诱人的。执行线程的行为主要类似于共享单个数据空间的进程:一个等待输入以修改参数,另一个计算要发出的波形。如何在便携式方式中实现执行线程?POSIX 线程库(请参阅“资源”)现在可用于大多数操作系统,包括前面提到的那些操作系统。例如,在 STN Atlas Elektronik,我们在多处理器设置(两个 CPU 和 Linux 2.2 SMP)中使用线程进行声音生成,并使用多个声卡。正如 David Butenhof 的优秀著作中所解释的那样,线程使软件调试变得非常困难;因此,我们将避免在此处使用它们。

图 2. 此用户界面允许精确调整参数。请注意,您实际上可以听到混沌状态;它们听起来明显“脏”,不像其他区域那样“干净”。
列表 4. C 语言的阶段 2 和 3,integrator.c
幸运的是,处理不同步事件的问题可以使用经常被低估的 select 系统调用来解决(列表 4,函数main)。列表 4 的主循环有一个短循环来检查是否有来自标准输入的数据。然后,它通过有限差分法计算数据块,最后发出它。在计算时,一些数据点被打印到标准输出。只打印那些在驱动力的周期周期的整数倍处发生,并且具有相同相位角的数据点。这种选择要显示的数据点的技术是 Poincaré 截面的核心,Poincaré 截面是一种频闪观测器,可以揭示混沌数据中的隐藏顺序(图 5)。请注意,此数据是阶段 4 的输入。
图 3. 默认参数显示对初始条件的敏感依赖性(参见第 4 页,Thompson/Stewart)。
这是容易的部分;困难的部分是以便携式方式将数据交给声音系统。在这方面,Linux 是最容易处理的平台。将数据写入特殊文件 /dev/dsp 就足够了。对于 IRIX,我们只需要一个函数调用;不是通常的write,而是一个特殊的声音函数。在这两种情况下,同步都是通过这些函数的阻塞行为来实现的。这与 Win32 形成对比,Win32 用缓冲区处理来困扰程序员,并且必须使用回调函数来完成同步。使用新的 DirectSound API 不是一种选择,因为 Microsoft 未能在 Windows NT 中实现 DirectX API。
阶段 4 再次非常容易实现(列表 5,图 5)。读取的每个数据点都作为相空间图中的一个点打印出来。当生成如阶段 3 中的 Poincaré 截面数据时,线性振荡器会产生圆形或螺旋形,退化为固定点,这相当枯燥。需要一个混沌振荡器来绘制图 5 中所示的奇异吸引子。
图 4. 一个混沌吸引子的高分辨率 Poincaré 截面
在边栏“如何编译和运行列表 4”中,您可以看到如何在不同平台上编译阶段 3 程序以及如何作为 UNIX 管道启动整个应用程序。您可能会惊讶地发现编译器gcc甚至可以与 Win32 操作系统一起使用。这怎么可能?Win32 的 gcc 是一个名为 Cygwin Toolset 的 UNIX 兼容环境的一部分(请参阅“资源”)。它允许您在任何 Win32 操作系统上使用 gcc 及其朋友,就像它是一个行为良好的 UNIX 系统一样。许多 GNU 软件包都可以开箱即用。如果要使用 Win32 的多媒体功能(以及 DirectX 访问)或 POSIX 线程库,则必须单独获取并安装它们(请参阅“资源”)。自从我开始使用 Win32 以来,遵守 XPG4 标准集中写下的 POSIX 系统调用变得越来越重要。这样做已经成为一种有益的习惯,因为它是 UNIX 世界中可移植性的基石。
最令我高兴的是可以在我的 Linux 2.2 系统上从 Cygwin Toolset 安装 gcc 作为交叉编译器。现在我可以使用 Linux 编写和调试我的软件,无论它应该在何处运行。最后,我使用交叉编译器编译它并获得调试后的 Win32 可执行文件。多么了不起的成就!如果您也想以这种方式工作,请遵循 Mumit Khan 烹饪交叉编译器的食谱(请参阅“资源”)。
Jürgen Kahrs (Juergen.Kahrs@t-online.de) 是德国不莱梅 STN Atlas Elektronik 的一名开发工程师。在那里,他使用 Linux 在教育模拟器中生成声音。他喜欢 GNU AWK 和 Tcl/Tk 等老式工具。他还为将 TCP/IP 支持集成到 gawk 中做了初步工作。