Drive the Allen & Heath Xone:K2's 34 LEDs from preprogrammed patterns, scripted YAML scenes, MIDI clock, or live audio. Includes a calibration workflow, six audio-reactive effects, scrolling-text mode, and live control from the K2's bottom-row knobs.
- Patterns. Built-in
chaser,sweep,pulse,breathing,static, plus YAML keyframe scenes for arbitrary custom animations. - Audio-reactive. Six effects:
rms,spectrum,bands,vu(8-band sideways meter),beat,drop(particle rain). All driven by a live mic / line-in input. - Live control. Press a bottom-row knob to cycle effects; turn the left knob for intensity; turn the right for sensitivity. Press both knobs to toggle a scrolling-text mode (cycle fonts with the right knob, set scroll speed with the left).
- MIDI clock. Patterns can sync to incoming 24 ppqn MIDI clock with a 500 ms stall detector, or run on a fixed BPM.
- Calibration. Two interactive tools (
calibrate,map-leds) recover the K2's input/output note mapping for any unit in a few minutes.
- Connection: USB direct to a Mac (tested bus-powered on M-series).
- Latch position: The K2's latch switch sets the MIDI channel. This
project assumes channel 2 (0-indexed), the most common default. Other
layers are exposed via
--layer B|C|D. - Audio input: Any system input device. Pass
--input-device NAMEor--input-device 1to pick by name or index, or rely on the OS default.
python3 -m venv .venv
.venv/bin/pip install -e ".[test]"Tested on Python 3.11+ (developed against 3.14.5). Dependencies: mido,
python-rtmidi, sounddevice, numpy, pyyaml, click, rich.
# Discovery
.venv/bin/midi-lights list-ports # MIDI in/out — should show "XONE:K2"
.venv/bin/midi-lights list-audio # audio input devices
# Sanity check the device map
.venv/bin/midi-lights test --color red --duration 5
.venv/bin/midi-lights test --color cycle --duration 15
# Recovery if any LED gets stuck
.venv/bin/midi-lights blackout
# Patterns
.venv/bin/midi-lights pattern --name chaser --bpm 120
.venv/bin/midi-lights pattern --name breathing
.venv/bin/midi-lights pattern --file config/patterns/chaser.yaml
# Audio-reactive
.venv/bin/midi-lights audio # starts on `rms`
.venv/bin/midi-lights audio --effect vu # 8-band sideways VU
.venv/bin/midi-lights audio --effect drop # particle rain
# MIDI clock sync (DAW or hardware sending 24 ppqn over IAC)
.venv/bin/midi-lights pattern --name chaser --clock midi --clock-port "IAC Bus 1"- Press either bottom-row knob alone → cycle to the next effect.
- Press both bottom-row knobs together → toggle scrolling-text mode.
- Turn the left knob → adjust intensity (also scroll speed in text mode).
- Turn the right knob → adjust sensitivity (cycles font in text mode).
Buttons and lights are addressed by row × col, both 0-indexed, written
as AxB. So 0x0 is the top-left indicator light, 4x0 is the top-left
button of the lower block, 8x3 is the bottom-right under-fader button.
See docs/k2-layout.md for the full diagram.
name: red-sweep
bpm: 120
loop: true
length_beats: 4
keyframes:
- at: 0.0
cells:
- [1x0, red]
- [1x1, red]
- at: 1.0
cells:
- [1x0, off]
- [1x2, amber]
- at: 2.0
cells: clear_matrix # special: turn off every matrix cellCells use the AxB naming. Colors are red, amber, green, or off.
The special directive clear_matrix turns off every cell in rows 1–7;
clear_all clears every controllable LED.
The shipped device map (src/midi_lights/device/xone_k2.py) reflects the
unit this project was developed against. If your K2 is in a different
latch position or operating mode, recover the map with these two tools.
# 1. Capture input notes by pressing every button in row-major order.
# Press SPACE when finished. The tool prints (row, col) → input_note.
.venv/bin/midi-lights calibrate
# 2. Discover LED notes by sweeping every MIDI note 0-127 on channel 2.
# When a button lights, press it. SPACE to skip notes that light
# nothing or only light row-0 indicators.
.venv/bin/midi-lights map-ledsPaste the resulting tables into xone_k2.py's INPUT_NOTES and
_LED_RGB dicts. Also useful while exploring an unknown unit:
.venv/bin/midi-lights sniff # log every incoming MIDI message
.venv/bin/midi-lights note --channel 2 --note 36 --duration 3 # single LED probe
.venv/bin/midi-lights scan --channels 2 --start-note 0 --end-note 127
.venv/bin/midi-lights cc-scan --channel 2 # for CC-driven LEDsaudio callback ──► lock-free ring buffer ──► Analyzer (RMS, FFT bands, onset)
│
▼
scheduler ─► Clock ─► ctx ─► Mode/Pattern.render(ctx) ─► Frame ─► Renderer ─► MIDI ─► K2
(60Hz, drift-corrected
on perf_counter)
engine/frame.py—LEDStateenum, immutable sparseFrame.engine/renderer.py— diff against last state, mutex-color enforcement (red→green sends red-off then green-on), 128-msgs-per-tick rate cap, and ablackout()primitive that brute-force sends velocity 0 to every LED note even after state desync.engine/scheduler.py— drift-corrected 60Hz loop usingperf_counter. Skips frames if behind by more than one interval rather than catching up.engine/context.py— read-only per-tick state passed to modes.audio/source.py—sounddeviceinput → lock-free ring buffer.audio/analysis.py— 8 log-spaced FFT bands, RMS, spectral-flux onset with adaptive threshold and a refractory cooldown.audio/modes.py— six audio-reactive modes plus a registry.audio/text_mode.py— 4×4-bitmap scrolling text with four fonts.audio/controller.py— debounced chord detector for the bottom knobs: 150 ms window distinguishes a single press (cycle effect) from a chord (toggle text mode).patterns/— clocks (FixedBPMClock,MidiClockwith stall handling), built-in patterns, YAML loader.device/xone_k2.py—INPUT_NOTES,_LED_RGB, layer-to-channel map,format_cell/parse_cellfor AxB names.
.venv/bin/python -m pytest14 tests covering the renderer diff, blackout, mutex-color ordering, rate
cap, MIDI clock stall detection, and YAML pattern loading. Renderer tests
run against an in-memory FakeMidiOut (in tests/fixtures/fake_midi.py),
so they require no hardware.
k2-lights/
├── pyproject.toml
├── config/patterns/
│ ├── chaser.yaml
│ └── breathing.yaml
├── docs/
│ └── k2-layout.md # K2 physical layout + AxB naming
├── src/midi_lights/
│ ├── cli.py # click commands
│ ├── engine/ # frame, renderer, scheduler, context
│ ├── device/ # xone_k2.py, midi_io.py
│ ├── audio/ # source, analysis, modes, text_mode, controller
│ ├── patterns/ # clocks, ambient, tempo, scripted YAML loader
│ └── modes/ # CLI ↔ mode registry
└── tests/
├── fixtures/fake_midi.py # recording MIDI backend
└── test_*.py
The Xone:K2 MIDI mapping was reverse-engineered live against one physical
unit using calibrate and map-leds. Both tools are in the CLI; pull
requests with mappings from other K2 units (or other Allen & Heath
controllers with similar topology) are welcome.