<!-- 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 through useChannelEq / useChannelGate / useChannelComp, which emit the eq.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 chain StagePanel — chainProcessors / setProcessorPlugin / setProcessorEnabled / reorder verbs. 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

  1. 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.
  2. 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.
  3. An empty insert chain renders nothing on the native tabs — never a "no … stage" message (StagePanel empty state is intentionally silent).
  4. Defaults: gate on: false, comp on: false, EQ flat & on: true (a flat EQ is inert). See @freemixer/catalog's default*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.