A procedural light show engine for a row of N simple on/off lamps (relay- or MOSFET-switched LEDs, an old industrial stack light, whatever you've got), running on an ESP32. Instead of picking effects from a flat random list, it arranges patterns using real musical structure — beats, bars, phrases, and sections in an A-A-B-A layout — so the result reads as composed rather than random, without needing any audio input.
No microphone, no audio analysis, no sync to actual playing music. The engine generates its own dramaturgy purely from timing rules and weighted randomness.
Most DIY blink projects pick a new pattern at random on every step. The result almost always feels arbitrary, not "composed" — because there's no musical structure underneath it: no groove that holds for a while, no highlight that stands out from the baseline, no build-up.
This engine instead reproduces the fixed time hierarchy that pop and techno arrangements use:
Beat → Bar (4 beats) → Phrase (4 bars) → Section (2/4/8 phrases)
- Every section has a base motif A, a contrasting motif B (A-A-B-A), and ends with a fill.
- Every pattern ("family") belongs to one of three impact tiers (base / noticeable / highlight) — a section's highlight must always outrank the running motif, otherwise it doesn't land as one.
- Energy (1–4) rises and falls like a staircase, not a smooth wave, and only changes at section boundaries.
- Right before an energy increase there's a short blackout — the moment that makes the next hit land.
- Occasionally a multi-bar riser: the same motif gets subdivided more finely bar by bar (quarter → eighth → sixteenth notes) and resolves into a strobe — the classic techno build-up.
More background and the reasoning behind these choices: docs/CONCEPT.md
- Manual mode: press the button, one lamp lights up; short presses step through the row (bouncing back and forth)
- Show mode: double-tap starts the generative sequence. Short press = shuffle (reroll the current section immediately if you don't like it)
- Off: long-press (~1.8s) from anywhere
13 pattern families, 7 strobe variants, 5 rhythmic subdivisions (down to sixteenth notes) — hundreds of thousands of valid section configurations. Everything is generated at runtime; no frame is hardcoded.
The engine works with any lamp count (2 or more). Position-based patterns
(Sweep, Bounce, Comet, ...) use whatever order you list your pins in — see
config.h. That order should match the physical arrangement of your
lamps, not wiring convenience, since it's what "next to" means for the
engine.
Built and tested on: ESP32 DevKit + low-side MOSFET switch modules + a repurposed 3-zone industrial stack light. None of that is required — any microcontroller with enough digital outputs and any switchable lamps work, as long as you can drive each one from a simple HIGH/LOW pin.
Full build guide for the stack-light version: docs/WIRING.md
- Open
firmware/structured_led_sequencer/structured_led_sequencer.inoin the Arduino IDE (config.hneeds to sit in the same folder — the IDE will show both as tabs automatically) - Board: ESP32 Dev Module (may show as "ESP32 Dev Board" on newer core versions)
- Edit
config.h— at minimum, setNUM_LAMPS,LAMP_PINS[]in your physical order, andBUTTON_PIN - Upload
To preview patterns and tune weightings without hardware:
python3 tools/simulator.py --lamps 3 --duration 60
python3 tools/simulator.py --lamps 8 --duration 60 # try a different lamp countPrints an ASCII preview of the generated show straight to the terminal —
useful for iterating on config.h values without reflashing every time.
The pattern logic lives in buildFrames() in the main .ino. Everything
above it (timing, section structure, impact rules) is independent of lamp
count and pattern content, so adding a new family only means adding a case
to that one function and, if it should be usable as a base motif or fill,
adding it to MOTIF_FAMILIES or FILL_FAMILIES.
MIT — see LICENSE. Do whatever you want with it.