feat: three EQ layers, a graphic EQ, a parametric EQ and a fixed filter bank, with matrices on the last two #171

Merged
bcox merged 17 commits from eq-layers into main 2026-10-06 21:31:51 +00:00
Owner

Three EQ engines as layers, mod matrices on two of them, and a fix to the console strip's EQ. This is the EQ in a layer that the feature survey listed as a gap.

What lands

Engine Kind Wrapper Factory patch
Graphic EQ: 31 third-octave bands, ±12 dB kGraphicEq GREQ graphic-eq
Parametric EQ: 8 bands, any shape on any band kParametricEq PREQ parametric-eq
Fixed filter bank: lowpass, 12 bandpasses, highpass kFilterBank FBNK filter-bank

The three follow the Leslie in LayerKind and share one format minor, 2.11. sizeof(VoiceType) did not move, and render_golden_test and master_pin_test are unmoved.

  • Graphic EQ. The bands are parallel, not a cascade of bells: boosts are summed forward and cuts sit in a feedback loop that is solved, not delayed. Neighbouring sliders add as an analog unit's do; every slider at +12 dB measures +14.0 to +15.3 dB. It has no matrix, on purpose.
  • Parametric EQ. Each band is a high or low pass (6, 12, 18, 24, 36 or 48 dB/oct, Butterworth), a low or high shelf, or a bell; ±18 dB, Q 0.5 to 16. A boost and the same cut cancel exactly, and the signal between bands carries enough headroom that an 18 dB boost followed by a cut does not clip in between.
  • Filter bank. Fixed frequencies half an octave apart from 125 Hz, a level per band, one Q for the bandpasses. With every level down it is silence. The lowpass and highpass are summed inverted; uninverted they cancelled against their neighbours and left 12 dB holes.
  • Matrices. The parametric has three lanes per band (frequency in octaves, gain, Q in octaves); the filter bank has one per band (level). Sixteen entries each. Neither engine has an LFO of its own: they read the voice's shared sources, which include the voice and channel LFOs.
  • Reach. Bands go to 0.46 of the rate (20 kHz at 44.1 kHz), still or swept. dsp/svf.h's tangent table stops at a quarter of the rate, so still bands take their coefficients from the resolver and swept bands are designed per sample in dsp/svf_section_design.h, which mirrors a centre past the quarter about it.
  • Strip EQ fix. DesignEqBand held a band at a quarter of the rate, so a strip bell or shelf set between 12 and 20 kHz at 48 kHz sat at 12 kHz. It now uses the same mirror. Centres at or under the quarter are designed word for word as before.

Each engine has a resolver, format reader and writer, patch-diff walker, editor page, bench rows, and a spec and a ledger under docs/superpowers/. The ledgers list every injection run against the tests.

Things to check

  • Nobody has looked at the three editor pages. They build and pass the binding and ID-conflict tests, but the layouts (31 vertical faders, an eight-row table, 14 faders) were never opened in the editor.
  • The factory patches are not returns. A channel whose main output lands on one should be heard only through it. That is read from the fabric's description and was not tried in the workstation.
  • A swept parametric band is designed from tables, so adding the first matrix entry that names a band shifts that band's response by up to 0.012 dB.
  • A strip high shelf near the top of the rate has its midpoint off the asked frequency. The strip warps a shelf's frequency, not its tangent. This is unchanged, and the new test holds the strip to that model.
  • The bank is 68 patches. No factory patch uses a matrix yet.
  • The survey's Leslie lines changed too. The feature survey and docs/modular-gaps.md now describe the tree with these engines in it. The survey still called the Leslie a branch, so those lines and the layer count (sixteen) were corrected in the same pass.

Tests

On the merged tree, bazel test //..., --config=opt //... and --config=ubsan //... each pass 345 targets. --config=asan and the TSan lane were not run after the merge with main; they are CI's.

🤖 Generated with Claude Code

