当抽象层失效:DigiMixer与Behringer Wing的技术适配挑战

本文探讨了在DigiMixer软件中集成Behringer Wing混音器时遇到的抽象层失效问题,详细分析了音频信号路径建模、抽象层设计缺陷及三种解决方案,展示了实际开发中的架构决策与技术债务权衡。

当抽象层失效

在我撰写上一篇DigiMixer预览文章时,我正期待着我最新混音器的到来:Behringer Wing机架式混音器。几天后它到货了,我很高兴地说,将其与DigiMixer集成并没有花费太长时间。(它现在是我的主力混音器。)虽然大部分集成过程一帆风顺,但Wing有一个方面与抽象层不太匹配——这使它成为博客文章的绝佳主题。

Wing输出通道

在现实世界(与课程中通常给出的刻意示例相反),抽象层总是会有点失效。世界并不像我们希望的那样整齐地放入盒子中。重要的是要区分故意有损的抽象和抽象中的实际“断裂”。有损抽象忽略了一些对于我们使用抽象方式不重要的细节。例如,DigiMixer在混音器功能方面非常有损:它不尝试建模混音器的路由、可应用的任何效果器,或输入如何配置前置放大器增益、微调、立体声平衡等。这一切都很好,尽管Wing有很多功能未被抽象捕获,但这并不新鲜。

但当抽象层无法表示我们关心的方面时,抽象就会失效。在Wing的情况下,那就是主输出。让我们重新审视我之前提到的关于通道的内容:

每个通道都有以下信息:

  • 其名称
  • 其推子电平(对于输入通道,这是“每个输出通道一个推子电平”)。这可以由应用程序控制。
  • 是否静音。这可以由应用程序控制。
  • 表头信息(即当前输入和输出电平)

混音器都有输入和输出通道。有不同类型的输入和不同类型的输出,但在大多数情况下,我们不需要区分这些“类型”。混音器可能会这样做,但DigiMixer不必如此。它确实有“主”输出的概念,假定为单个立体声输出通道。这有一对预设的通道ID(100和101),并且X-Touch Mini集成确实特殊处理这一点,假设顶部的旋转编码器应用于控制每个输入的主电平。但在大多数情况下,它只被视为一个普通通道,有自己的音量推子和静音。

插曲:音频信号的路径

我想花点时间确保我已经清楚地说明了音频路径的工作原理,至少是简单形式。假设我们有一个简单的混音器,有两个输入和一个主输出。暂时忘记单声道/立体声。

我们会有三个推子(用于控制音量的物理滑块):

  • 一个用于输入1
  • 一个用于输入2
  • 一个用于输出

这意味着,如果有人通过麦克风在输入1唱歌,电吉他在输入2提供信号,你可以:

  • 通过调整输入推子来改变唱歌和吉他之间的平衡
  • 通过调整输出推子来改变整体音量

还会有三个静音按钮:每个输入一个,输出一个。因此,如果麦克风开始产生反馈,你可以只静音它(但保持吉他可听),或者你可以用输出静音按钮静音一切。

如果我们有两个输出——称它们为“主”和“辅助”——那么逻辑上会有六个推子(在物理控制台上它们不太可能都是单独的滑块):

  • 一个用于输入1馈送到主输出的信号
  • 一个用于输入1馈送到辅助输出的信号
  • 一个用于输入2馈送到主输出的信号
  • 一个用于输入2馈送到辅助输出的信号
  • 一个用于主输出
  • 一个用于辅助输出

将不同推子电平应用于不同输出的不同输入的能力是我们每周在教堂使用的:我们有一个麦克风拾取会众的歌声,以便通过Zoom发送……但我们根本不想在教堂建筑内放大它。同样,对于某人说话,我们可能在建筑内比在Zoom上放大更多,或者反之。

DigiMixer建模静音的方式是,每个输入只有一个静音,每个输出只有一个静音,因此在我们的“两个输出”混音器上,我们会有六个推子,但只有四个静音。实际上,大多数混音器确实提供“每个输入,每个输出”静音,但也有链接静音的概念,其中静音一个输入通道会静音所有输出。

但这一切的结果是,即使在我们简化的模型中,从输入到输出的音频信号也通过包含两个推子和两个静音的路径:有多种方式可以调整音量或应用静音,取决于你想要做什么。

考虑到这一点,让我们回到Wing……

Wing上的主LR与主1-4

Behringer Wing有很多通道:48个立体声输入通道和20个立体声输出通道(加上矩阵混音和其他复杂功能——我在这里简化了很多,坦白说,我离完全理解Wing的能力还有很长的路)。输出分为四个主通道(M1-M4)和16个总线通道(B1-B16)。每个输出都有一系列每输入推子,自己的整体输出推子和一个静音。到目前为止,一切顺利。

然后还有“主LR”。

“主LR”听起来应该是一个常规的立体声主输出通道,通道ID为100和101,没有问题。就每个输入有一个主LR推子而言,这很好。

但主LR本身实际上不是一个输出。它没有自己的“整体”推子或静音。它没有表头电平。你不能将它路由到任何东西。用推子调整的输入电平应用于所有M1-M4,然后还被输入到M1到M4的推子调整。因此,如果你有一个从M1发送到扬声器的单个输入,你有三个推子可以用来调整它:

  • 该输入的主LR推子
  • 该输入的M1推子
  • 整体M1推子

在该场景中有两个静音选项:

  • 输入的静音
  • M1的静音

DigiMixer中的主LR

所有这些都可以在DigiMixer中表示——我们可以为Main LR添加一个“伪”输出通道——并且这样做确实有用,作为通过X-Touch Mini上的旋转编码器调整的“主要输入”推子。

