Pattern library for TidalCycles/Strudel-style live coding in Elixir.
UzuPattern provides a query-based pattern system with time transformations, structure manipulation, conditional application, signal patterns for modulation, and effect parameters. It uses exact rational arithmetic for precise timing and integrates mini-notation parsing from UzuParser.
def deps do
[
{:uzu_pattern, "~> 0.8.0"}
]
end# Parse mini-notation into a pattern
pattern = UzuPattern.parse("bd sd hh cp")
# Query events for a specific cycle
haps = UzuPattern.query(pattern, 0)
# Apply transformations
pattern
|> Pattern.fast(2)
|> Pattern.rev()
|> Pattern.every(4, &Pattern.slow(&1, 2))Patterns are query functions, not static event lists. This enables cycle-aware transformations:
pattern = UzuPattern.parse("bd sd") |> Pattern.every(2, &Pattern.rev/1)
Pattern.query(pattern, 0) # Cycle 0: reversed
Pattern.query(pattern, 1) # Cycle 1: normal
Pattern.query(pattern, 2) # Cycle 2: reversedEach query returns a list of Hap structs containing:
whole- The timespan of the complete eventpart- The portion of the event within the queried rangevalue- Map with:sound,:sample, and effect parameterscontext- Source locations and tags
[hap | _] = UzuPattern.query(pattern, 0)
Hap.sound(hap) # => "bd"
Hap.sample(hap) # => 0
hap.part.begin # => Ratio.new(0, 1) (exact rational)
hap.part.end # => Ratio.new(1, 4)All time values use exact rational arithmetic via the Ratio library. This prevents rhythmic drift and enables precise pattern calculations:
# Times are exact fractions, not floats
hap.part.begin # => Ratio.new(1, 3) not 0.333...
# Conversion to floats only at the scheduling boundary
TimeSpan.begin_float(hap.part) # => 0.3333333...Functions are organized into logical groups:
| Module | Functions |
|---|---|
| Pattern | query, events, pure, silence, stack, cat, slowcat, fastcat, timeCat |
| Pattern.Time | fast, slow, early, late, ply, compress, zoom, linger, inside |
| Pattern.Structure | rev, palindrome, mask, degrade, jux, jux_by, superimpose, off, echo, striate, chop |
| Pattern.Conditional | every, sometimes, often, rarely, almostNever, almostAlways, iter, first_of, last_of, when_fn, chunk |
| Pattern.Effects | gain, pan, speed, cut, room, delay, lpf, hpf, set_param |
| Pattern.Rhythm | euclid, euclid_rot, swing, swing_by |
| Pattern.Signal | sine, saw, isaw, tri, square, rand, irand, range, rangex, segment |
| Pattern.Algebra | add, sub, mul, div_op, mod, pow, app_left, app_right, app_both |
Signals provide continuous values for modulating parameters:
# Modulate filter cutoff with sine wave (200-2000 Hz)
UzuPattern.parse("bd sd hh cp")
|> Pattern.lpf(Signal.sine() |> Signal.range(200, 2000))
# Random gain per cycle
UzuPattern.parse("bd sd")
|> Pattern.gain(Signal.rand() |> Signal.range(0.5, 1.0))
# Stereo panning sweep
UzuPattern.parse("arpy:0 arpy:1 arpy:2 arpy:3")
|> Pattern.pan(Signal.tri() |> Signal.range(-1, 1))
# Discretize signal into stepped values
Signal.saw() |> Signal.segment(8) # 8 discrete steps per cycleAvailable waveforms: sine, saw, isaw, tri, square, rand, irand
All effects accept static values or signal patterns:
pattern
|> Pattern.gain(0.8) # Static gain
|> Pattern.pan(-0.5) # Static pan (left)
|> Pattern.speed(2) # Double speed playback
|> Pattern.cut(1) # Cut group (monophonic)
|> Pattern.room(0.5) # Reverb send
|> Pattern.delay(0.3) # Delay send
|> Pattern.lpf(2000) # Low-pass filter
|> Pattern.hpf(200) # High-pass filter
|> Pattern.lpf(Signal.sine() |> Signal.range(200, 2000)) # ModulatedPattern.fast(pattern, 2) # Double speed
Pattern.slow(pattern, 2) # Half speed
Pattern.rev(pattern) # Reverse
Pattern.early(pattern, 0.25) # Shift earlier by 1/4 cycle
Pattern.late(pattern, 0.25) # Shift later by 1/4 cycle
Pattern.ply(pattern, 3) # Repeat each event 3 times
Pattern.compress(pattern, 0.25, 0.75) # Fit into time window
Pattern.zoom(pattern, 0.5, 1.0) # Extract and expand portion
Pattern.linger(pattern, 0.25) # Loop first 1/4 of pattern
Pattern.inside(pattern, 4, &Pattern.rev/1) # Apply transform at 4x speedPattern.stack([p1, p2, p3]) # Layer patterns (simultaneous)
Pattern.cat([p1, p2, p3]) # Sequence patterns (one per cycle)
Pattern.fastcat([p1, p2, p3]) # Sequence within one cycle
Pattern.slowcat([p1, p2, p3]) # Alias for cat
Pattern.timeCat([{3, p1}, {1, p2}]) # Weighted sequence
Pattern.palindrome(pattern) # Forward then backward
Pattern.jux(pattern, &Pattern.rev/1) # Stereo split with transform
Pattern.jux_by(pattern, 0.5, fun) # Partial stereo separation
Pattern.superimpose(pattern, fun) # Layer with transform
Pattern.off(pattern, 0.125, fun) # Offset copy with transform
Pattern.echo(pattern, 3, 0.25, 0.8) # Multiple echoes with decay
Pattern.striate(pattern, 4) # Slice into 4 parts
Pattern.chop(pattern, 4) # Chop each event into 4Pattern.every(pattern, 4, fun) # Apply every 4th cycle
Pattern.sometimes(pattern, fun) # 50% probability
Pattern.often(pattern, fun) # 75% probability
Pattern.rarely(pattern, fun) # 25% probability
Pattern.almostNever(pattern, fun) # 10% probability
Pattern.almostAlways(pattern, fun) # 90% probability
Pattern.iter(pattern, 4) # Rotate by 1/4 each cycle
Pattern.first_of(pattern, 4, fun) # Apply on first of every 4
Pattern.last_of(pattern, 4, fun) # Apply on last of every 4
Pattern.when_fn(pattern, pred, fun) # Apply when predicate true
Pattern.degrade(pattern) # Remove ~50% of events randomly
Pattern.degrade_by(pattern, 0.3) # Remove ~30% of eventsPattern.euclid(pattern, 3, 8) # 3 hits over 8 steps (Euclidean)
Pattern.euclid_rot(pattern, 3, 8, 2) # With rotation offset
Pattern.swing(pattern, 2) # Swing on 8th notes
Pattern.swing_by(pattern, 0.2, 2) # Swing with specific amountCombine patterns with arithmetic operations:
Pattern.add(p1, p2) # Add values
Pattern.mul(p1, p2) # Multiply values
Pattern.app_left(p1, p2) # Apply structure from left pattern
Pattern.app_right(p1, p2) # Apply structure from right pattern
Pattern.app_both(p1, p2) # Combine both structuresThe parser supports the full Strudel/Tidal mini-notation:
# Basic sequences and rests
"bd sd hh cp" # Four sounds
"bd ~ sd ~" # With rests
# Subdivisions
"bd [sd sd] hh" # Subdivision
"[[bd sd] hh]" # Nested
# Modifiers
"bd*4" # Repeat 4 times
"[bd sd]/2" # Slow by 2
"bd:3" # Sample 3
"bd?" # 50% probability
"bd?0.25" # 25% probability
"bd@2 sd" # Weight (bd twice as long)
"bd _ sd" # Elongation
# Polyphony
"[bd,sd,hh]" # Chord (simultaneous)
"{bd sd, hh hh hh}" # Polymetric
# Alternation
"<bd sd hh>" # Cycle through options
"bd|sd|hh" # Random choice
# Euclidean
"bd(3,8)" # 3 over 8
"bd(3,8,1)" # With rotation
# Parameters
"bd|gain:0.8|speed:2"kicks = UzuPattern.parse("bd ~ bd ~")
snares = UzuPattern.parse("~ sd ~ sd")
hats = UzuPattern.parse("[hh hh hh hh]")
Pattern.stack([kicks, snares, hats])
|> Pattern.lpf(Signal.sine() |> Signal.range(800, 4000))UzuPattern.parse("bd sd hh cp")
|> Pattern.every(4, &Pattern.rev/1)
|> Pattern.every(8, &Pattern.fast(&1, 2))
|> Pattern.sometimes(&Pattern.degrade/1)
|> Pattern.gain(Signal.saw() |> Signal.range(0.6, 1.0))UzuPattern.parse("arpy:0 arpy:1 arpy:2 arpy:3")
|> Pattern.jux(&Pattern.rev/1)
|> Pattern.lpf(Signal.tri() |> Signal.range(500, 3000))Pattern.stack([
UzuPattern.parse("bd(3,8)"),
UzuPattern.parse("sd(5,8)"),
UzuPattern.parse("hh*8")
])
|> Pattern.every(4, &Pattern.fast(&1, 2))MIT License - see LICENSE for details.