Three EQ engines as layers, mod matrices on two of them, and a fix to the console strip's EQ. This is the EQ in a layer that the feature survey listed as a gap. ## What lands | Engine | Kind | Wrapper | Factory patch | |---|---|---|---| | Graphic EQ: 31 third-octave bands, ±12 dB | `kGraphicEq` | `GREQ` | `graphic-eq` | | Parametric EQ: 8 bands, any shape on any band | `kParametricEq` | `PREQ` | `parametric-eq` | | Fixed filter bank: lowpass, 12 bandpasses, highpass | `kFilterBank` | `FBNK` | `filter-bank` | The three follow the Leslie in `LayerKind` and share one format minor, 2.11. `sizeof(VoiceType)` did not move, and `render_golden_test` and `master_pin_test` are unmoved. - **Graphic EQ.** The bands are parallel, not a cascade of bells: boosts are summed forward and cuts sit in a feedback loop that is solved, not delayed. Neighbouring sliders add as an analog unit's do; every slider at +12 dB measures +14.0 to +15.3 dB. It has no matrix, on purpose. - **Parametric EQ.** Each band is a high or low pass (6, 12, 18, 24, 36 or 48 dB/oct, Butterworth), a low or high shelf, or a bell; ±18 dB, Q 0.5 to 16. A boost and the same cut cancel exactly, and the signal between bands carries enough headroom that an 18 dB boost followed by a cut does not clip in between. - **Filter bank.** Fixed frequencies half an octave apart from 125 Hz, a level per band, one Q for the bandpasses. With every level down it is silence. The lowpass and highpass are summed inverted; uninverted they cancelled against their neighbours and left 12 dB holes. - **Matrices.** The parametric has three lanes per band (frequency in octaves, gain, Q in octaves); the filter bank has one per band (level). Sixteen entries each. Neither engine has an LFO of its own: they read the voice's shared sources, which include the voice and channel LFOs. - **Reach.** Bands go to 0.46 of the rate (20 kHz at 44.1 kHz), still or swept. `dsp/svf.h`'s tangent table stops at a quarter of the rate, so still bands take their coefficients from the resolver and swept bands are designed per sample in `dsp/svf_section_design.h`, which mirrors a centre past the quarter about it. - **Strip EQ fix.** `DesignEqBand` held a band at a quarter of the rate, so a strip bell or shelf set between 12 and 20 kHz at 48 kHz sat at 12 kHz. It now uses the same mirror. Centres at or under the quarter are designed word for word as before. Each engine has a resolver, format reader and writer, patch-diff walker, editor page, bench rows, and a spec and a ledger under `docs/superpowers/`. The ledgers list every injection run against the tests. ## Things to check - **Nobody has looked at the three editor pages.** They build and pass the binding and ID-conflict tests, but the layouts (31 vertical faders, an eight-row table, 14 faders) were never opened in the editor. - **The factory patches are not returns.** A channel whose main output lands on one should be heard only through it. That is read from the fabric's description and was not tried in the workstation. - **A swept parametric band is designed from tables,** so adding the first matrix entry that names a band shifts that band's response by up to 0.012 dB. - **A strip high shelf near the top of the rate has its midpoint off the asked frequency.** The strip warps a shelf's frequency, not its tangent. This is unchanged, and the new test holds the strip to that model. - **The bank is 68 patches.** No factory patch uses a matrix yet. - **The survey's Leslie lines changed too.** The feature survey and `docs/modular-gaps.md` now describe the tree with these engines in it. The survey still called the Leslie a branch, so those lines and the layer count (sixteen) were corrected in the same pass. ## Tests On the merged tree, `bazel test //...`, `--config=opt //...` and `--config=ubsan //...` each pass 345 targets. `--config=asan` and the TSan lane were not run after the merge with `main`; they are CI's. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bcox added 17 commits 2026-10-06 21:29:43 +00:00
A boosted band adds its bandpass and a cut band sits in a feedback path
closed without a delay, so sliders add as an analog unit's do and a cut
inverts the boost of the same size. Every coefficient is resolved; the
engine has no LFO and no matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kGraphicEq goes last before kLayerKindCount and GraphicEqLayerType last in
VoiceType's list, so no existing kind's index moves. sizeof(VoiceType) is
measured unchanged; the layer is pinned at 1008 bytes and its patch at 256.

resolve/graphic_eq.h makes every word the engine multiplies by. Each
band's damping puts its lower half-power point a sixth of an octave under
its center at the running rate, so a band near the top of the rate keeps
its lower side where a fixed damping left the 20 kHz band at 48 kHz a
fraction of its width. a3 is held at one unit or more: rounded to zero, at
20 Hz and 96 kHz, it left a band's second state holding a small value
forever and its voice never freeing.

format/graphic_eq.h reads and writes GREQ at minor 2.10: the tap in a
leaf's header and the sliders as its run. No value refuses.
patchdiff/walk_graphic_eq.h names the tap and each slider, and the editor
gains a page of thirty-one faders, kGraphicEqShape, and a vertical slider
wrapper that reports its binding.

graphic-eq joins the bank, a channel-scope EQ reading the external input
with its sliders in a scoop, and //plugin:instrument_bench gains its rows.

Sites outside the -Werror=switch net: plugin/state_census.cc's engine
names, program_resolve_test's door lists, patch_test's bare-layer fixture,
patches_test's ReadsOnlyChannelInputs, the coverage test's engine list and
shard count, the bank's count and the size pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A band is a high pass or a low pass of 6 to 48 dB an octave, a low or a
high shelf, or a bell, in series, as up to four resolved second-order
sections and a first-order one. Every shape is one kind of section and
three output weights, which resolve/parametric_eq.h makes. The signal
between sections carries three more integer bits than a Sample, so a full
boost and a later cut of it do not clip between them.

dsp/svf_section.h takes the recursion the graphic EQ's band ran, with both
integrator outputs, a wider input and a one-pole beside it, and the
graphic EQ runs on it; resolve/svf_section.h makes a section's words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kParametricEq goes last before kLayerKindCount and ParametricEqLayerType
last in VoiceType's list. sizeof(VoiceType) is measured unchanged; the
layer is pinned at 1168 bytes and its patch at 328.

