Resurrecting the Panasonic WJ-MX50 in WebGPU
Resurrecting the Panasonic WJ-MX50 in WebGPU
在 WebGPU 中重现 Panasonic WJ-MX50
In which a 1990s two-bus video mixer is reduced to a reducer, and the operating manual turns out to be a test suite. The Panasonic WJ-MX50 was a desktop digital A/V mixer sold in the early 1990s for wedding videographers, cable-access studios, and anyone else doing A/B-roll editing on S-VHS decks. 在这篇文章中,一台 90 年代的双总线视频切换台被简化为一个“归约器”(reducer),而操作手册则成了测试套件。Panasonic WJ-MX50 是一款 90 年代初销售的桌面数字音视频切换台,主要面向婚礼摄像师、有线电视工作室以及任何在 S-VHS 录像机上进行 A/B 卷编辑的用户。
Two buses, four sources, 287 wipe patterns, a chroma keyer, a downstream keyer, eight event memories, and a joystick. The whole thing was defined by a 40-page operating manual that is unusually precise about behavior: which button blinks when, which effect excludes which, how many frames an auto-fade may take (0 to 510, in steps of 2 — not 1, not 5). That is what Panasonic sold it for. It is not what we used it for. 它拥有两个总线、四个信号源、287 种划像模式、色度键控、下游键控、八个事件存储器以及一个操纵杆。整台设备由一本 40 页的操作手册定义,手册对行为的描述异常精确:哪个按钮何时闪烁、哪个特效与哪个互斥、自动淡入淡出需要多少帧(0 到 510,以 2 为步长——不是 1,也不是 5)。这就是松下销售它的初衷,但却不是我们使用它的方式。
Well into the 2000s, this thing was on stage with us in techno clubs, doing VJ duty for hours at a stretch. Two of its four channels were fed by computers running VJ software; the others carried digital video players and stills, depending on the club setup. The computers generated the material, but the MX50 is where the performance happened. 直到 2000 年代,这台设备还一直伴随我们在电子舞曲俱乐部登台,连续数小时担任 VJ 工作。它的四个通道中,有两个由运行 VJ 软件的电脑提供信号;其余通道则根据俱乐部配置连接数字视频播放器和静态图像。电脑负责生成素材,但 MX50 才是表演发生的地方。
Used that way, it stops being an editing appliance and becomes an instrument: the lever is played, not set; the joystick throws a mosaic block around the screen in time with the kick; Auto Take with the transition control at minimum is a percussion button. It is a profoundly interactive way to do visuals — interactive enough that people watching us work the panel regularly mistook us for the DJs, or for a live act. Nobody mistakes a laptop VJ for anything. 以这种方式使用时,它不再是一个编辑设备,而变成了一件乐器:推杆是用来“演奏”的,而不是用来设置的;操纵杆随着鼓点在屏幕上甩动马赛克块;当过渡控制设为最小时,自动切换(Auto Take)按钮就成了打击乐器。这是一种极具交互性的视觉表演方式——交互性强到观看我们操作面板的人经常把我们误认为是 DJ 或现场表演者。而没人会把笔记本电脑 VJ 当作什么特别的角色。
So when I set out to reproduce the unit in a browser, the question was never whether it could be made to look like an MX50. The question was whether it could be made to behave like one — block for block, blink for blink — because an instrument lives in its behavior, and twenty-year-old muscle memory is an unforgiving reviewer. The manual served as the specification of record; my hands served as the acceptance test. 因此,当我着手在浏览器中重现这台设备时,问题从来不是它能否看起来像 MX50,而是它能否表现得像 MX50——每一个模块、每一次闪烁都要一致——因为乐器的灵魂在于其行为,而二十年的肌肉记忆是一个严苛的评审。手册作为记录规范,而我的双手则作为验收测试。
The result is web-mx-50: vanilla TypeScript, WebGPU for the video path, Web Audio for the mixer, no framework, no bundler, and about 5,700 lines of source. This article describes the parts of the exercise that turned out to be interesting. The artwork is not one of them. 最终成果是 web-mx-50:纯 TypeScript 编写,视频路径使用 WebGPU,混音使用 Web Audio,没有框架,没有打包工具,代码量约 5,700 行。本文将介绍这次实践中一些有趣的环节,界面美术设计并不在其中。
The manual is the program
手册即程序
The first job was not code. It was reading the manual until it stopped being a manual and started being a data structure. The MX50’s feature set looks sprawling, but it decomposes cleanly, because the hardware itself was built from blocks wired in a fixed order: Source -> bus assignment -> Colour Correction -> Digital Effect -> Mix/Wipe -> Downstream Key -> Fade -> Program Out. 第一项工作不是写代码,而是阅读手册,直到它不再是一本手册,而变成了一个数据结构。MX50 的功能集看起来很庞大,但可以清晰地分解,因为硬件本身就是由按固定顺序连接的模块构建的:信号源 -> 总线分配 -> 色彩校正 -> 数字特效 -> 混合/划像 -> 下游键控 -> 淡入淡出 -> 程序输出。
That order is not an implementation detail; it is observable behavior. The downstream key sits after the effects, which is why a title stays sharp over a mosaic. The fade sits last, which is why you can fade the video out and leave the title standing. So the first architectural decision was to make the block order structural: the renderer is a sequence of passes in exactly that order, and no code path can reorder them. The signal graph is written down once, in one module, and everything else reads it. 这个顺序不是实现细节,而是可观察的行为。下游键控位于特效之后,这就是为什么标题在马赛克之上依然清晰的原因。淡入淡出位于最后,这就是为什么你可以淡出视频而保留标题。因此,第一个架构决策是将模块顺序结构化:渲染器是按该顺序执行的一系列通道,任何代码路径都无法重新排序。信号图在单个模块中定义一次,其他所有部分都读取它。
The second observation was that nearly all of the manual’s fussy details are state transitions, not pixels. “Press A once, the LED blinks and CHROMA is active; press again, the LED goes solid and the RGB joystick is added; press a third time, correction is off.” “If Matte is selected on a bus and you press that bus’s direct-out button, the unit outputs the blinking substitute source instead.” “Still switches off automatically when Strobe is engaged, but Trail may run on top of Still.” None of that needs a GPU to verify. It needs a state machine and someone patient enough to transcribe the manual into scenarios. 第二个观察是,手册中几乎所有繁琐的细节都是状态转换,而不是像素处理。“按一次 A,LED 闪烁且 CHROMA 激活;再按一次,LED 常亮且 RGB 操纵杆功能加入;按第三次,校正关闭。”“如果在总线上选择了 Matte,按下该总线的直接输出按钮,设备会输出闪烁的替代信号源。”“当开启 Strobe 时,Still 会自动关闭,但 Trail 可以在 Still 之上运行。”这些都不需要 GPU 来验证,它需要的是一个状态机,以及一个有足够耐心将手册转化为测试场景的人。
One value, one reducer
一个值,一个归约器
The entire panel — both buses, the matte generator, the wipe pattern selection, the DSK sliders, the fade enables, all eight event memories — is a single plain JSON-serializable value. Every change goes through one pure reducer as a typed command. No classes, no observables, no GPU handles or HTMLVideoElement references inside the state; device bindings are referenced by id and resolved outside the store. 整个面板——包括两个总线、遮罩生成器、划像模式选择、DSK 滑块、淡入淡出启用开关以及所有八个事件存储器——都是一个单一的、可序列化为 JSON 的纯值。每一次更改都通过一个纯归约器(reducer)作为类型化命令进行处理。状态中没有类、没有可观察对象(observables)、没有 GPU 句柄或 HTMLVideoElement 引用;设备绑定通过 ID 引用,并在存储之外解析。
This is not a fashionable choice so much as a forced one, and the hardware forced it twice. First, the MX50 has Event Memory: eight snapshots of the complete panel, storable and recallable at the press of a button. If your state is one plain value, Event Memory is structuredClone and an array of eight slots. If your state is scattered across component instances, Event Memory is a research project.
这与其说是一个时髦的选择,不如说是一个被迫的选择,而且硬件两次强迫我这样做。首先,MX50 拥有事件存储功能:八个完整的面板快照,按下按钮即可存储和调用。如果你的状态是一个纯值,事件存储就是 structuredClone 和一个包含八个槽位的数组。如果你的状态分散在各个组件实例中,事件存储将是一个巨大的研究课题。
Second, the unit holds its settings across power cycles (about a week on the internal backup, says the manual), which maps to schema-versioned browser storage — again trivial if the state is one value, and miserable otherwise. The same store made the control layer thin. The original unit had a GPI contact input and an RS-422 port for an edit controller. The browser equivalent is a mapping layer that normalizes keyboard, gamepad, MIDI, and a small automation API onto the same command vocabulary the front panel uses. 其次,该设备在断电后仍能保持设置(手册称内部备份可维持约一周),这对应于带有模式版本的浏览器存储——如果状态是一个值,这很简单,否则将非常痛苦。同样的存储方式使控制层变得轻量。原设备有一个 GPI 触点输入和一个用于编辑控制器的 RS-422 端口。浏览器中的对应实现是一个映射层,它将键盘、游戏手柄、MIDI 和小型自动化 API 归一化为前面板使用的相同命令词汇表。
A MIDI fader and the on-screen lever end up as the same command with a different origin, which is exactly how the hardware treated its own remote protocol. This layer is also where the instrument angle survives translation: a screen full of widgets is not something you can play with your eyes on the crowd, but a MIDI controller with a real crossfader is, and mapping one onto the MX50’s command set takes a table, not a subsystem. MIDI 推子和屏幕上的推杆最终成为具有不同来源的相同命令,这正是硬件处理其远程协议的方式。这一层也是“乐器”视角得以保留的地方:满屏的控件无法让你在注视观众的同时进行演奏,但带有真实交叉推子的 MIDI 控制器可以,而且将其映射到 MX50 的命令集只需要一个映射表,而不是一个子系统。
Cucumber, of all things
竟然用了 Cucumber
The behavioral spine of the project is 536 Gherkin scenarios (3,921 steps) in 26 .feature files, one per hardware block, each scenario ground-truthed against a section of the manual. Alongside them run 208 conventional unit tests. All of it executes headlessly against the real domain code — no GPU, no DOM, no browser. I am aware that Cucumber has a reputation, mostly earned, as a ceremony generator for enterprise projects where nobody reads the tests. 该项目的行为骨干是 536 个 Gherkin 场景(3,921 个步骤),分布在 26 个 .feature 文件中,每个硬件模块对应一个文件,每个场景都根据手册的相关部分进行了实地验证。与它们并行运行的还有 208 个常规单元测试。所有这些都在无头模式下针对真实的领域代码执行——没有 GPU,没有 DOM,没有浏览器。我知道 Cucumber 的名声(大部分是应得的),它常被视为企业项目中那种没人会去读测试的“仪式生成器”。