A quantized drum part can sound like a machine counting. A humaniser loosens it: each hit lands a few milliseconds off the grid, each one a little softer or harder, and the part starts to breathe. Play a steady repeated note below, then raise Timing and listen to the grid melt.
Play a steady run of notes and raise Timing. Do any notes ever arrive early, or only late?
Loosen a stiff part
Humanize
Add timing and velocity jitter for human feel.
- Timing 0.40
- Velocity Rand 0.30
- Seed 7.00
Real players rush and drag: some notes land early, some late. So why does this humaniser, and almost every live one you'll meet, only push notes later? If a tool wanted to move an incoming note 10 ms earlier, where would it get that note from?
A live tool can't see the future
A MIDI tool works on events as they arrive. When a note reaches it, that note is already happening now. To play it 10 ms earlier, the tool would have needed it 10 ms ago, before the player pressed the key.
So a live tool has one direction available: later. Humanize delays each note by a random amount between zero and a window set by Timing, up to 50 ms at the top of the range. At 0.4, the window is 20 ms.
This turns out to suit music better than you might expect. Musicians "lay back" behind the beat far more often than they rush it; a lot of soul, hip-hop and neo-soul gets its feel from notes that sit just behind the grid, and J Dilla's loose, off-grid drums are the famous example. A delay-only humaniser is a small version of that pocket.
There is a side effect. Every note lands somewhere between zero and the full window late, so on average the whole part sits about half a window behind. If the part feels late against the rest of your track, slide it earlier in your DAW by about that much, and the wobble spreads evenly around the beat again. Offline editors can move notes earlier directly, because they can see the whole phrase in advance.
A delayed note can outlive its block
The host doesn't hand a MIDI tool one note at a time. It hands it a block: a short slice of time, often 128 samples (2.7 ms at 48 kHz), with any events that fall inside it. A tool's output events must also land inside the block it's handing back.
Now delay a note by 5 ms. It started in this block, but it belongs two blocks from now.
A note that belongs to a later block
Humanize handles this by keeping a pending list. Each incoming note goes in with its absolute target time. As blocks go by, the tool checks the list and sends out any note whose time falls inside the current block, at the right position within it. Nothing is squeezed into the block it arrived in, and nothing is dropped because the delay crossed a boundary.
That list needs a size limit, chosen in advance. Humanize keeps enough room for the longest delay it can produce at its supported block sizes, and refuses to start under conditions it can't cover rather than silently losing notes. Your own tool needs the same decision: what happens when someone plays faster than the list can hold?
Random, but the same every time
Feed Humanize the same phrase twice, from a fresh start, with the same Seed, and the wobble is identical: each note gets the same nudge as last time. Change the Seed and you get a different, equally plausible feel. That surprises people: isn't random supposed to come out different each time?
Not in audio code. A seeded generator produces a sequence that looks random but is completely determined by its starting number. Humanize draws one value per note from a counter mixed with the seed, and uses it for both the timing nudge and the velocity change. Same seed, same notes in the same order, same fresh start: same feel.
That's a feature you'll want:
- Recall. You find a groove you love on seed 7. Reopen the project next month and it's still seed 7's groove, not a new roll of the dice.
- Bouncing. An offline render matches what you heard while playing.
- Testing. A test can expect exact output, because there is an exact output.
In a Maru Processor, Math.random isn't even allowed; the build rejects it and points you to a seeded generator such as @maruaudio/dsp/noise/xorshift32. Give any randomness in your own tool a Seed parameter, and treat each seed as a sound.
Keep a ledger of what you started
Now a transposer, the simplest MIDI tool there is: add twelve to every note number. Hold a note, turn the knob from +12 to +7, then let go. Which note should the release turn off?
When the key goes up, which output note has to end?
Turn the knob while a key is held
note 60 onKey down
Transpose is +12, so the tool sends note 72 on.
- transpose
- +12
- sent
- 72 on
- sounding
- 72
The arithmetic was right in all three cases. The bug in the first one is memory: a note-on is a decision that lasts until its release, and the tool has to keep it. Maru's MIDI Transpose module works exactly like the second and third cases. A release always ends the pitch its note-on was sent as, whether you turned the knob, changed octave or bypassed in between. Overlapping repeats of one held key keep their first pitch until every release arrives. Notes on channel 4 leave on channel 4.
Test your MIDI tool against this rule: every note-on the tool sends must be ended exactly once. MIDI Transpose's tests replay knob turns, bypasses, repeats, a panic and a reset, then check that rule for every emitted note.
Where variation stops helping
A humaniser can push an accent so late it drags the groove, soften the downbeat until the phrase loses its shape, or reorder two fast notes so their releases cross. More randomness isn't more human. Human timing has intent: a snare that's late on purpose, a hi-hat that pushes into the chorus.
Our take: start with a small window and listen to a whole phrase, not one bar. Change timing or velocity, not both at once. When a seed sounds right, keep it.
Same idea, different place: Rhythm, meter and groove treats timing as part of the phrase: swing, accents and where a player chooses to sit. A random window is one tool among those, not a replacement for them.
Make it yours
Specify a small transform before you code it: "every third note goes up an octave". Then work it on paper with a tricky phrase, a held note overlapping the next, a repeated key, a chord. Which notes count as "third"? Does each channel count separately? What does a reset do to the counter? Write down which output note each release must end.
There's no MIDI tool template to scaffold yet, so start from MIDI Transpose's source in modules/midi-transpose: it already has the ledger, the range rule and the tests. Change the transform, keep the bookkeeping.
If your tool uses randomness, give it a Seed parameter. Then save a preset with a seed you like and a short held-chord test phrase that turns your knobs while keys are down. Run it after every change: when every note that started also ends, your tool is safe to put in front of anyone's synth.
Quick check
- A held note started at +12, then the knob moved to +7. Which pitch should its release end? The +12 note. Its note-on made the decision, and the release has to honour it. Recomputing with the current knob is the natural first try, and it's how stuck notes are born.
- A plugin promises to move notes 10 ms early, live, with no added latency. What's missing? The future. A live tool only has notes once they've happened. It can delay, or it can delay everything first (adding latency) and then move some notes less, but it can't reach back in time.
- Two renders use the same seed but sound different. What else could have changed? The notes or their order, or the starting state (no reset between runs). A seed repeats a sequence only when everything it's applied to repeats too.
Next steps
- Use it: Realtime and numerical safety. Humanize had to plan for notes that outlive a block. What else breaks when your code is called in blocks of unknown size?
- Preparation: MIDI. What note, velocity and channel messages actually carry.