format/parametric_eq.h reads and writes PREQ at minor 2.11: the tap in a
leaf's header and the eight bands as its run. No value refuses: a shape
the reader does not know is a band switched off, a slope lands on the
nearest listed, and the rest are held inside their ranges.
patchdiff/walk_parametric_eq.h names the tap and each band's five fields,
and the editor gains a page of eight rows, each drawing what its shape
reads, and kParametricEqShape.

parametric-eq joins the bank with one of each shape and a bell standing on
its high shelf, and //plugin:instrument_bench gains its rows.

The bank is sixty-five patches and a project has sixty-four channels, so
format/project_test.cc's busy project takes the bank's last sixty-four
where it took all of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fourteen fixed bands half an octave apart, each a resolved section with a
level, summed: every level down is silence, where the graphic EQ at zero
is its input. The lowpass and the highpass are summed inverted. As they
stood each canceled against the bandpass beside it, and every level up at
the default Q measured holes 12 dB deep; inverted it is 1.2 to 6.5 dB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kFilterBank goes last before kLayerKindCount and FilterBankLayerType last
in VoiceType's list. sizeof(VoiceType) is measured unchanged; the layer is
pinned at 464 bytes and its patch at 128.

format/filter_bank.h reads and writes FBNK at minor 2.12: the tap and the
bandpasses' Q in a leaf's header and the fourteen levels as its run. No
value refuses. patchdiff/walk_filter_bank.h names each, and the editor
gains a page of fourteen faders and kFilterBankShape.

filter-bank joins the bank, two peaks left standing at 710 Hz and 1.4 kHz,
and //plugin:instrument_bench gains its rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DesignEqBand held a band's center at kSvfFrequencyLimit, a quarter of the
rate, because that is where SvfTangent's table stops: a bell or a shelf set
anywhere from 12 to 20 kHz at 48 kHz sat at 12 kHz. A band past the quarter
is now designed at its mirror image about it, where the tangent is the
reciprocal and the table reaches: the mirror's a1 and a3 exchanged are the
band's own. The clamp is 0.46 of the rate, which keeps 20 kHz at 44.1 kHz.
A center at or under the quarter is designed word for word as it was.

app/eq_curve.h draws the same clamp, so the thumbnail still draws a band
where it sounds.

A shelf's warp is still taken on its frequency and not on its tangent, as
it was; near the top of the rate that puts its midpoint off the asked
frequency, and the new test holds the shelf against that model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parametric EQ's matrix has three lanes a band: frequency in octaves,
gain against the band's own as a fraction of the range, and Q in octaves.
The filter bank's has a lane a band, its level. Neither engine has an LFO
of its own; both read the voice's shared sources, the voice and channel
LFOs among them. Sixteen entries each. The graphic EQ has none.

A band the parametric's matrix moves is designed again every sample, in
fixed point, by dsp/svf_section_design.h: the tangent from SvfTangent,
taken at the center's mirror image past a quarter of the rate so a swept
band still reaches 0.46 of it; the gain, the damping and a shelf's warp as
exponentials of what the resolver holds as logarithms; and the reciprocal
of 1 + g * (g + k) normalized over a damping up to thirty-two, which a
wide bell at full cut needs. A band nothing moves is the resolved one, bit
for bit. A moved band at rest measures within 0.012 dB of the still one.

A moved band of the filter bank is summed from its section at a level of
one and the level read that sample, and runs whatever its level.

The matrix is written after the bands in PREQ and FBNK, which no file
outside this branch holds yet, so 2.11's and 2.12's entries say so and the
minor does not move. sizeof(VoiceType) is still measured unchanged; the
parametric's layer is 1416 bytes and its patch 720, the bank's 632 and 520.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An EQ in a layer is no longer a gap: its row is out of the gap table, the
two feature rows carry no ticket, and each instrument's EQ is under what
fits or is reachable, with what does not match said there. Neither
document names a branch or the commit it was first read at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Leslie landed on main as the thirteenth layer kind and format 2.10
before this branch was cut, and the branch was cut from a local main that
did not have it. The Leslie keeps its kind and its minor; the graphic EQ,
the parametric EQ and the filter bank are the three kinds after it, and
their three wrappers are one minor, 2.11, since they land together.

The bank is sixty-eight patches. format/project_test.cc's busy project is
main's, which holds the bank in two projects. The feature survey's layer
count is sixteen, and its lines that called the Leslie a branch say what
the tree has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: the lanes run on the merged tree, and the two left to CI
Some checks failed
push / small (push) Successful in 32m49s
ci / opt (pull_request) Failing after 48m13s
ci / asan (pull_request) Failing after 1h20m27s
ci / ubsan (pull_request) Failing after 57m35s
ci / tsan (pull_request) Successful in 20m0s
ci / coverage (pull_request) Failing after 58m19s
15c95d6109
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bcox merged commit 06490a7b75 into main 2026-10-06 21:31:51 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
bcox/tymbal!171
No description provided.