0002 — PipeWire-native summing; mod-host for inserts only

Status: Accepted (both built) — see the 2026-07-29 note at the foot: the engine the live rig runs is not the one described here Date: 2026-06-29

Context

A real mixer needs two distinct kinds of DSP:

  • Structural DSP — summing buses, faders, pan, channel/output EQ, loudspeaker crossovers, delay alignment. This is the skeleton of the console.
  • User inserts — the LV2 plugins an operator drops onto a strip (a compressor, a reverb, a saturator).

It is tempting to run everything through one plugin host (mod-host). But that means a process per node and N-way port summing wired by hand, and it runs into a concrete limit: on this platform mod-host's JACK-style connect truncates long PipeWire port names (error -205), which is exactly the situation at the summing stage. PipeWire also will not sum on a port — a port takes a single link, so every convergence point needs an explicit mixer node anyway.

PipeWire ships builtin filter-chain nodes (MIT-licensed): mixer (up to 8 inputs, each with a live gain — that is a summing bus with per-tap send gain), linear (faders), bq_* biquads (EQ), convolver + biquads (crossovers), and delay (alignment). Props changes are smoothed by PipeWire, which gives click-free fader/mute ramps for free (0006 notes the alternative dB-space ramp).

Decision

Draw a clean two-layer line:

  • Structural DSP is PipeWire-native. Each bus is one filter-chain mixer node; faders are linear nodes; EQ/crossover/delay are biquad/convolver/delay nodes. This is where summing, sends, DCAs (as gain contributions), matrix, and loudspeaker management live.
  • mod-host hosts user inserts only. The per-strip ordered LV2 insert chain runs in mod-host; nothing structural does.
  • pw-link is used only for source patching — wiring REAC/USB I/O into bus nodes, and bus outputs to physical outs.

Built: the mod-host insert layer (ChainManager, AudioEngineAdapter), the pw-link router, and the PipeWire filter-chain summing layer — bus-node.ts's BusControl spawns a filter-chain mixer-tree bus node (a tree of builtin mixer nodes: ceil(N/8) group mixers summed by one sum mixer, N input slots each with a live g<grp>:Gain k send gain), and SoftwareMixer builds the structural graph on top: aux/group buses with per-channel sends, DCAs as control-domain gain coupling (0008), matrix outputs (a bus sourced from buses/mains), and output insert chains on bus/matrix/main outputs (reusing ChainManager). The main mix is itself one such bus. Verified on real PipeWire: an N=40 bus loads with 40 playback_AUX0…39 input ports + a routable stereo <bus>_out:output_FL/FR, and per-tap gains set via pw-cli set-param … Props.

Consequences

  • Collapses process sprawl: one filter-chain node per bus instead of many loopback processes plus hand-wired summing.
  • Sidesteps the mod-host long-port-name truncation for the summing stage, because the structural graph never goes through mod-host's connect.
  • Gives declick for free via PipeWire Props smoothing — a hard mute or fader snap clicks; ramping does not.
  • Resolves the central "native nodes vs plugin host" question with a rule, not a case-by-case judgement: structural = PipeWire-native, inserts = mod-host.
  • Outputs host insert chains exactly like channels, so separate subs + tops, aux-fed subs, and per-channel HPF/LPF are configuration, not new code — openmixer subsumes the separate loudspeaker processor (DriveRack/Lake) for the rig.
  • It is the largest remaining build item in the software engine.

Note — 2026-07-29: the shipping engine is a third thing this ADR does not name

The decision stands: two distinct kinds of DSP, structural summing separate from plugin inserts, mod-host for inserts only. The mod-host half is exactly what runs. What has changed is the realisation of the structural half.

This ADR specifies structural DSP as PipeWire's built-in filter-chain nodes — one mixer node per bus, linear nodes for faders, biquad/convolver/delay nodes for EQ and delay. That path was built, and was for a while the fallback. It is no longer even that: OPENMIXER_NATIVE_MIXER — the switch that chose between the two topologies — was deleted in 2026-07, and the filter-chain realisation of the channel EQ and gate (packages/audio-engine/src/eq-node.ts, gate-node.ts, both of which cited this ADR as their authority) went with it, having had no caller. They were rejected on measurement, not on taste: the filter-chain path introduced latency that a live console cannot afford. bus-node.ts survives — the software mixer still uses it for bus plumbing. The live rig runs openmixer's own C pw_filter node instead: one node per stagebox group, with hand-written DSP kernels for summing, fader, pan, polarity, the EQ bank, gate/comp, delay and reverb — packages/pipewire-native/src/mixer.c, mixer_rt.c, mix_dsp.h. ADR 0017 already recorded this in passing; this note makes it explicit here, because a reader who follows this ADR looking for bq_peaking nodes will never find the code that processes the show.

Why the change happened is worth keeping: the filter-chain-per-bus shape needed one PipeWire node per bus and a pw-loopback per strip, and ~40 of those is the fork pile the module doc comment in packages/pipewire-native/src/mixer.c calls "the fork-killer". The decision — structural DSP below the node boundary, inserts above it — is what survived; the choice of whose C it is did not. See native DSP vs mod-host inserts.

The consequence line "It is the largest remaining build item in the software engine" was true when written and is no longer.