Skip to content

Microtiming

This content is for v0.7. Switch to the latest version for up-to-date documentation.

Microtiming combinators shift individual events in a pattern forward or backward in time, adding groove and human feel without changing the notes themselves. All amounts are step-relative: they represent a fraction of the event’s step size, so the same value produces proportionally the same feel regardless of pattern density.

Original note durations are preserved — notes are moved in time, not shortened or lengthened.

Apply per-step timing offsets to pattern events. Each offset is a fraction of the event’s step size (0.0 = no shift, positive = forward, negative = backward). Offsets cycle through the array for each 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

The offsets array wraps around if the pattern has more events than offsets:

// Two offsets applied to 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. Notes may overlap slightly.

Offsets can come from a pattern, changing per cycle:

// Alternating offset arrays: one cycle nudged, next cycle straight
[bd sd bd sd].nudge(<[0 0.25 0 -0.1] [0 0 0 0]>)

Classic swing feel. Even-indexed events stay on the grid, odd-indexed events shift forward by amount as a fraction of their step size.

[bd sd bd sd].swing(0.25) // moderate swing

Typical swing amounts:

AmountFeel
0.15Subtle shuffle
0.25Moderate swing
0.33Triplet feel (classic MPC)
0.5Maximum / dotted feel

Because amounts are step-relative, swing(0.33) produces the same triplet feel whether the pattern has 4 events or 16 events.

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 swing amount can vary per cycle:

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

Deterministic random timing variation. Each event receives a unique offset derived from a hash of its start time, mapped to the range [-amount, +amount] as a fraction of the event’s step size.

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

Key properties:

  • Deterministic: the same pattern always produces the same offsets. No randomness between playback cycles.
  • Unique per event: each event’s offset is different, derived from its position.
  • Bounded: offsets never exceed [-amount, +amount] of the event’s step size.
  • Density-invariant: the same amount produces proportionally the same jitter for 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

The offsets are unique per event — derived from a hash of each event’s start time. This means the variation is always the same on every playback, but each step gets a different offset.

Recommended amounts:

AmountFeel
0.05Barely perceptible, just off-grid
0.1Subtle human feel
0.15Noticeable but musical
0.25Loose, relaxed timing

The humanize amount can vary per cycle:

// Tight on one cycle, loose on the next
[bd sd bd sd].humanize(<0.05 0.2>)

Microtiming combinators can be chained. Each combinator sees the already-shifted events from the previous one.

Apply structural timing first, then randomness last:

  1. .nudge() or .swing() — set the groove skeleton
  2. .humanize() — add variation on top
// Recommended: swing first, then humanize
[bd sd bd sd].swing(0.25).humanize(0.05)
// Also valid: custom groove with humanize
[bd sd bd sd].nudge([0.0, 0.15, 0.0, -0.08]).humanize(0.05)

Applying humanize before swing means the swing offsets land on already-jittered positions, which can produce less predictable groove structures.

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
.nudge([0 0.1])Gentle push on every other beat (like mild swing)
Original: | bd | sd | bd | sd |
0 0.25 0.5 0.75 1.0
After .swing(0.25): | bd | sd | bd | sd |
0 0.3125 0.5 0.8125 1.0
After .humanize(0.05):
| bd | sd | bd | sd |
0 0.31 0.51 0.81 1.0
← ← ←
Each event jittered ±5% of step from its swung position
  • Boundary filtering: Events shifted outside the cycle [0, 1) are dropped. The pattern engine only keeps events whose start time falls within the cycle.
  • Negative nudge on first event: An event at position 0.0 nudged by a negative offset moves before the cycle start and is filtered out.
  • Zero / empty passthrough: .nudge([]), .nudge([0, 0, 0]), .swing(0.0), and .humanize(0.0) all pass events through unchanged with no performance overhead.
  • Duration preservation: Notes keep their original duration after shifting. A shifted note may extend past the cycle boundary — it sustains at its full length.
  • Overlaps: Small overlaps between notes from timing shifts are allowed and sound natural.
use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("cr78")));
drums << [bd sd bd sd].swing(0.25);
let hats = AudioTrack("hats");
hats.load_instrument(Sampler(Kit("cr78")));
hats << [hh*16].humanize(0.1);

For full control over the timing of each step:

// Push the snare late, pull the last kick early
drums << [bd sd bd sd].nudge([0.0, 0.15, 0.0, -0.08]);

Different layers can have independent microtiming:

let kicks = AudioTrack("kicks");
kicks.load_instrument(Sampler(Kit("cr78")));
kicks << [bd*4].humanize(0.05);
let hats = AudioTrack("hats");
hats.load_instrument(Sampler(Kit("cr78")));
hats << [hh*8].swing(0.2).humanize(0.05);
let snares = AudioTrack("snares");
snares.load_instrument(Sampler(Kit("cr78")));
snares << [_ sd _ sd].nudge([0.0, 0.1]);