Every drummer believes they have good timing until they hear a recording of themselves. Behind the kit a fill feels locked in; on the recording, it rushed. Rhythm Checker closes that gap: every hit measured against the grid, every drum tuned against a saved target — honest numbers instead of feel.
What it deliberately does not do: judge the music. Groove is a human call. Sometimes dragging slightly is the whole point. The data has one job, telling the truth about your time and your tuning. Deciding what to do with that truth stays with you.
The project is two tools that share one engine (see docs/PLAN.md):
Live app (webapp/) |
Drum tuner, Guitar-Hero-style trainer across all 40 PAS rudiments, live timing check, per-device latency calibration. Runs on iPhone (installable PWA, works offline) and any laptop browser. Zero cloud APIs, zero runtime cost. All DSP happens on the device. |
Deep analyzer (rhythm_checker/) |
Post-session truth: drift, fills, per-position stats, multi-week history, tuning reports from recordings. Python CLI. |
Install it on any iPhone: open
https://ben-k-jordan.github.io/Rhythm-Checker/ in Safari, then Share,
then Add to Home Screen. After the first visit it works fully offline.
iOS requires HTTPS for mic access, and GitHub Pages provides that for free.
Pushes to main that touch webapp/ redeploy it automatically.
To run it locally instead:
cd webapp && python -m http.server 8000 # then open http://localhost:8000- Tuner: tap the head, read the fundamental in Hz, note, and cents. Every drum carries two targets, batter and reso, with the gap between the heads shown in semitones and what that gap does to the feel. One tap loads a player's whole setup, sized to your exact drums — rock at John Bonham, punk at Travis Barker, metal at Joey Jordison, djent at Tomas Haake. It writes their batter and reso targets, seeds that player's working tempo into the trainer and timing screens, and floats their signature rudiments to the top of the rudiment picker (Haake's 3/5/7 groupings, Bonham's single-stroke triplets): their ballpark, your ears and hands finish it. Lug mode logs a pass around the drum and marks the lugs that are off, and a fold-out crib sheet covers how to get a clean reading at soundcheck.
- Rudiments: a falling-note highway across all 40 PAS rudiments (paradiddles, rolls, flams, drags, ratamacues) synced to a sample-accurate metronome. Every hit is judged perfect, good, ok, or miss with its signed ms error, streaks, and an honest end-of-run report including per-step means ("your RR doubles run early"). Accents are yours on every one of them: keep the rudiment's written accents, accent the pulses, strip them all, or tap your own onto the pattern. The groove bar makes the practice variables one-tap adjustable: big BPM steps, tap tempo, time signatures from 2/4 to 12/8 with odd groupings (7/8 as 2+2+3, 3+2+2, or 2+3+2), lead hand switch, and a tempo ramp (+5 BPM every 4 bars) whose report breaks the spread down per tempo, showing the exact BPM where the rudiment falls apart.
- Timing: free play against the click (same groove bar: any meter, any tempo) with a live early/late strip and running mean, spread, and pocket. Save a great day as your baseline.
- History: every completed run is analyzed and saved automatically on the device. Spread trend across sessions, per-run table, CSV export.
- Calibrate: measures your device's fixed audio latency by tapping along with clicks, then subtracts it from every score. Run it once per device.
The scope is deliberately lean. Through v2.2 the app also carried a full show-night flow — pre-show readiness checks, live set monitoring, a stamped DIALED / NOT YET verdict. It worked, and it was cut anyway: a stranger installing a practice tool shouldn't meet an ARM FOR SHOW button. It lives intact in the git history.
The look is a photocopied gig poster: cream newsprint, hard ink borders, one loud red, hazard stripes, offset shadows with zero blur, and newsprint grain over everything. Type is Anton for the shouting, Oswald for the labels, and Space Mono for every number. A four-bar level meter ticks in the header next to the wordmark, the hazard tape crawls, and the tuner needle swings. All of it is drawn in CSS and SVG with no raster assets (the app icon aside), runs GPU-composited so the audio thread never feels it, and under Reduce Motion (which iOS Low-Power Mode also triggers) pares back to just those two small identity loops — the level bars and the hazard tape — with the flashy motion switched off.
pip install . # numpy only
pip install .[charts] # adds matplotlib for the HTML chart reportsWAV files work out of the box: 8, 16, 24, and 32-bit PCM, float, stereo, and
WAVE_FORMAT_EXTENSIBLE headers included. For phone-native formats (.m4a,
.mp3), install ffmpeg and Rhythm Checker will use it
automatically.
- Set your metronome to the tempo you're practicing (say 120 BPM) and note it.
- Put your phone somewhere it can hear the kit, hit record, play your session.
- Get the file onto your computer and run:
rhythm-checker analyze practice.wav --bpm 120 --subdivision 4 --html report.html--subdivision is the finest grid you actually played: 1 quarters, 2
eighths, 3 eighth triplets, 4 sixteenths (default).
If the software can't hear your metronome, it fits the grid to your own playing. That makes your consistency, drift, and fill-vs-groove differences measurable, but it hides any constant early or late tendency, because the grid moves to meet you. The report always tells you which mode you're in.
To measure absolute push or drag, anchor the grid: let the recording hear the metronome alone for four beats before you start playing (hold the phone near the metronome speaker, or just don't wear headphones for those four clicks), then:
rhythm-checker analyze practice.wav --bpm 120 --count-in 4The first four detected hits become the grid anchor, and the report gains a
per-beat-position breakdown (are your "&"s consistently late?). The tool
cross-checks that those four clicks are actually spaced like your --bpm and
warns if they aren't.
TIMING vs the grid (negative = early/ahead, positive = late/behind)
mean -6.3 ms median -5.8 ms spread (SD) 11.2 ms
62% early · 31% late · 44% within ±10 ms
DRIFT across the session: +3.1 ms/min toward late (correlation r = 0.55)
first half: mean -8.1 ms ...
second half: mean -4.4 ms ...
HIGH-DENSITY PASSAGES (busy playing, often fills): 3 found, 84 hits
in dense passages: mean -11.9 ms ...
everywhere else: mean -4.9 ms ...
- mean / median: your average placement against the grid (anchored mode), or your balance around your own average time (unanchored mode).
- spread (SD): consistency. This is the number to watch across weeks.
- drift: the slope of your deviations over the session. Positive means sliding late as you go (hello, fatigue).
- high-density passages: bursts of busy playing, usually fills. The tool reports density, not "fills": it can measure busy, it can't know intent.
- unattributable hits: anything farther than 40% of a grid step from every grid line is reported as unattributable rather than silently forced onto the nearest line.
Add --json data.json for the full per-hit table, and --fit-tempo if you
suspect your phone's clock (it corrects up to ±0.5% skew, but it will also
absorb genuine steady drift, so leave it off by default).
Record a lug pass (tap 2 to 3 inches from each lug, let each ring) and:
rhythm-checker tune lugs.wav --target 141Every tap is pitched (the fundamental, not the louder overtones a drumhead also rings), listed with its cents vs the drum's median, and the odd lugs are flagged. Multiple drums in one recording cluster into separate groups.
Every analysis is inserted into a SQLite database at
~/.rhythm-checker/sessions.db. It's plain SQL, so inspect it with any sqlite
client, or override the location with --store or $RHYTHM_CHECKER_STORE.
rhythm-checker historyshows every session's spread, pocket percentage, and drift side by side. It's the honest picture that no amount of feel can give you, and the thing to open before you decide what to practice today.
python examples/make_demo.py demo.wav
rhythm-checker analyze demo.wav --bpm 110 --subdivision 4 --count-in 4 \
--html demo-report.html --no-saveThe synthetic drummer in the demo starts about 8 ms ahead of the click, rushes the one sixteenth-note fill, and drags progressively as the "session" goes on. Check that the report says exactly that. Finding it is the whole point of the tool.
- Onset detection: spectral flux over a chunked STFT with an adaptive median threshold, then each hit is refined against the waveform's amplitude envelope to well under the STFT hop (about 1 ms relative precision on clean percussive material).
- Grid fit: circular (phase) statistics lock the grid to the count-in clicks (anchored) or to the playing itself (unanchored, clearly labeled).
- Honest stats: signed per-hit deviations, spread, linear drift fit, density segmentation, per-beat-position breakdown. Outliers are reported as unattributable, not forced into the average.
Every statistic is verified by tests that synthesize a drummer with known
flaws and require the report to find them: 84 Python tests (pytest), plus a
Playwright browser harness that runs the real app in Chromium with a fake
microphone, executes an in-page DSP ground-truth self-test, and walks every
screen (node tests/web/run.mjs, with playwright resolvable on
NODE_PATH).
- Unanchored mode cannot see a constant early or late tendency. The report
says so every time; use
--count-inwhen you want the absolute number. - Hits closer together than 30 ms merge into one, so a tight flam or drag
counts as a single hit (
--min-gap-msadjusts the window), and a hit inside the first 25 ms or so of the recording can't be detected. Leave a moment of room tone before playing. - Very quiet ghost notes may be missed (raise
--sensitivity), and buried metronome bleed can occasionally be picked up as hits. --fit-tempocannot distinguish device clock skew from a perfectly steady human tempo drift. It's off by default for that reason.- Swung or shuffled playing measured against a straight grid splits into two
clusters; the report warns and suggests a subdivision that fits instead of
quietly printing a meaningless spread. Likewise, a wrong
--bpmis refused outright (the hits don't concentrate on any grid at that tempo), tempos outside 20 to 400 BPM are rejected, and recordings below 16 kHz get a warning that several ms of the reported spread may be measurement error. - The tool measures a recording, not a performance. A bad mic position will smear transients and inflate the spread a few milliseconds.






