A metronome that runs anywhere a browser does — and looks just as good in a terminal.
Sub-millisecond timing · click-to-edit accent patterns · works offline as a PWA
- BPM 30 – 300 —
+ / −with press-and-hold acceleration, direct numeric input, or60 / 80 / 100 / 120 / 140shortcuts - Built-in presets — 2/4, 3/4, 4/4 (4/4 = strong / weak / medium / weak)
- Custom patterns of 1 – 32 beats — tap each cell to cycle 4 intensities
- 4 distinct click sounds —
█ 1500 Hz/▄ 1000 Hz/░ 700 Hz/· mute - Live beat highlight synced to the audio clock
- Mobile-first PWA — add to home screen, wake-lock keeps the screen alive while practicing, Service Worker caches everything for offline use
- Bonus terminal build — the same metronome as a Textual TUI
The whole web app is a single static folder — no build step, no dependencies. Three ways to run it:
Double-click web/index.html. Web Audio works from file://; only the Service Worker (offline cache) is unavailable. Everything else is identical.
cd web
python -m http.server 8000
# desktop: http://localhost:8000
# phone: http://<your-LAN-ip>:8000 (same Wi-Fi)Then on the phone, Add to Home Screen for the full-screen PWA experience.
Drop web/ on any static host (GitHub Pages, Netlify, Vercel, Cloudflare Pages…). Serving over HTTPS unlocks full PWA install + offline cache on iOS.
WSL2 users: see
web/README.mdfor mirrored-mode /netsh portproxyrecipes if you want phones to hit a WSL-side server directly.
| action | |
|---|---|
Space |
start / stop |
← → or + − |
BPM ± 1 |
↑ ↓ |
BPM ± 10 |
1 2 3 |
2/4 · 3/4 · 4/4 |
| tap any beat cell | cycle intensity (mute → weak → medium → strong) |
Same model, same sounds, rendered with Textual. Useful when you're already in a shell.
sudo apt install -y libportaudio2 # Linux only; PortAudio for sounddevice
pip install -e .
beatronome # or: python -m beater ┌─────────┐
│ 120 │ BPM
└─────────┘
[ − ] [120] [ + ]
Time: [ 2/4 ] [ 3/4 ] [✓4/4] [ Custom… ]
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ █ │ │ ░ │ │ ▄ │ │ ░ │
└───┘ └───┘ └───┘ └───┘
1 2 3 4
[ ▶ START ]
space start/stop · +/- BPM · 1/2/3 presets · c custom · q quit. No audio device? Falls back to terminal bell.
Why a custom scheduler? setInterval and time.sleep accumulate drift on the order of tens of milliseconds — audible at 200 BPM. The web build uses the Chris-Wilson lookahead pattern: a 25 ms setInterval queues clicks 100 ms ahead against AudioContext.currentTime, so jitter is sample-accurate. The Python build does the equivalent with a time.perf_counter() accumulator on a daemon thread.
Why a PWA? Phones are where you actually want a metronome. Add-to-home-screen plus wake-lock plus offline caching means once you've loaded it, you don't need a network — or even a working web server — to use it again.
Pattern data model
const PRESETS = {
"2/4": [3, 1],
"3/4": [3, 1, 1],
"4/4": [3, 1, 2, 1], // strong, weak, medium, weak
};
// 0 = mute, 1 = weak, 2 = medium, 3 = strongCustom patterns are any array of those, length 1 – 32. Tap cells to cycle.
Beatronome/
├── web/ # the main app — zero-build static PWA
│ ├── index.html
│ ├── app.js # Web Audio scheduler, wake-lock, keyboard
│ ├── app.css # mobile-first dark theme
│ ├── manifest.webmanifest
│ ├── sw.js # cache-first service worker
│ ├── icon.svg
│ └── README.md # hosting recipes (Windows / WSL2 / port-proxy)
├── beater/ # optional Python TUI build
│ ├── app.py # Textual App, screen wiring
│ ├── engine.py # threaded scheduler, perf_counter accumulator
│ ├── audio.py # numpy click synthesis, sounddevice playback
│ ├── patterns.py # Intensity enum + presets
│ ├── home_screen.py
│ ├── custom_screen.py
│ └── beater.tcss
├── pyproject.toml
└── banner.svg
MIT. Use it, fork it, hand it to a friend who's learning to keep time.