0004 — One canonical device-neutral model with per-device adapters

Status: Accepted (built) Date: 2026-06-29

Context

Every digital console speaks its own remote-control protocol, with its own addressing, its own fader taper, and its own quirks (the X32 inverts mute; the Midas has no single pan node). A mixer surface and a mix engine written against any one of those protocols is welded to that desk forever. We want the surface, the engine, and the I/O to be reusable across consoles and across a from-scratch software mixer — and to be able to route one desk's surface to drive another.

Decision

Put one canonical, device-neutral model in the middle (@freemixer/core) and make adapters the only code that knows a protocol.

  • The model names things by role: ChannelId = { kind, index }, ChannelStrip, FaderLevel, MixerTopology with a mandatory capability map. Values are normalised (a fader is an opaque 0..1 position with dB alongside; the device-raw taper is private to the adapter — see 0011).
  • An adapter implements MixerAdapter (writes are promises) and pushes reads to a MixerReceiver (because desks stream state after subscribe). The software audio engine implements the same MixerAdapter contract, so it is just another sink.
  • The MixerEngine holds state for one bound device and is its MixerReceiver, so device-pushed and surface-pushed changes share one path and one fan-out.

Consequences

  • Add a console = add an adapter. Nothing in the surface, server, or engine changes. Three console adapters exist (Midas mature, X32 substantial, Roland a documented stub); since this ADR a fourth, adapter-xtouch, attaches an X-Touch control surface through the same contract, and a software adapter name selects the built-in mix engine (loadAdapter in packages/server/src/adapter-loader.ts).
  • The valuable, reusable assets are the neutral model and the adapters — not any one desk's protocol.
  • A desk can be both a sink (we control it) and a source (its surface drives others) with no special case: subscribe one engine's events, call another engine's methods.
  • The model must be a superset of what real desks expose, and capabilities must be honest, so a surface greys out features a given device lacks rather than erroring.
  • Adapter packages are loaded lazily (guarded dynamic import()), so the server builds and tests with no adapter packages present; the built-in MockAdapter is the default.