Rhythm: grooves and polyrhythm
Blocks cover the simple half of rhythm — *how many notes share a slot*. This page covers the other half: how a slot can be unevenly subdivided (grooves) and how multiple lines can run at independent periods (polyrhythm).
Both stay relative. A groove is a per-slot offset that applies to *any* block; a polyrhythm is an operator that combines *any* two lines.
Grooves
A perfectly-equal block sounds quantised. Real grooves nudge each slot by some small amount: a hip-hop snare lands a few percent late, a swing-feel pushes the offbeat. relanote captures that as a groove declaration — a vector of fractional offsets that repeats every length slots.
; Standard jazz 8th-note swing — the offbeat lands 17% late.
groove Swing67 = offsets {
length: 2,
steps: [0.0, 0.17]
}
; J Dilla's drunken hip-hop feel — snare drags, hat-hats jitter.
groove Dilla = offsets {
length: 16,
steps: [
0.00, 0.04, -0.02, 0.05,
-0.03, 0.06, 0.02, 0.07,
0.00, 0.05, -0.02, 0.05,
-0.04, 0.07, 0.02, 0.08
]
}Activate one globally or per-track:
set groove = Dilla ; everything from here on grooves
let beat = | R - R - | R R R - |
beat |> groove Swing67 ; only this passage swingsThe stdlib ships a starter pack — Straight, Swing67, Swing75, Dilla, Bossa, Shuffle, HalfTimeLazy, Drunken — in grooves.rela.
Polyrhythm with over
a over b plays both sides at the same time, with a looped at its own period and b looped at its. The combined figure repeats every lcm(slots(a), slots(b)) slots:
scale Major = { R, M2, M3, P4, P5, M6, M7 }
let three = | <1> <3> <5> | ; period 3
let four = | <1> <2> <4> <5> | ; period 4
three over four ; 3-against-4, resolves every 12 slotsA "3 against 2" hemiola — the rhythmic backbone of much African, Brazilian and modern classical music — is one line:
| <1> <3> <5> | over | <1> <5> |over composes:
let twelve_eight_clave =
| R - R - R - |
over
| R R R R R R R R |Polymeter
A polyrhythm gives two lines different *subdivisions* of the same time. A polymeter gives them different *bar lengths*. Same operator — relanote doesn't draw the distinction at the syntax level:
let line_a = | <1> <2> <3> <4> <5> |:5 ; 5 beats long
let line_b = | <1> <3> |:4 ; 4 beats long
line_a over line_b ; 5 / 4 polymeter — bars realign every 20 beatsTuplets inside grooves
Tuplets ({ ... }:n) still work as before — they're a *local* subdivision, separate from the groove or polyrhythm system. Mix them freely:
set groove = Swing67
let phrase = | <5>~ { <6> <5> <4> }:2 <5>~ - |
phrase ; grooved sixteenths around a triplet turnStatus
Expr::Over, Item::GrooveDef and the groove keyword are on the AST (src/ast/sound.mbt); preset grooves are in the stdlib. The parser and evaluator skeletons recognise the shapes but don't yet apply them at render time — that lands alongside the parser / evaluator ports.
Listen-through example
This keeps the examples audible today: straight pulse, a 3-in-2 tuplet and a denser answer phrase.
scale Major = { R, M2, M3, P4, P5, M6, M7 }
let pulse = | <1> - <5> - |:4
let trip = | { <1> <3> <5> }:2 |
let push = | <1> <2> <3> - <5> <6> - <5> |:4
pulse ++ trip ++ push