<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
Native built-in DSP vs mod_host plugin inserts
Read this before touching the EQ / Gate / Compressor UI or the processor chain. The two have been repeatedly confused, producing regressions ("this channel has no EQ stage", the EQ curve routed through a plugin, an LV2 "add" offer on the EQ tab). They are two independent mechanisms. Do not conflate them.
1. Native built-in DSP — the channel's OWN processing (always present)
Every processing channel (input strips; buses that carry processing) has a
built-in EQ, gate and compressor that run inside the native C engine
(packages/pipewire-native/src/mixer.c — a biquad cascade for the EQ, one
omx_dynamics atom for gate/comp). See ADR 0002 (structural DSP is
PipeWire-native), ADR 0006 (the channel strip is an ordered processor list),
ADR 0009 (C for the hot path).
- Always there. There is no "EQ stage" to add and no way to be without one.
A fresh channel has a flat EQ (shaping nothing) — that is honest, not a
placeholder. You disable a block with its on-switch (
EqState.on,GateState.on,CompState.on), you never "remove the stage". - State → DSP path. The model lives in
@freemixer/core/@freemixer/catalog(EqState,GateState,CompState,defaultEqState(),defaultGateState(),defaultCompState()). The web-ui edits it throughuseChannelEq/useChannelGate/useChannelComp, which emit theeq.set/ gate / comp verbs. The server threads those to the native strip (gateStateToNativeDyn, the EQ coeff push,native.setStripEq/setStripDyn), where they run as biquads / the dynamics atom. Word-atomic params (JS writes relaxed, the RT thread reads). - The UI. The EQ / Gate / Compressor tabs draw the native state directly:
EqCurve.vue(EQ),GateCurve.vue(gate),CompCurve.vue(comp). These curves ARE the native processor. The EQ RTA pre/post taps read the signal around the native EQ cascade.
2. mod_host / LV2 plugin inserts — OPTIONAL, separate, ad-hoc
Free LV2 plugins hosted via mod-host, inserted into a channel's processor chain as ad-hoc effects (ADR 0002: inserts = mod-host). This is a different thing from the built-in DSP above.
- Managed by the Plugins tab (the
StripInsertRack/ insert rack) and the chainStagePanel—chainProcessors/setProcessorPlugin/setProcessorEnabled/reorderverbs.useChain/useInserts. - A chain having "no eq stage" means no plugin EQ insert — it says nothing about the channel's native EQ, which is always present (§1). Never surface a chain-stage emptiness as "this channel has no EQ".
- LV2 EQs/dynamics belong ONLY here (the Plugins tab), never as the path to a channel's own EQ/gate/comp.
Rules for anyone (human or agent) working here
- The EQ / Gate / Comp tabs edit the NATIVE built-in DSP (§1), always, for
every processing channel. Bind the curve to
eqFor()/gateFor()/compFor()and emit the native verbs. Do not gate the curve on chain-stage presence, and do not route the built-in EQ/gate/comp through mod_host / a plugin. - mod_host inserts live in the Plugins tab (§2). Do not put an "add plugin" / LV2 affordance on the EQ/Gate/Comp tabs — those tabs are the native DSP.
- An empty insert chain renders nothing on the native tabs — never a
"no … stage" message (
StagePanelempty state is intentionally silent). - Defaults: gate
on: false, compon: false, EQ flat &on: true(a flat EQ is inert). See@freemixer/catalog'sdefault*State().
If a change makes you write "add EQ", "no EQ stage", or route EQ through a plugin, stop — you're fighting mod_host. Re-read §1.