Patterns¶
Patterns are the sequencing system in krach. They describe what happens and
when, using composable IR trees with rational time. Patterns are independent
of nodes -- you build a pattern, then bind it to a node with kr.play().
What is a pattern?¶
A pattern is a tree of events distributed over a cycle (one bar by default). The tree is pure data -- an intermediate representation that the Rust engine compiles to block-rate automation curves. No per-event IPC happens during playback.
Patterns are:
- Composable -- combine with
+,|,*, and transforms - Rational time -- subdivisions are exact, no floating-point drift
- Reusable -- the same pattern can play on different nodes
Atoms¶
Atoms are the smallest pattern elements. All pattern builders live on the kr
namespace.
krp.note() -- melodic trigger¶
Sets freq and fires gate (trigger + reset). Accepts multiple pitch formats:
krp.note(440.0) # float Hz
krp.note("C4") # string pitch name
krp.note(60) # int MIDI note number (converted via mtof)
krp.note(440.0, vel=0.7, cutoff=1200.0) # extra params set alongside freq/gate
Note syntax details¶
Float -- interpreted as Hz directly:
String -- pitch name with octave. Sharps use s or #:
krp.note("C4") # middle C
krp.note("Cs4") # C sharp 4
krp.note("C#4") # also C sharp 4
krp.note("Bb3") # B flat 3
Integer -- MIDI note number, converted to Hz internally:
Chords (multiple simultaneous pitches)¶
Pass multiple pitches to krp.note() for a chord:
Or use the | (layer) operator:
Chords require polyphony
The voice must have count >= the number of simultaneous pitches:
krp.hit() -- percussive trigger¶
Fires a gate trigger without setting pitch. Use for drums and one-shot sounds:
krp.rest() -- silence¶
A single beat of silence:
krp.seq() -- sequential notes¶
Plays notes one at a time, in sequence. Use None for rests:
krp.seq(55.0, 73.0, None, 65.0) # Hz values, with a rest
krp.seq("C4", "E4", "G4") # pitch names
krp.seq("A2", "D3", None, "E2") # bass line with rest
krp.seq() also accepts krp.note() objects, letting you set per-note params:
krp.seq(
krp.note(220.0, cutoff=800.0),
krp.note(330.0, cutoff=1200.0),
None,
krp.note(440.0)
)
# Mix pitch strings and note objects freely:
krp.seq("A2", "D3", krp.note("E2", vel=0.5), None)
krp.seq() is NOT for chords
krp.seq("A4", "C5", "E5") plays three notes one after another -- it
is a melody, not a chord. For chords, use krp.note("A4", "C5", "E5").
Operators¶
+ -- sequence¶
Concatenates patterns in time, dividing the cycle equally:
| -- layer (simultaneous)¶
Stacks patterns to play at the same time:
* -- repeat¶
Repeats a pattern N times within one cycle:
krp.hit() * 4 # 4-on-the-floor
krp.hit() * 8 # 8th notes
(krp.hit() + krp.rest()) * 4 # offbeat pattern, repeated 4x
Time transforms¶
.over(n) -- stretch to N cycles¶
Stretches a pattern to span multiple bars:
krp.seq("A2", "D3", None, "E2").over(2) # 4 notes over 2 bars
krp.ramp(200.0, 2000.0).over(4) # 4-bar ramp
.fast(n) -- speed up¶
Doubles (or triples, etc.) the playback speed:
.slow(n) -- slow down¶
The inverse of .fast():
Combinators¶
.every(n, fn) -- periodic transform¶
Apply a transform every Nth cycle:
p = krp.hit() * 4
p.every(4, lambda p: p.reverse()) # reverse every 4th bar
p.every(3, lambda p: p.fast(2)) # double time every 3rd bar
.reverse() -- reverse¶
Plays the pattern backwards:
.spread(hits, steps) -- euclidean rhythm¶
Distributes hits evenly across steps slots:
krp.hit().spread(3, 8) # 3 hits in 8 steps (tresillo)
krp.hit().spread(5, 8) # 5 hits in 8 steps (cinquillo)
krp.hit().spread(7, 16) # 7 hits in 16 steps
.thin(probability) -- degrade¶
Randomly drops events with the given probability:
.mask(mask_str) -- selective silence¶
Suppress events at specific positions using a mask string. 1/x = keep,
0/./~ = silence:
krp.seq("A2", "D3", "E2").mask("1 1 0") # silences the third event
krp.seq("C4", "E4", "G4", "B4").mask("x . x .") # play 1st and 3rd only
.sometimes(prob, fn) -- probabilistic transform¶
Apply a transform with probability prob each cycle. On cycles where it
does not fire, the original pattern plays:
p = krp.hit() * 4
p.sometimes(0.3, lambda p: p.reverse()) # reverse 30% of cycles
p.sometimes(0.5, lambda p: p.fast(2)) # double time 50% of the time
Multi-pattern combinators¶
krp.cat() -- cycle-level concatenation¶
Plays each pattern for one full cycle, then loops:
a = krp.seq("A2", "D3", None, "E2")
b = krp.seq("C3", "E3", "G3", "B3")
kr.play("bass", krp.cat(a, b)) # a for 1 cycle, b for 1 cycle, repeat
This is equivalent to (a + b).over(2) but reads more clearly for
multi-cycle structures.
krp.stack() -- layer patterns¶
Plays all patterns simultaneously. Same as the | operator, but
accepts any number of arguments:
krp.stack(
krp.note("A4"),
krp.note("C5"),
krp.note("E5"),
)
# equivalent to: krp.note("A4") | krp.note("C5") | krp.note("E5")
krp.struct() -- impose rhythm onto melody¶
Takes the onset positions from a rhythm pattern and fills them with values from a melody pattern (cycling if melody is shorter):
rhythm = krp.p("x . x x . x . .")
melody = krp.seq("A2", "D3", "E2")
kr.play("bass", krp.struct(rhythm, melody))
# plays A2, D3, E2, A2 at the rhythm's hit positions
Continuous patterns¶
Continuous patterns generate smooth control sweeps. Use them for
modulation, filter control, and automation. All return Pattern objects
and compose with .over(), +, etc.
krp.sine(lo, hi) -- sine sweep¶
krp.sine(200, 2000) # one-cycle sine from 200..2000
krp.sine(200, 2000).over(4) # 4-cycle sine sweep
krp.ramp(lo, hi) -- sawtooth ramp¶
krp.rand(lo, hi) -- random values¶
Different values each cycle. Use .over(N) to slow the change rate.
Using continuous patterns¶
Play them on a control path or use the @ operator:
# With @ operator
bass @ ("cutoff", krp.sine(200, 2000).over(4))
# With kr.play()
kr.play("bass/cutoff", krp.sine(200, 2000).over(4))
Modulation patterns¶
Modulation patterns generate continuous control values instead of note
triggers. All return Pattern objects and compose like any other pattern.
krp.sine(lo, hi) # sine sweep
krp.tri(lo, hi) # triangle sweep
krp.ramp(lo, hi) # linear ramp
krp.ramp_down(lo, hi) # ramp down
krp.square(lo, hi) # square wave
krp.exp(lo, hi) # exponential curve
krp.rand(lo, hi) # random values
All accept an optional steps=64 parameter to control resolution.
Using modulation patterns¶
Play them on a control path:
# Sine LFO on bass cutoff over 4 bars
kr.play("bass/cutoff", krp.sine(200.0, 2000.0).over(4))
# Triangle LFO on gain
kr.play("bass/gain", krp.tri(0.1, 0.5).over(8))
# One-shot ramp
kr.play("bass/cutoff", krp.ramp(200.0, 2000.0).over(4))
Or use the kr.mod() shorthand:
kr.mod("bass/cutoff", krp.sine(200.0, 2000.0), bars=4)
kr.mod("bass/gain", krp.tri(0.1, 0.5), bars=8)
Stop a modulation with kr.hush():
Playing patterns¶
@ operator (REPL-friendly)¶
The @ operator on a node handle plays a pattern immediately:
bass @ krp.seq("A2", "D3", None, "E2").over(2) # play pattern
bass @ "A2 D3 ~ E2" # play mini-notation string
bass @ ("cutoff", krp.sine(200, 2000).over(4)) # modulate a control
bass @ None # hush
kr.play(target, pattern) (explicit)¶
Binds a pattern to a node or control path. The pattern starts on the next cycle boundary:
kr.play("kick", krp.hit() * 4)
kr.play("bass", krp.seq("A2", "D3", None, "E2").over(2))
kr.play("bass/cutoff", krp.ramp(200.0, 2000.0).over(4))
Node handles¶
kr.node(), kr.node(), and kr.node() all return a handle:
bass = kr.node("bass", acid_bass, gain=0.3)
bass @ krp.seq("A2", "D3", None, "E2").over(2)
bass["cutoff"] = 1200
bass.fade("cutoff", 200, bars=4)
bass @ ("cutoff", krp.sine(400, 2000).over(4))
bass.mute()
Convenience kwargs¶
kr.play() accepts swing= as a keyword argument:
Pattern retrieval¶
Get the current pattern from a node to modify and replay:
p = kr.pattern("kick") # get current pattern by name
kr.play("kick", p.fast(2)) # modify and replay
# Or via a node handle:
kick = kr.node("drums/kick", kick_fn, gain=0.8)
p = kick.pattern()
kick.play(p.every(4, lambda p: p.reverse()))
How binding works¶
Pattern atoms like krp.note() and krp.hit() produce bare parameter
names (e.g., freq, gate). When you call kr.play("bass", pattern), the
system binds those bare names to the node's control namespace:
freqbecomesbass/freqgatebecomesbass/gatecutoffbecomesbass/cutoff
This means the same pattern can be reused on different nodes:
melody = krp.seq("A2", "D3", None, "E2").over(2)
kr.play("bass", melody)
kr.play("lead", melody) # same pattern, different node
Common recipes¶
4-on-the-floor kick¶
Offbeat hi-hat¶
Bass line with rests¶
Chord stabs¶
kr.node("rhodes", rhodes_fn, gain=0.3, count=4)
kr.play("rhodes", krp.note("A4", "C5", "E5") + krp.rest())