Skip to content

rpmessner/uzu_pattern

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UzuPattern

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.

Installation

def deps do
  [
    {:uzu_pattern, "~> 0.8.0"}
  ]
end

Quick Start

# 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))

Core Concepts

Patterns as Query Functions

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: reversed

Haps (Events)

Each query returns a list of Hap structs containing:

  • whole - The timespan of the complete event
  • part - The portion of the event within the queried range
  • value - Map with :sound, :sample, and effect parameters
  • context - 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)

Exact Rational Timing

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...

Modules

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

Signal Patterns

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 cycle

Available waveforms: sine, saw, isaw, tri, square, rand, irand

Effects

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))  # Modulated

Time Transformations

Pattern.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 speed

Structure

Pattern.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 4

Conditional

Pattern.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 events

Rhythm

Pattern.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 amount

Pattern Algebra

Combine 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 structures

Mini-Notation Features

The 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"

Examples

Layered Drum Pattern

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))

Evolving Pattern

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))

Stereo Arpeggio

UzuPattern.parse("arpy:0 arpy:1 arpy:2 arpy:3")
|> Pattern.jux(&Pattern.rev/1)
|> Pattern.lpf(Signal.tri() |> Signal.range(500, 3000))

Polyrhythmic Layers

Pattern.stack([
  UzuPattern.parse("bd(3,8)"),
  UzuPattern.parse("sd(5,8)"),
  UzuPattern.parse("hh*8")
])
|> Pattern.every(4, &Pattern.fast(&1, 2))

Related Projects

License

MIT License - see LICENSE for details.

About

transforms parsed uzu strings before scheduling

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages