ENGINE=edrums · src/engine/edrums/edrums_engine.{h,cpp} · class EdrumsEngine

A drum machine whose hits are placed by Euclidean rhythms and voiced by synthesized drums (no samples). It is the first engine to sequence off the shared platform transport — it subscribes to the clock's ticks rather than just reading tempo. Status: working — four drums (two per deck), confirmed on hardware and covered by the headless slot test (make -C host test-edrums); this doc is the as-built player reference.

Four drums, two editable at a time. Each of the two decks holds two drums (a "slot" pair), and all four sequence and sound at once. The platform stays two-deck: a deck's knobs and ring address whichever of its two drums is focused, and the Rev pad swaps the focus. The other drum keeps playing in the background. So you build a four-piece kit but only ever edit two voices at a moment — see Four drums.

Implementation, the file map, the hardware-test feedback/resolutions, and the roadmap live in docs/dev/edrums-impl.md.


Concept

Four drums: two per deck

The two physical decks each carry two drums in a slot pair (_track[deck][slot], slot 0/1). All four always sequence (their own length, density, division) and always sound — focus never gates audio.

Defaults assign a four-piece kit — deck A = Kick (slot 0) + Tom (slot 1), deck B = Snare (slot 0) + Hat (slot 1) — but the kit boots silent: every drum is seeded with zero onsets (POS=0). The player builds the groove up from nothing by raising POS on each drum (and Rev-swapping each deck to reach the slot-1 pair). Because only the onset count is withheld — model, pitch, decay, and length are all pre-seeded — a drum is in tune and correctly voiced the instant its first onset appears.

Audio: the synthesized voice

Each drum has one Voice (a small abstraction so sample playback could replace it later behind the same trigger() / process() seam) — four in all. The voice is:

Drum models (Alt + PITCH, live)

The voice has 5 models, selected live by holding Alt and turning PITCH (no commit step — the change applies on the next hit; release Alt and PITCH is pitch again). Each model sets the full voice character — body/noise balance, pitch-sweep amount and time, body frequency offset, body and noise decay, noise centre/Q and band-vs-high-pass, the second partial, saturation, and attack click. PITCH still drives body frequency + noise colour within the model, and the grit/flux macros (see Live sound macros) offset the model's drive/sweep/brightness/balance/decay. Per deck, independently. The model baselines all live in the single kModels table in edrums_engine.cpp — the one surface to tune the kit by ear.

#ModelCharacter
0Kickdeep sine body, long (~55 ms) pitch drop, beater click + drive, longer decay
1Snaretwo-partial body + bright band-pass noise that rings past the body
2Clapnoisy, multi-burst (3 quick re-triggers ~11 ms apart), tail rings
3Closed hathigh-passed noise (metallic), very short
4Tomtwo-partial pitched body, mild drop, mallet click

Defaults, per drum: deck A = Kick (slot 0) + Tom (slot 1); deck B = Snare (slot 0) + Hat (slot 1). Each slot is fully voiced at init (length, pitch, decay, model) but seeded with zero onsets, so the kit boots silent and the player builds it up with POS — no drum waits on a knob move to be correctly voiced once raised.

Why a knob and not a load-pad: synth model switching is instantaneous and free, so it applies live > — the select-then-load ceremony is only needed for expensive operations (sample loading), and is > reserved for that (see roadmap). The same Alt+PITCH control will select sample slots later, the only > difference being a deferred/debounced load instead of an instant apply.

The live model select rides a new platform seam: CapAux + ParamId::Aux. The platform routes Alt+PITCH to Aux only for engines that advertise CapAux; granular (which uses Alt+PITCH for pitch-quantize) is unaffected.

The sequencer triggers run in the transport-tick callback (the audio-block context); trigger() only re-arms envelope state, so it is allocation/lock free.

Output routing (the hardware routing switch, control 26)

The 3-position routing switch picks how the four voices reach the two outputs (via set_config(Route), mirroring granular's int mapping so the panel's L/C/R read the same). A pre-limiter trim then a final SoftLimit keep the summed bus in range — four simultaneous voices sum hotter than the original two, so the trim avoids constant limiting:

SwitchRouteEdrums behavior
CentreStereoall four voices summed to both outputs (a mono drum bus) — the conventional default
LeftDoubleMonodeck A's two drums → left, deck B's two → right (the split)
RightGenerativeStereoeach hit randomly panned across the stereo field

route() reports the mode for the panel's L/C/R LED.

Sequencer

Each drum runs a Euclidean step sequencer: POS sets how many onsets are spread across the pattern, SIZE sets its length, and ENV rotates it. (Implementation detail in docs/dev/edrums-impl.md.)

Control map (as built)

Edrums control surface
Edrums control surface - open full size download PDF

_Generated from docs/diagrams/controls/edrums.json via make diagrams._

