A rhythm and polyrhythm training tool with multiple visual modes, challenge scoring, and progressive difficulty. Built with Python and Pygame.
- Python 3.14+
- uv (Python package manager) — install instructions
- Windows/macOS/Linux (tested on Windows 11)
- Optional: MIDI controller (e.g., Arturia Keystep 37) for input
# Clone and enter the project
git clone https://github.com/RoboticHuman/PolyrhythmTrainer.git
cd PolyrhythmTrainer
# Install dependencies
uv sync
# Run the app
uv run python -m src.main| Key | Action |
|---|---|
| D / F | Hit Layer 0 (left hand) |
| J / K | Hit Layer 1 (right hand) |
| Space | Hit Layer 2 (3-layer presets) |
| Key | Action |
|---|---|
| Tab | Toggle stats HUD |
| V | Cycle visual mode |
| C | Toggle CRT filter |
| P | Sound Designer (wavetable editor) |
| I | MIDI Settings (device + layer mapping) |
| M | Mute metronome |
| S | Toggle subdivision ticks (LCM grid audio) |
| H | Toggle hit sound mode (granular/uniform) |
| N | Toggle difficulty (relaxed/strict) |
| +/- | Adjust BPM |
| [ / ] | Cycle presets (within current section) |
| Shift+[ / ] | Halve / double layer 0 beats (2-layer presets) |
| Ctrl+[ / ] | Halve / double layer 1 beats (2-layer presets) |
| \ | Switch section (Polyrhythms / Time Signatures) |
| 1-9 | Jump to preset (within current section) |
| R | Restart session |
| Esc | Back to menu / Quit |
| Key | Action |
|---|---|
| Up/Down | Select option |
| Left/Right | Adjust BPM (in preset selector) |
| Enter/Space | Confirm selection |
| V | Change visual mode (in preset selector) |
| Esc | Back / Quit |
- Freeplay — Infinite practice with no timer or score
- Challenge — Timed rounds (30/60/90s) with scoring, auto BPM ramp, and letter grades (S/A/B/C/D)
- Progression — Unlock harder rhythms by earning grades on easier ones (5 tiers)
- Surprise Me! — Random preset, straight into freeplay
- Orbits — Concentric rings with beat markers
- Game of Life — Conway's GoL that evolves with the beat
- Automata — 1D cellular automaton scrolling upward
- Boxing — Two fighters in a ring with referee, coaches, and crowd
- Blacksmith — Silhouette forge scene with sparks, customers, and speech bubbles
- Dance Battle — Disco dance-off with crowd, moderator, and CPU opponent AI
- Cashier — Grocery store checkout with conveyor belt, mood system, and impatient customers
- Samurai — Moonlit bridge duel with health bars, cherry blossoms, and sword clashes
Presets are split into two sections — Polyrhythms and Time Signatures — each ordered by difficulty across 5 tiers:
Polyrhythms
| Tier | Examples |
|---|---|
| Easy | 3:2, 2:3 |
| Medium | 3:4, 4:3 |
| Hard | 5:4, 5:3, grouped polyrhythms (5/4 vs 4, 7/8 vs 3) |
| Very Hard | 7:4, 7:8, 7/8 vs 4, 9/8 vs 4 |
| Expert | 5:4:3, 3:4:5, 7:5:3 (3 simultaneous layers) |
Time Signatures
| Tier | Examples |
|---|---|
| Easy | 5/4 (3+2), 5/4 (2+3) |
| Medium | 7/8 (2+2+3), Bossa Nova, Afro-Cuban 6/8, Rumba Clave |
| Hard | 9/8 (2+2+2+3), Taksim 10/8 |
| Very Hard | 11/8, 13/8 |
The subdivision ticks on the timeline and orbit visualizer show the LCM grid — the shared pulse that both layers divide evenly. Toggle S to hear it as an audio counting aid.
| Mode | Perfect | Good | OK | Miss |
|---|---|---|---|---|
| Relaxed (default) | ±40ms | ±100ms | ±180ms | ±250ms |
| Strict | ±20ms | ±50ms | ±100ms | ±150ms |
PolyrhythmTrainer/
├── src/
│ ├── main.py # Entry point, game loop, state machine
│ ├── config.py # Presets, difficulty, constants
│ ├── engine/ # Timing, rhythm math, scoring, progression
│ ├── input/ # Keyboard and MIDI input handlers
│ ├── audio/ # Metronome, hit sounds, wavetable synthesis
│ ├── visuals/ # Visual modes and shared components
│ └── ui/ # HUD, menus, results, sound designer
├── tests/ # Automated tests
├── data/ # User saves (gitignored)
└── pyproject.toml # Project config and dependencies
# Fast tests only (default) — ~0.1s
uv run pytest tests/ -v
# Include real-time metronome tests — ~45s
uv run pytest tests/ -v -m ""
# Only real-time tests
uv run pytest tests/ -v -m realtimeMIDI uses pygame.midi — no extra dependencies or C++ compiler needed.
- Connect your MIDI controller before launching — the first input device is opened automatically
- Press
Iduring gameplay to open MIDI Settings:- Device dropdown — select which MIDI device to use
- Layer mapping — click a layer, then press a key on your controller to assign it
- F5 to refresh/reconnect if a device was power-cycled
- All note-on events map to Layer 0 by default until you assign them
In src/config.py, add a Preset object to the PRESETS list:
# Even polyrhythm (e.g. 3 against 4)
Preset(
id="my_poly", # Unique string ID (stable, never changes)
name="My Rhythm", # Display name
layers=[3, 4], # int = evenly spaced beats per layer
base_beats=4, # Reference for BPM (4 = quarter note, 8 = eighth)
category="poly", # For menu color coding
tier=TIER_MEDIUM, # Difficulty tier (controls progression unlocking)
)For grouped rhythms, use _grouping() — this creates ALL subdivisions with accents on grouping starts:
# 7/8 with 2+2+3 accent pattern = 7 beats, accents on 0, 2, 4
Preset(
id="odd_7_8_custom",
name="7/8 (3+2+2)",
layers=[_grouping([3, 2, 2])], # All 7 subdivisions, accents on group starts
base_beats=8,
category="odd",
tier=TIER_MEDIUM,
)
# Grouped layer against an even layer
Preset(
id="pg_7_8v4",
name="7/8 (2+2+3) vs 4",
layers=[_grouping([2, 2, 3]), 4], # Layer 0: grouped, Layer 1: even
base_beats=8,
category="poly-grouped",
tier=TIER_HARD,
)Tiers and progression update automatically — no index management needed.
- Create
src/visuals/mymode.pywith a class extendingBaseVisualizer - Implement
render(),on_hit(),on_beat() - Use
Timelinecomponent for the beat bar at the bottom - Add to
VISUAL_MODESinconfig.py - Import and add to
self.visualizerslist inmain.py