28 milliseconds: turning drum notation into a schedule
Drum notation looks like it should be trivial to turn into sound. It is a grid of hits. Put a click on each one.
Then you try to encode a flam, and the whole model falls apart.
A flam is not two hits at the same time
On the page, a flam is two notes drawn almost on top of each other: a small grace note, then the main note. A naive reading says "play both", and if you do that literally you get a doubled click — which is not a flam, it is a mistake.
What a flam actually is: one hand plays a quiet grace note slightly before the main note, so close that the ear hears a single thicker attack. The spacing is tens of milliseconds, and the volume difference is most of the effect.
In the trainer I build, the spacing is 28 ms and the grace note plays at 0.10 of full amplitude against 0.18 for an ordinary note and 0.30 for an accented one. Those three numbers are the whole difference between a flam, a tap, and a flam accent. Get any of them wrong and the rudiment stops sounding like the rudiment.
That was the first lesson: the notation is not the schedule. The schedule is (time, hand, velocity), and the notation is a compressed description that has to be expanded first.
Encoding 40 rudiments as strings
The data for all 40 standard rudiments is a list of short strings:
{ n: 'Single Paradiddle', s: 'RLRRLRLL', g: 'Paradiddle' }
{ n: 'Flam Accent', s: 'lR L R rL R L', g: 'Flam' }
{ n: 'Flam', s: 'lR', g: 'Flam' }
Three rules do all the work:
- A space separates steps. A step is a sixteenth note, so the grid is musical rather than absolute.
- An uppercase letter is a main note; the letter is the hand (R or L).
- A lowercase letter is a grace note, played 28 ms early and quiet.
Parsing is one line per character:
out.push({ right: lower === 'r', grace: ch === lower });
A single line covers a rudiment that would take a paragraph to describe in prose. And because the strings are readable, adding a rudiment is editing a string, not writing code.
The trade-off is that this encoding cannot express everything. It has no dynamics beyond accent/grace, no flams with double grace notes written differently from single ones, and no subdivision inside a step. For the standard 40 that is enough — and where it is not enough, I would rather have a second field than a more expressive string language.
Time is musical, not milliseconds
The tempo is in BPM, but the scheduler thinks in sixteenth notes:
function stepMs() { return 60000 / state.bpm / 4; }
Everything derived from tempo goes through that one function. Change the BPM and the whole sequence rescales, because no duration is hard-coded anywhere else. If any part of the code had stored "a step is 187.5 ms", the trainer would only work at one tempo.
This is the same reason the accent pattern is an array of step indices (acc: [0, 2, 4, 6]) rather than a per-note flag: the pattern has to survive a tempo change and a subdivision change without being rewritten.
Hearing which hand played
Both hands play the same voice — a triangle wave with a fast envelope — but at different frequencies: 1180 Hz for the right hand, 790 Hz for the left. Roughly a fifth apart, which is enough separation to hear two distinct hands and not so much that it sounds like two different instruments.
That is a small decision with a large effect on whether the tool teaches anything. If both hands sound identical, a student can hear that something played and cannot hear which hand played it, which is precisely the information a rudiment trainer exists to convey. Pitching the hands apart turns the audio into a check on the sticking: you can hear a wrong hand.
The envelope is four milliseconds of attack into an exponential decay. Without the fast attack the clicks smear; without the decay tail they sound like a test tone rather than a stroke.
The same index drives the screen
The player keeps a single index — which step is sounding — and the renderer highlights the matching span. There is no separate animation timeline to drift out of sync with the audio, because both read the same state.
This matters more than it sounds. Two timelines (one audio, one visual) always separate eventually, usually right when someone is watching closely. One source of truth cannot desynchronise from itself.
What this design costs
The audio is synthesised, not sampled from a real snare. That is a real limitation: a student practising to a sine click is learning the grid, not the sound. Recorded samples would sound better and would also multiply the page weight, the licensing questions, and the number of browsers that fail to decode them.
The encoding is deliberately narrow, and the tool is a reference rather than a teacher — it will play a rudiment at your tempo with the hands separated, and it will not tell you that your grip is collapsing.
Those are the boundaries I chose. The interesting part was not the audio code; it was discovering that most of the work sits between the notation and the schedule, in 28 milliseconds and three velocity levels.
The trainer described here runs at drumdesk.net/tools/rudiment-trainer — all 40 rudiments, 40–220 BPM, hands separated, no account.
Top comments (0)