The platform gives each deck 7 knobs (see README); edrums uses all of them:

KnobParamId (routing)Function
POSPosdensity — Euclidean onsets, as a fraction of the current length
SIZESizepattern length (2..16 steps)
ENVEnvrotation (pattern shift)
PITCHSpeeddrum pitch — sine body (~30..480 Hz) and the noise band centre
SOSMixper-drum gain (level)
MOD_AMTModAmpprobability an onset fires (0..100%, default 100% = full clockwise)
MODFREQset_mod_speedclock division — 1/16, 1/8, 1/4 (per deck)
Alt + PITCHAuxdrum model select (live; see below)
grit + SOSGritMixdecay (amp-env time, ~30 ms..1.2 s)
grit + PITCHGritIntensitydrive (saturation) — see Live sound macros
flux + PITCHFluxIntensitypitch-sweep amount (±2 oct on the model)
flux + SOSFluxMixbody↔noise balance
flux + POSFluxFbnoise brightness (filter cutoff, ±2 oct)
Play padon_play_padstop / start the focused drum (stays grid-locked; tails ring out)
Rev padon_play_padswap the deck's focused drum
Alt + Playon_record_padmute the focused drum (it keeps stepping, audio silenced)
hold Alt + Seq (~1.5 s)clear_sequencereset that deck's two drums to factory defaults — see Presets

The seven plain knobs act on the deck's focused drum; the Rev pad swaps focus to the other drum (see Four drums). Density is stored as a fraction and re-derived over the active length, so changing SIZE keeps the relative fill. POS and MOD_AMT are engine-seeded (the platform reads param(Pos) / param(ModAmp) for their initial values), so init pre-seeds them: density 0 on every drum (the silent boot — POS seeds to minimum, so turning it up adds hits) and probability 100% (so the knob defaults to "every onset fires" with full clockwise = 100%). The display draws the focused drum's pattern over the active length (the length fills the 32-LED ring; onset lit, playhead bright) in that drum's colour, with a play-LED flash on each hit; the Rev LED carries the backgrounded drum's colour. Colours are per (deck, slot): A slot 0 amber / slot 1 magenta, B slot 0 cyan / slot 1 violet.

Live sound macros

The plain knobs above run the sequencer + pitch/gain; the per-drum sound shaping lives on the grit and flux pad modifiers (hold the pad, turn PITCH/SOS/POS), so the kit is tweakable in performance rather than fixed per model. Each macro is a bipolar offset on the model's baseline — at noon the drum is exactly as the model voiced it, so a fresh kit is unchanged until you move a knob — and like every other knob it addresses the focused drum (repointing on a Rev swap):

GestureMacroRange
grit + SOSdecay (amp-env time)~30 ms .. 1.2 s
grit + PITCHdrive (saturation)clean .. crunch
flux + PITCHpitch-sweep amountx the model's sweep, ±2 oct
flux + SOSbody↔noise balance±0.5 around the model
flux + POSnoise brightness (filter cutoff)±2 oct

With PITCH (pitch), SOS (gain) and Alt+PITCH (model), that is eight live axes per drum — and all four drums voice independently. The macro channels route through the platform's existing grit/flux held-modifier layer (GritIntensity/GritMix/FluxIntensity/FluxMix/FluxFb), which is pickup-seeded from param(), so no platform change was needed. (Decay sits on grit+SOS rather than Alt+SOS because the Feedback channel Alt+SOS routes to is not pickup-seeded.) Not yet exposed: an independent body-vs-noise (snap) decay — the voice has separate envelopes for it, but it needs a sixth modifier channel (a small platform addition), so the one decay currently scales both.

Transport: stop/start and mute

Two per-drum performance controls on the Play pad (they address the focused drum, like the knobs; Rev to reach the deck's other drum), neither persisted (every drum boots running + unmuted):

The Play LED tracks the focused drum's transport state: off when stopped, a dim hit-flash when running-but-muted, a full hit-flash when running and sounding.

Presets (QSPI auto-persist)

The whole kit auto-saves to flash, so your tweaks (all four drums plus the route and per-deck focus) survive a power cycle with no save gesture, and reload at boot. To reset to the factory kit, hold Alt + Seq on a deck for ~1.5 s — per deck: hold on deck A to reset kick/tom, on deck B for snare/hat, or both for a full kit reset. (Storage mechanics in docs/dev/edrums-impl.md.)

Polymeter

Per-drum length and division make all four drums cycle independently (a drum's cycle = length × division ticks). On the external clock a transport grid reset (e.reset) realigns every drum (step phase + pattern position) to the bar; on the internal clock there is no reset, so the drums free-run and realign naturally at their least-common-multiple. Tempo/clock-source are the shared transport's (TAP / tap-hold+MODFREQ_A / Alt+TAP — see README).