Skip to content

Microtiming

A pattern on the grid is precise, but precision isn’t always what you want. Real drummers push and pull against the beat — that’s where groove lives. Microtiming methods nudge individual events forward or backward in time without touching the notes themselves, the way a DAW’s groove templates or “humanize” button shift hits a few milliseconds off the grid.

Every amount here is step-relative: it’s a fraction of the event’s own step, not an absolute time. So the same value gives you proportionally the same feel whether the pattern has four steps or sixteen. And notes keep their original length — they move in time, they don’t stretch or shrink.

Here’s the payoff up front — a backbeat with a moderate swing:

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd].swing(0.25);
PLAY;

The three methods below — .nudge(), .swing(), and .humanize() — cover everything from hand-placed grooves to deterministic human jitter.

.nudge() gives you per-step control: each offset is a fraction of that event’s step (0.0 = on the grid, positive = later, negative = earlier). The offsets cycle through the array, one per event.

[bd sd bd sd].nudge([0.0, 0.25, 0.0, -0.1]);
OffsetMeaning
0.0No shift
0.2525% of step forward
-0.110% of step backward
0.5Half a step forward

If the pattern has more events than offsets, the array wraps around:

// Two offsets across four events: [0, shift, 0, shift]
[bd sd bd sd].nudge([0.0, 0.1]);
Offsets: 0.0 +0.25 0.0 -0.1
Original: | bd | sd | bd | sd |
0 0.25 0.5 0.75 1.0
Nudged: | bd | sd | bd | sd |
0 0.3125 0.5 0.725 1.0
^^ ^^
shifted +25% shifted -10%
of step of step

Each event keeps its original duration after shifting, so notes may overlap slightly — which is exactly what you want for a natural feel.

The offsets can come from a pattern, so the groove changes from cycle to cycle:

// One cycle nudged, the next cycle straight
[bd sd bd sd].nudge(<[0 0.25 0 -0.1] [0 0 0 0]>);

.swing() is the classic shuffle: even-indexed events stay put, odd-indexed events shift later by amount (a fraction of their step). One number, instant groove.

[bd sd bd sd].swing(0.25); // moderate swing
AmountFeel
0.15Subtle shuffle
0.25Moderate swing
0.33Triplet feel (classic MPC)
0.5Maximum / dotted feel

Because the amount is step-relative, swing(0.33) lands the same triplet feel whether your pattern has 4 steps or 16.

Original: | bd | sd | bd | sd |
0 0.25 0.5 0.75 1.0
Moderate (0.25): | bd | sd | bd | sd |
0 0.3125 0.5 0.8125 1.0
Triplet (0.33): | bd | sd | bd | sd |
0 0.3325 0.5 0.8325 1.0

The amount can vary per cycle too:

// Moderate swing on one cycle, triplet on the next
[bd sd bd sd].swing(<0.25 0.33>);

.humanize() scatters each event by a small random offset in the range [-amount, +amount] of its step. Unlike a real humanize button, this one is deterministic — the offset for each event comes from a hash of its start time, so it’s different for every step but identical on every playback.

[bd sd bd sd].humanize(0.1); // +/- 10% of step jitter

That gives you the best of both worlds:

  • Deterministic — the same pattern always jitters the same way. No drift between cycles, nothing to surprise you mid-set.
  • Unique per event — each step gets its own offset, derived from its position.
  • Bounded — offsets never exceed [-amount, +amount] of the step.
  • Density-invariant — the same amount feels proportionally the same at any pattern density.
Original: | bd | sd | bd | sd |
0 0.25 0.5 0.75 1.0
Humanized: | bd | sd | bd | sd |
(amount=0.1) 0 0.24 0.51 0.53 0.74 1.0
← → → ←
Each event scattered independently within ±10% of step
AmountFeel
0.05Barely perceptible, just off-grid
0.1Subtle human feel
0.15Noticeable but musical
0.25Loose, relaxed timing
// Tight on one cycle, loose on the next
[bd sd bd sd].humanize(<0.05 0.2>);

These methods chain, and each one sees the already-shifted events from the previous one. The rule of thumb: set the groove skeleton first, add randomness last.

  1. .nudge() or .swing() — lock in the groove structure
  2. .humanize() — sprinkle variation on top
// Recommended: swing first, then humanize
[bd sd bd sd].swing(0.25).humanize(0.05);
// A custom groove with a little human feel
[bd sd bd sd].nudge([0.0, 0.15, 0.0, -0.08]).humanize(0.05);

Humanizing before swinging means the swing offsets land on already-jittered positions, which muddies the groove structure — so keep humanize at the end.

CombinationUse case
.swing(0.25)Clean shuffle feel
.humanize(0.1)Subtle imperfection, no groove change
.swing(0.25).humanize(0.05)Shuffle with human feel
.nudge([...]).humanize(0.05)Custom groove with variation
  • Boundary clamping: an event shifted outside the cycle [0, 1) snaps to the cycle boundary rather than disappearing — pushed before the start, it lands at 0.0; pushed past the end, it lands at the last valid position. No events are ever silently dropped.
  • Zero / empty passthrough: .nudge([]), .swing(0.0), and .humanize(0.0) pass events through unchanged with no overhead.
  • Duration preserved: notes keep their full length after shifting; a shifted note may sustain past the cycle boundary.
  • Overlaps: small overlaps from timing shifts are allowed and sound natural.

Different layers can carry independent microtiming — straight kicks, swung-and- humanized hats, a nudged snare:

use "std/instruments" { Sampler, Kit };
let kit = Sampler(Kit("CR-78"));
let kicks = AudioTrack("kicks");
kicks.load_instrument(kit);
kicks << [bd*4].humanize(0.05);
let hats = AudioTrack("hats");
hats.load_instrument(kit);
hats << [hh*8].swing(0.2).humanize(0.05);
let snares = AudioTrack("snares");
snares.load_instrument(kit);
snares << [_ sd _ sd].nudge([0.0, 0.1]);
PLAY;

With timing under control, let patterns generate themselves.