但然后我们得到三个我们不想要的东西,因为它们在混音器本身上没有表示:

  • 一个主LR整体推子
  • 一个主LR表头
  • 一个主LR静音

抽象层没有足够的细微差别来表示这一点——它没有“仅用于输入推子的输出通道”的概念。

这三个额外的部分最终在DigiMixer中显示为无用的用户界面元素。我不是UI设计师(正如我认为我们通过之前部分的截图已经确立的那样),但即使我也知道足够多,以至于对什么都不做的UI元素感到厌恶。

处理失效的抽象层

希望我已经合理地解释了我之前描述的DigiMixer抽象层如何最终对Wing不足。(如果没有,请留下评论,我会尝试使其更清晰。我怀疑在没有实际操作真实混音器的情况下,仅仅移动不同的推子看看会发生什么,完全“理解”它本质上是棘手的。)

下一步大概是修复抽象层,对吧?嗯,也许。我想出了三个选项,我认为这些可能合理地代表了大多数类似情况下可用的选项。

选项1:忽略它

UI很糟糕,有一个从不显示任何东西的表头,以及一个似乎可操作但实际上根本不调整混音器的推子和静音按钮。

但是……所有应该工作的UI元素都工作。这比完全忽略主LR通道要好得多,这会减少功能。

我本可以完全忽略这个问题。有时这绝对没问题——重要的是权衡抽象层断裂的实际后果与处理它的成本。这就是考虑你对抽象层将如何使用的了解程度很重要的地方。DigiMixer应用程序(复数,但都是我写的)是DigiMixer抽象层的唯一消费者。除非其他人开始编写自己的应用程序(我想是可能的——都是开源的),否则我可以推理断裂的所有影响。

如果这是Noda Time,例如,那将是另一回事——人们将Noda Time用于各种事情。当然,这并不意味着Noda Time中暴露的抽象层没有尖锐的角落。我可以用这些填充多篇博客文章——包括我如何考虑修复它们、兼容性问题等。

选项2:扩展抽象层

使DigiMixer中的核心抽象层更具信息性并不难。实际上只是更新从DetectMixerConfiguration返回的MixerChannelConfiguration以包含更多每通道细节。至少,这将是一个起点:该信息然后将在“中间”层被消耗,并再次向上暴露给应用层。

我本可以直接实现这个选项……但有一件事仍然困扰着我:可能还有另一个变化即将到来。扩展抽象层以完美适应Wing可能会使以后的生活更困难,当有另一个混音器以某种稍微不同的方式破坏模型时。我宁愿等到有更多的点来画一条直线,如果你明白我的意思。

当然,“等待和观察”策略存在风险和开放性问题:我等多久?如果我在六个月内没有看到类似的东西,我是否应该在那时“正确地完成工作”?也许一年?我等得越久,代码中的一些丑陋之处存在的时间就越长——但我越早停止等待,出现其他事情的机会就越高。

再次,这种时间方面在比DigiMixer重要得多的抽象层中相当常见。成本通常也会上升:如果DigiMixer已经作为一组遵循语义版本控制的NuGet包发布,那么我要么必须尝试找出如何扩展抽象层而不进行破坏性更改,要么升级到新的主版本。

选项3:接受泄漏性

目前,我选择在较低层次保持抽象层断裂,并仅在应用层处理问题。我已经创建的WPF用户控件使得使用数据绑定来条件化静音和表头是否可见足够容易。推子稍微棘手一些,部分原因是DigiMixer应用程序的两种“模式”:按输出分组输入,或按输入分组输出。基本上,这最终是关于决定将哪些推子包含在集合中。

下一个问题是如何用正确的信息初始化视图模型。这本可以在配置文件中完成——但那会有与选项2相同的问题。相反,我完全弄脏了:当设置混音器视图模型时,代码知道硬件类型是什么(通过配置)。因此我们可以只说“如果是Behringer Wing,适当调整事物”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// The Wing has a "Main LR" channel which doesn't have its own overall fader, mute or meter.
// We don't want to show those UI elements, but it's an odd thing to model on its own.
// For the moment, we detect that we're using a Wing and just handle things appropriately.
if (Config.HardwareType == DigiMixerConfig.MixerHardwareType.BehringerWing)
{
    // Note: no change for InputChannels, as we *want* the fader there.
    // Remove the "overall output" fader, meters and mute for Main LR when grouping by output.
    foreach (var outputChannel in OutputChannels)
    {
        if (outputChannel.ChannelId.IsMainOutput)
        {
            outputChannel.RemoveOverallOutputFader();
            outputChannel.HasMeters = false;
            outputChannel.HasMute = false;
        }
    }
    // Remove the whole "overall output" element when grouping by output.
    OverallOutputChannels = OverallOutputChannels.Where(c => !c.ChannelId.IsMainOutput).ToReadOnlyList();
}

这段代码违反了拥有抽象层的初衷。混音器视图模型不应该知道或关心它在与什么硬件对话!然而……然而,它有效。

我不是建议这通常是正确的方法。但有时,这是一个实用的方法。这绝对是需要小心的事情——如果我必须为下一个混音器添加类似的块,我会更加不情愿。这是我故意承担的技术债务的一个例子。我希望将来移除它——我希望将来有更多信息指导我转向选项2。目前,我会忍受它。

结论

我总是说我希望DigiMixer展示抽象层的现实世界问题以及干净的一面。我没有期望在上篇文章之后这么快就得到如此清晰的例子。

在我的下一篇文章中——如果一切按计划进行——我将研究一个听起来简单但花了我很长时间才达到当前方法的设计挑战。我们将一起研究“单位”,就推子和表头而言。希望那会比这段文字听起来更有趣……

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计