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,MixerTopologywith 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 aMixerReceiver(because desks stream state after subscribe). The software audio engine implements the sameMixerAdaptercontract, so it is just another sink. - The
MixerEngineholds state for one bound device and is itsMixerReceiver, 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 asoftwareadapter name selects the built-in mix engine (loadAdapterinpackages/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-inMockAdapteris the default.