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
mixernode; faders arelinearnodes; 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-linkis 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.