
Pattrns
- 17 installs
- 52 repo stars
- Updated March 4, 2026
- bfollington/terma
pattrns is a Claude Code skill for creating generative, emergent music with Pattrns, the Lua-based pattern sequencing engine for Renoise.
About
pattrns is a Claude Code skill for creating generative, algorithmic music with Pattrns, a Lua-based pattern engine for Renoise. It structures patterns through a Pulse to Gate to Event pipeline that separates rhythm from melody. A developer uses it to compose evolving breakbeats, generative melodies and harmonies, euclidean rhythms, and textural ambient patterns across genres like jungle, IDM, jazz, and ambient.
- Creates generative, emergent music with Pattrns in Renoise
- Uses a Pulse to Gate to Event pipeline separating rhythm from melody
- Supports euclidean rhythms and Tidal Cycles mini-notation
Pattrns by the numbers
- 17 all-time installs (skills.sh)
- Ranked #1,012 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
pattrns capabilities & compatibility
- Capabilities
- image generation
What pattrns says it does
Guide for creating generative, emergent music with Pattrns, the Lua-based pattern sequencing engine for Renoise.
Pattrns separates rhythm from melody through a **Pulse → Gate → Event** pipeline
pulse = pulse.euclidean(7, 16) -- 7 hits distributed in 16 steps
npx skills add https://github.com/bfollington/terma --skill pattrnsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 52 |
| Last updated | March 4, 2026 |
| Repository | bfollington/terma ↗ |
What it does
Compose generative, evolving music patterns in Renoise with the Lua-based Pattrns engine.
Who is it for?
Composing algorithmic, evolving patterns and euclidean rhythms in Renoise.
When should I use this skill?
You are composing generative or algorithmic music patterns in Renoise with Pattrns.
By the numbers
- Uses a 3-stage Pulse -> Gate -> Event pipeline
- Covers 5 genre families (breakbeat, IDM, jazz, industrial, ambient)
Files
Pattrns - Generative Music Creation
Overview
Pattrns is a Lua-based pattern generation engine for Renoise that enables algorithmic, generative music creation. Use this skill when working with Pattrns to create emergent, evolving musical patterns across genres including breakbeat/jungle, IDM, jazz, industrial/trip-hop, and ambient music.
Core Architecture: Pattrns separates rhythm from melody through a Pulse → Gate → Event pipeline:
- Pulse: Defines when events occur (rhythm)
- Gate: Optional filter for pulse values (probability, complexity)
- Event: Generates notes, chords, or sequences (melody/harmony)
This separation enables powerful generative techniques where rhythms and melodies can evolve independently and be recombined in creative ways.
When to Use This Skill
Invoke this skill when:
- Creating generative or algorithmic patterns in Renoise
- Working with euclidean rhythms or polyrhythms
- Designing evolving, emergent musical structures
- Generating breakbeats, complex drum patterns, or rhythmic variations
- Creating generative melodies, harmonies, or chord progressions
- Building textural, ambient, or atmospheric patterns
- Using Tidal Cycles mini-notation for pattern creation
- Developing patterns that resample and resequence for iterative composition
Quick Start
Basic Pattern Structure
Every Pattrns pattern follows this structure:
return pattern {
unit = "1/16", -- Time grid (1/16, 1/8, 1/4, bars, etc.)
resolution = 1, -- Multiplier (2/3 for triplets)
offset = 0, -- Delay pattern start
repeats = true, -- Loop pattern
pulse = {1, 0, 1, 1}, -- Static rhythm array
event = {"c4", "e4", "g4"} -- Static note sequence
}Essential Techniques at a Glance
Euclidean Rhythms (most common starting point):
pulse = pulse.euclidean(7, 16) -- 7 hits distributed in 16 stepsScale-Based Melodies:
local s = scale("c4", "minor")
event = s.notes -- Use scale notes as sequenceRandom Generation with Control:
event = function(context)
local notes = scale("c4", "pentatonic minor").notes
return notes[math.random(#notes)]
endTidal Cycles Mini-Notation:
return cycle("kd ~ sn ~, [hh hh]*4"):map({
kd = "c4 #1", -- Kick drum
sn = "c4 #2", -- Snare
hh = "c4 #3 v0.5" -- Hi-hat, quieter
})Core Workflow
1. Choose Musical Goal
Identify what to create:
- Rhythmic: Drum pattern, breakbeat, groove
- Melodic: Bassline, lead, arpeggio
- Harmonic: Chord progression, pad, texture
- Textural: Atmosphere, drone, evolving soundscape
2. Design Rhythm (Pulse)
Start with rhythmic foundation:
Static Arrays:
pulse = {1, 0, 1, 1, 0, 1, 0, 0} -- Hand-crafted rhythmEuclidean Distribution:
pulse = pulse.euclidean(5, 8) -- Algorithmic rhythmDynamic/Generative:
pulse = function(context)
return math.random() > 0.5 -- Probabilistic rhythm
endSubdivisions (Cramming):
pulse = {1, {1, 1, 1}, 1, 0} -- Quarter + triplet + quarter + rest3. Design Note Generation (Event)
Create melodic/harmonic content:
Static Sequence:
event = {"c4", "e4", "g4", "b4"}Scale-Based:
local s = scale("c4", "minor")
event = function(context)
return s.notes[math.imod(context.step, #s.notes)]
endChord Progressions:
local s = scale("c4", "minor")
event = sequence(
s:chord("i"), -- Tonic
s:chord("iv"), -- Subdominant
s:chord("v") -- Dominant
)Generative/Evolving:
event = function(init_context)
local state = initial_value
return function(context)
-- Update and use state to create evolution
state = state + delta
return generate_from(state)
end
end4. Add Variation (Optional Gate)
Filter or modify pulse triggers:
gate = function(context)
-- Higher probability on downbeats
local is_downbeat = (context.pulse_step - 1) % 4 == 0
local probability = is_downbeat and 0.9 or 0.3
return math.random() < probability
end5. Add Control (Parameters)
Enable live tweaking without code changes:
parameter = {
parameter.integer("variation", 1, {1, 4}),
parameter.number("density", 0.5, {0.0, 1.0}),
}
-- Access in functions
event = function(context)
local var = context.parameter.variation
-- Use var to select different patterns
end6. Iterate & Evolve
Workflow for Emergent Music: 1. Generate pattern → Test in Renoise 2. Render/bounce to audio 3. Resample and chop 4. Create new patterns using resampled material 5. Layer and arrange into parts 6. Repeat process for continuous evolution
Common Musical Tasks
Creating Breakbeats
Load references/genre_recipes.md and search for "Breakbeat" for complete examples.
Quick pattern:
-- Euclidean-based break
local kick = pulse.euclidean(4, 16)
local snare = pulse.euclidean(3, 16, 2)
local hats = pulse.from{1,0,1,0}:repeat_n(4)
-- Use cycle notation to combine
return cycle("[kd*16], [sn*16], [hh*16]"):map({
kd = function(ctx) return kick[math.imod(ctx.step,16)] and "c4 #1" end,
sn = function(ctx) return snare[math.imod(ctx.step,16)] and "c4 #2" end,
hh = function(ctx) return hats[math.imod(ctx.step,16)] and "c4 #3" end
})Generative Melodies
Load references/core_techniques.md and search for "Constrained Random Walk" or "Scale-Based Generation".
Quick pattern:
return pattern {
unit = "1/16",
pulse = pulse.euclidean(7, 16), -- Rhythmic interest
event = function(init_context)
local notes = scale("c4", "pentatonic minor").notes
local last_idx = 1
return function(context)
-- Prefer small intervals (smoother melody)
local next_idx = last_idx
while math.abs(next_idx - last_idx) > 2 do
next_idx = math.random(#notes)
end
last_idx = next_idx
return notes[next_idx]
end
end
}Chord Progressions
Load references/core_techniques.md and search for "Chord Progressions".
Quick pattern:
local s = scale("c4", "minor")
return pattern {
unit = "1/4", -- Whole notes
pulse = {1, 1, 1, 1},
event = sequence(
s:chord("i", 3), -- i minor
s:chord("iv", 3), -- iv minor
s:chord("v", 3), -- v minor
s:chord("i", 3) -- i minor
)
}Evolving Textures
Load references/genre_recipes.md and search for "Ambient" or "Textural".
Quick pattern:
return pattern {
unit = "1/16",
pulse = function(context)
return math.random() > 0.85 -- Sparse
end,
event = function(context)
local notes = scale("c4", "phrygian").notes
return note(notes[math.random(#notes)])
:volume(0.2 + math.random() * 0.3)
:delay(math.random() * 0.5)
:panning(math.random() * 2 - 1)
end
}Polyrhythms
Load references/core_techniques.md and search for "Polyrhythms".
Quick pattern:
-- 3:4:5 polyrhythm
cycle("[c4*3]/4, [e4*4]/4, [g4*5]/4")Key Techniques Reference
For detailed explanations and complete code examples, load these reference files:
Core Techniques (references/core_techniques.md)
Load when working with:
- Euclidean rhythms and pulse operations
- Randomization and controlled chaos
- Scale-based generation
- Pattern evolution and stateful generators
- Polyrhythms and unusual time signatures
- Note transformations
- Tidal Cycles mini-notation
- Texture generation
Search patterns:
grep -i "euclidean" references/core_techniques.mdgrep -i "random" references/core_techniques.mdgrep -i "scale" references/core_techniques.md
Genre-Specific Recipes (references/genre_recipes.md)
Load when creating:
- Breakbeat/Jungle/DnB: Complex breaks, swing, reese bass
- IDM/Experimental: Glitchy effects, algorithmic complexity
- Jazz: Swing, walking bass, chord substitutions
- Industrial/Trip-Hop: Heavy grooves, dark textures
- Ambient: Slow evolution, drones, sparse atmospheres
Search patterns:
grep -i "breakbeat\|jungle\|dnb" references/genre_recipes.mdgrep -i "idm\|experimental\|glitch" references/genre_recipes.mdgrep -i "jazz\|swing\|walking" references/genre_recipes.md
API Quick Reference (references/api_quick_reference.md)
Load when needing:
- Function signatures and parameters
- Available scale modes
- Context object properties
- Utility functions
- Common code patterns
- Workflow tips and gotchas
Pattern Templates
The assets/ directory contains starter templates for common patterns:
euclidean_drum.lua: Euclidean-based drum patterngenerative_melody.lua: Scale-based melody with constraintsevolving_chord.lua: Slowly evolving chord progressiontexture_cloud.lua: Sparse atmospheric texture
Copy and modify these as starting points.
Best Practices
Start Simple, Add Complexity
1. Begin with static pulse and static event 2. Make one dynamic (usually event first) 3. Add gate for probability/filtering 4. Add parameters for control 5. Layer multiple patterns
Controlled Randomness
Use seeded random for reproducibility:
event = function(init_context)
local rand = math.randomstate(12345) -- Consistent seed
return function(context)
return generate_with(rand)
end
endState Management
- Global state: Shared across all triggers (use sparingly)
- Local state (in generators): Per-trigger isolation (preferred)
- Use init function + inner function pattern for local state
Performance Optimization
- Cache expensive calculations in init functions
- Use local variables
- Avoid creating tables in inner loops
- Return
nilfor rests, not 0 or false
Tracker Context for Programmers
- Lua uses 1-based indexing:
array[1]is first element - Use
math.imod(step, #array)for array wrapping - Patterns run in musical time (beats/bars), not CPU time
- BPM and time signature from Renoise project settings
Troubleshooting
Pattern not triggering:
- Check
unitis appropriate for tempo - Verify
pulsereturns non-zero values - Check
gate(if present) isn't filtering all events
Notes out of key:
- Use
scale()to constrain note generation - Check root note and mode are correct
Pattern too predictable:
- Add randomization with
math.random() - Use euclidean rhythms with different parameters
- Implement constrained random walk for melodies
Pattern too chaotic:
- Use seeded random for consistency
- Add constraints to random generation
- Use scale-based generation for harmonic coherence
- Lower probability in gates
Lua errors:
- Check 1-based indexing
- Verify init function returns inner function for generators
- Use
math.imodfor array wrapping, not%
Generative Workflow Summary
The power of Pattrns for emergent music:
IDEA → ALGORITHM → PATTERN → EVENTS → AUDIO → RESAMPLE → NEW PATTERN1. Generate: Create algorithmic patterns with controlled randomness 2. Evolve: Use stateful generators to create evolving material 3. Render: Bounce patterns to audio in Renoise 4. Resample: Chop and manipulate rendered audio 5. Resequence: Create new patterns using resampled material 6. Iterate: Repeat for continuous evolution and emergence
This workflow enables creating "parts" that can be assembled into songs in Renoise or exported to traditional DAWs, with each iteration adding new layers of complexity and emergence.
Resources
references/
Detailed technical documentation and genre-specific recipes. Load as needed:
core_techniques.md: In-depth technique explanations and code patternsgenre_recipes.md: Complete examples for different musical stylesapi_quick_reference.md: Function signatures and API reference
assets/
Pattern templates ready to copy and modify:
- Starter templates for common musical tasks
- Genre-specific boilerplate patterns
-- Euclidean Drum Pattern Template
-- A versatile drum pattern using euclidean rhythm distribution
-- Modify the euclidean parameters (hits, steps, offset) to create variations
-- Define euclidean rhythms for each drum element
local kick = pulse.euclidean(4, 16) -- 4 kicks in 16 steps
local snare = pulse.euclidean(3, 16, 2) -- 3 snares in 16 steps, offset by 2
local hats = pulse.from{1,0,1,0}:repeat_n(4) -- 16th note hi-hats
local ghost_snare = pulse.euclidean(7, 16):map(function(k, v)
return v * (math.random() > 0.6 and 0.3 or 0) -- Sparse ghost notes at low volume
end)
-- Combine drums using cycle notation with mapping
return cycle("[kd*16], [sn*16], [gs*16], [hh*16]"):map({
kd = function(context)
return kick[math.imod(context.step, 16)] and "c4 #1"
end,
sn = function(context)
local v = snare[math.imod(context.step, 16)]
return v and note("c4 #2"):volume(v)
end,
gs = function(context)
local v = ghost_snare[math.imod(context.step, 16)]
return v > 0 and note("c4 #2"):volume(v)
end,
hh = function(context)
return hats[math.imod(context.step, 16)] and "c4 #3 v0.5"
end
})
-- Tips for customization:
-- 1. Change euclidean parameters: (hits, steps, offset)
-- - More hits = denser rhythm
-- - More steps = longer pattern
-- - Offset rotates the pattern
-- 2. Adjust instrument numbers (#1, #2, #3) to match your drum kit
-- 3. Modify volume values to change dynamics
-- 4. Add more drum elements by extending the pattern
-- Evolving Chord Progression Template
-- Creates a slowly changing chord progression with variation
-- Perfect for ambient, jazz, or evolving harmonic content
return pattern {
unit = "1/4", -- Quarter notes (adjust for tempo)
pulse = {1, 1, 1, 1}, -- Steady pulse
event = function(init_context)
-- Define chord progressions for different sections
local scale_root = "c4"
local scale_mode = "minor"
-- Try: "major", "dorian", "phrygian", "lydian", "mixolydian"
local s = scale(scale_root, scale_mode)
-- Multiple chord progressions to cycle through
local progressions = {
{s:chord("i", 3), s:chord("iv", 3), s:chord("v", 3), s:chord("i", 3)},
{s:chord("i", 3), s:chord("vi", 3), s:chord("iv", 3), s:chord("v", 3)},
{s:chord("i", 4), s:chord("iii", 4), s:chord("vi", 4), s:chord("ii", 4)},
}
local current_progression = 1
local evolution_counter = 0
return function(context)
evolution_counter = evolution_counter + 1
-- Change progression every 16 steps (4 bars of 4 beats)
if evolution_counter % 16 == 0 then
current_progression = (current_progression % #progressions) + 1
end
local progression = progressions[current_progression]
local chord_index = math.imod(context.step, #progression)
local current_chord = progression[chord_index]
-- Optional: add subtle variations
if math.random() > 0.8 then
-- Occasionally add a random inversion or voicing
return note(current_chord)
:transpose({math.random(-12, 12), 0, 0})
else
return current_chord
end
end
end
}
-- Tips for customization:
-- 1. Change scale_root and scale_mode for different tonalities
-- 2. Add more progressions to the progressions table
-- 3. Adjust evolution speed (% 16) for faster/slower changes
-- 4. Modify chord sizes: s:chord("i", 4) for 4-note chords
-- 5. Add dynamics: :volume(0.3 + math.random() * 0.4)
-- 6. Use different time units (1/8, bars) for different feels
-- Generative Melody Template
-- Creates a constrained random melody that stays in key
-- Uses a random walk with interval constraints for musicality
return pattern {
unit = "1/16",
-- Euclidean rhythm for rhythmic interest
pulse = pulse.euclidean(7, 16),
-- Generative melody with constraints
event = function(init_context)
-- Choose your scale
local notes = scale("c4", "pentatonic minor").notes
-- Or try: scale("c4", "minor").notes
-- scale("c4", "major").notes
-- scale("c4", "dorian").notes
-- scale("c4", "phrygian").notes
local last_index = 1
-- Optional: seed for reproducible randomness
-- local rand = math.randomstate(12345)
return function(context)
local next_index = last_index
-- Constrained random walk: prefer small intervals (max 2 steps)
-- This creates smoother, more musical melodies
while math.abs(next_index - last_index) > 2 do
next_index = math.random(#notes)
-- If using seeded random: next_index = rand(1, #notes)
end
last_index = next_index
return notes[next_index]
end
end
}
-- Tips for customization:
-- 1. Change scale root and mode to explore different tonalities
-- 2. Adjust interval constraint (> 2) for larger jumps
-- 3. Modify euclidean parameters for different rhythms
-- 4. Add note transformations like :volume(), :delay(), :panning()
-- 5. Use seeded random (uncomment lines) for consistent results
-- Texture Cloud Template
-- Creates sparse, atmospheric texture with random note events
-- Perfect for ambient, drone, or textural layers
return pattern {
unit = "1/16",
-- Sparse random triggers
pulse = function(context)
-- Adjust threshold for density (0.92 = ~8% trigger rate)
-- Lower number = more triggers
-- Higher number = fewer triggers (more sparse)
return math.random() > 0.92
end,
event = function(init_context)
-- Choose your scale
local notes = scale("c4", "phrygian").notes
-- Try: "pentatonic major", "pentatonic minor", "minor", "major"
-- Optional: seed for reproducibility
-- local rand = math.randomstate(54321)
return function(context)
-- Pick from upper register for atmospheric quality
local note_index = math.random(5, #notes)
-- If using seeded random: local note_index = rand(5, #notes)
return note(notes[note_index])
:volume(0.2 + math.random() * 0.3) -- Quiet, varying volume
:delay(math.random() * 0.8) -- Random timing offset
:panning(math.random() * 2 - 1) -- Spread across stereo field
end
end
}
-- Tips for customization:
-- 1. Adjust pulse threshold (0.92) for more/less density
-- 2. Change scale for different moods
-- 3. Modify note_index range: (1, #notes) for full range
-- 4. Adjust volume range for louder/quieter textures
-- 5. Change unit to "1/32" or "1/64" for finer-grained events
-- 6. Add instrument routing: :instrument(math.random(0, 7))
-- 7. For drones, use lower threshold and longer notes
-- Variations to try:
-- Dense texture: pulse threshold 0.7
-- Very sparse: pulse threshold 0.97
-- Low texture: note_index from (1, 4)
-- High texture: note_index from (8, #notes)
Pattrns API Quick Reference
Pattern Structure
return pattern {
unit = "1/16" | "1/8" | "1/4" | "bars" | "beats" | "ms" | "seconds",
resolution = 1, -- Multiplier (2/3 for triplets, 3/2 for dotted)
offset = 0, -- Delay start in time units
repeats = true, -- Loop pattern
pulse = {...} | function,
gate = function, -- Optional
event = {...} | function | cycle_string
}Time Units
"1/1","1/2","1/4","1/8","1/16","1/32","1/64""bars","beats"(alias for 1/4)"ms","seconds"(wall-clock time)
Pulse Functions
Euclidean Rhythms
pulse.euclidean(hits, steps) -- Distribute hits in steps
pulse.euclidean(hits, steps, offset) -- With rotationPulse Operations
pulse.from{1, 0, 1, 0} -- Create from array
pulse.new(len, fn) -- Generate from function
pulse.new(len, iterator) -- Generate from iterator
-- Transformations
:reverse() -- Reverse order
:rotate(n) -- Shift by n steps
:repeat_n(n) -- Repeat n times
:spread(factor) -- Stretch/compress
:take(n) -- First n elements
:map(fn) -- Transform each element
:distributed(n, len) -- Alternative distribution
-- Combinations
pulse1 + pulse2 -- Concatenate
pulse * n -- Repeat n timesScale Functions
Creating Scales
scale(root, mode) -- Create scale
scale("c4", "major")
scale("c4", "minor")
scale("c4", "dorian")
scale("c4", "phrygian")
scale("c4", "lydian")
scale("c4", "mixolydian")
scale("c4", "pentatonic major")
scale("c4", "pentatonic minor")
scale("c4", "chromatic")Scale Properties
s.notes -- Array of MIDI note numbers
s.root -- Root note
s.mode -- Scale mode nameScale Methods
s:chord(degree) -- Chord from scale degree
s:chord(degree, num_notes) -- Chord with n notes
s:chord("i") -- Roman numeral (i-vii)
s:chord("I") -- Uppercase for major
s:notes_iter() -- Iterator over notesNote Functions
Creating Notes
note("c4") -- MIDI note name
note(60) -- MIDI note number
note{"c4", "e4", "g4"} -- Chord
chord("c4", "maj") -- Named chord
chord("c4", "min")
chord("c4", "dom7")
chord("c4", "maj7")Note Transformations
note("c4"):transpose(n) -- Transpose by semitones
note("c4"):volume(v) -- Set volume (0.0-1.0)
note("c4"):amplify(factor) -- Multiply volume
note("c4"):panning(p) -- Pan (-1.0 to 1.0)
note("c4"):delay(d) -- Timing delay (0.0-1.0)
note("c4"):instrument(n) -- Instrument/sample number
-- Chord-specific
note(chord):transpose({12,0,0}) -- Transpose individual notesSequence Functions
sequence("c4", "e4", "g4") -- Static sequence
sequence(note1, note2, ...) -- Note objects
-- Transformations (same as note)
:transpose(n)
:volume(v)
:amplify(factor)
:panning(p)
:delay(d)
:instrument(n)Cycle (Tidal Mini-Notation)
Creating Cycles
cycle("c4 e4 g4") -- Simple sequence
cycle("c4 e4 g4, d4 f4 a4") -- Polyphonic (parallel)
cycle("[c4 e4] [g4 b4]") -- Groups
cycle("c4*4") -- Repeat
cycle("c4 ~") -- Rest
cycle("c4 e4_") -- Elongate
cycle("c4?0.5") -- 50% probability
cycle("<c4 e4 g4>") -- Alternate each cycle
cycle("c4|e4|g4") -- Random choice
cycle("c4(3,8)") -- Euclidean (3 hits in 8)Cycle Attributes
cycle("c4:v0.5") -- Volume
cycle("c4:p-1") -- Pan
cycle("c4:d0.2") -- Delay
cycle("c4:2") -- Instrument
cycle("c4:v0.5:p-0.5:2") -- CombinedCycle Mapping
cycle("kd ~ sn ~"):map({
kd = "c4 #1", -- Map to note
sn = function(ctx) -- Map to function
return note("c4 #2"):volume(0.8)
end
})Context Object
Available in gate and event functions:
-- Timing info
context.beats_per_min -- Current BPM
context.beats_per_bar -- Time signature
context.step -- Global step counter
context.pulse_step -- Pulse step counter
-- Pulse info
context.pulse_value -- Current pulse value (0.0-1.0)
-- Parameters (if defined)
context.parameter.param_name -- Access parameter valuesParameters
parameter = {
parameter.integer("name", default, {min, max}),
parameter.number("name", default, {min, max}),
parameter.boolean("name", default),
}Access in functions: context.parameter.name
Random Functions
math.random() -- 0.0 to 1.0
math.random(n) -- 1 to n (integer)
math.random(min, max) -- min to max (integer)
math.randomseed(seed) -- Set global seed
math.randomstate(seed) -- Create local RNG
-- Example usage
local rand = math.randomstate(1234)
rand() -- 0.0 to 1.0
rand(10) -- 1 to 10Utility Functions
math.imod(value, modulus) -- 1-based modulo for arrays
table.contains(tbl, value) -- Check if value in table
table.find(tbl, value) -- Get index of valueCommon Patterns
Static Pulse, Dynamic Event
pulse = pulse.euclidean(7, 16),
event = function(context)
return scale("c4", "minor").notes[math.random(7)]
endDynamic Pulse, Static Event
pulse = function(context)
return math.random() > 0.5
end,
event = {"c4", "e4", "g4"}Stateful Generator
event = function(init_context)
local state = initial_value
return function(context)
-- Update state
state = state + 1
-- Use state to generate output
return something_based_on(state)
end
endProbability Gate
gate = function(context)
return context.pulse_value > math.random()
endPosition-Dependent Pattern
event = function(context)
local position = context.pulse_step % 16
if position < 4 then
return pattern_a
else
return pattern_b
end
endWorkflow Tips
1. Start simple: Static pulse + static event 2. Add variation: Make one dynamic (usually event first) 3. Add complexity: Make both dynamic or add gate 4. Add control: Use parameters for live tweaking 5. Optimize: Cache calculations in init functions 6. Test: Use repeats = false for testing finite patterns
Common Gotchas
- Lua is 1-indexed:
array[1]is first element, notarray[0] - Use math.imod: For wrapping array access with proper 1-based behavior
- Global vs local state: Use generators for per-trigger state isolation
- Return nil for rests: Don't return 0 or false, return
nilor{} - Pulse values: Can be binary (0/1) or weighted (0.0-1.0)
- Context availability: Init functions don't have full context, inner functions do
Pattrns Core Techniques Reference
Architecture Overview
Three-Stage Pipeline: Pulse → Gate → Event
Pattrns separates rhythm from melody through a three-stage architecture:
Pulse (Rhythm Generator)
- Defines when events occur
- Can be static arrays:
{1, 0, 1, 1}or dynamic functions - Supports subdivisions (cramming):
{1, {1, 1, 1}}= quarter note then triplet - Pulse values: 0/1 (binary) or 0.0-1.0 (weighted/probabilistic)
- Default: continuous pulse of 1's
Gate (Pulse Filter)
- Optional filter between pulse and event
- Default: threshold gate (passes values > 0)
- Use for probability-based triggering, complex filtering, dynamic pattern morphing
- Accesses
context.pulse_valueto make decisions
Event (Note Generator)
- Produces actual musical output (notes, chords)
- Can be static sequences, static chords, dynamic functions, or Tidal Cycles mini-notation
Basic Pattern Structure
return pattern {
unit = "1/16", -- Time grid
resolution = 1, -- Multiplier (e.g., 2/3 for triplets)
offset = 0, -- Delay start
repeats = true, -- Loop forever
pulse = {1, 0, 1, 1}, -- Rhythm
gate = function(ctx) ... end, -- Optional filter
event = {"c4", "e4", "g4"} -- Notes
}Euclidean Rhythms
The core algorithmic rhythm tool. Distributes N hits evenly across K steps.
pulse.euclidean(3, 8) -- {1,0,0,1,0,0,1,0} (tresillo)
pulse.euclidean(5, 8) -- {1,0,1,0,1,0,1,1} (cinquillo)
pulse.euclidean(7, 16, 2) -- 7 hits in 16 steps, rotate by 2Musical Applications:
- (3,8): African tresillo rhythm
- (5,8): Cuban cinquillo
- (7,16): Complex polyrhythm for breakbeat variation
- Combine multiple:
pulse.euclidean(3,8) + pulse.euclidean(5,8)(Schillinger technique)
Euclidean with Notes:
local s = scale("c3", "minor")
local notes =
pulse.from(s:chord("i", 3)):euclidean(8) +
pulse.from(s:chord("vi", 3)):euclidean(8, 1):reverse() +
pulse.from(s:chord("v", 3)):euclidean(8)Pulse Operations
Transformations:
reverse(): Invert orderrotate(n): Shift left/rightrepeat_n(n): Duplicate patternspread(factor): Expand/compress timingtake(n): First n elementsmap(fn): Transform each elementdistributed(n, len): Similar to Euclidean, different algorithm
Combinations:
+: Concatenate patterns*: Repeat pattern- Example:
pulse.from{1,0} * 3 + {1,1}={1,0,1,0,1,0,1,1}
Randomization & Controlled Chaos
Seeded Randomization
Uses Xoshiro256PlusPlus RNG for cross-platform consistency:
-- Global seed (affects all random calls)
math.randomseed(12345)
-- Local generator (independent stream)
event = function(init_context)
local rand = math.randomstate(1234) -- Consistent random sequence
local notes = scale("c5", "minor").notes
return function(context)
return notes[rand(1, #notes)]
end
endProbability Gates
gate = function(init_context)
local rand = math.randomstate(12366)
return function(context)
return context.pulse_value > rand() -- pulse value as probability
end
endConstrained Random Walk
event = function(init_context)
local notes = scale("c4", "pentatonic minor").notes
local last_index = 1
return function(context)
local next_index = last_index
-- Prefer small intervals for smoother melodies
while math.abs(next_index - last_index) > 2 do
next_index = math.random(#notes)
end
last_index = next_index
return notes[next_index]
end
endScale-Based Generation
Random Notes from Scale
local s = scale("c4", "minor")
event = function(context)
return s.notes[math.random(#s.notes)]
endChord Progressions from Scale Degrees
local cmin = scale("c4", "minor")
event = sequence(
cmin:chord("i"), -- C minor (tonic)
cmin:chord("iv"), -- F minor (subdominant)
cmin:chord("v"), -- G minor (dominant)
note(cmin:chord("i")):transpose({-12}) -- C minor, bass down
)Using Cycles for Progressions
cycle("i iv v i"):map(function(init_context)
local s = scale("c4", "minor")
return function(context, value)
return s:chord(value) -- Roman numeral chord degrees
end
end)Pattern Evolution
Stateful Generators
event = function(init_context)
local counter = 0
local variation = 1
return function(context)
counter = counter + 1
-- Change variation every 16 steps
if counter % 16 == 0 then
variation = (variation % 4) + 1
end
local note_sets = {
{"c4", "e4", "g4"},
{"d4", "f4", "a4"},
{"e4", "g4", "b4"},
{"f4", "a4", "c5"}
}
local notes = note_sets[variation]
return notes[math.imod(context.step, #notes)]
end
endTime-Based Evolution
event = function(context)
-- Melody gets higher as pattern progresses
local octave = math.floor(context.step / 32)
local base_note = scale("c", "minor").notes[math.imod(context.step, 7)]
return base_note + (octave * 12)
endPolyrhythms & Unusual Time Signatures
Via Cycles
-- 4 over 3 polyrhythm
cycle("[C3 D#4 F3 G#4], [[D#3 G4 F4]/64]*63")Via Subdivisions
pulse = {1, {1, 1, 1}, 1, {1, 1}} -- 4 + triplet + 4 + dupletVia Resolution
unit = "1/4",
resolution = 5/4 -- 5 quarter notes in space of 4 (quintuplet)Note Transformations
-- Single transformations
note("c4"):transpose(12) -- Up one octave
note("c4"):volume(0.5) -- Half volume
note("c4"):amplify(1.5) -- 150% of current volume
note("c4"):panning(-1) -- Hard left
note("c4"):delay(0.1) -- Slight timing delay
note("c4"):instrument(2) -- Route to instrument 2
-- Chord transformations
note("c4'min"):transpose({12, 0, 0}) -- 1st inversion
-- Sequence transformations
sequence("c4", "e4", "g4"):amplify(0.5)Tidal Cycles Mini-Notation
Key Symbols
(space): Separates steps,: Parallel patterns (polyphony)< >: Alternates between values each cycle|: Random choice*N: Repeat N times_: Elongate/hold~: Rest(n,k,o): Euclidean rhythm (n hits in k steps, o offset)?p: Probability (e.g.,c4?0.3= 30% chance)
Pattrns-Specific Syntax
: sets attributes (instrument/volume/pan/delay):
-- Attribute syntax
cycle("c4:v0.5:p-0.5") -- C4, volume 0.5, pan left
cycle("c4:2") -- C4 on instrument 2
-- Multi-channel drums with mapping
cycle("[kd ~]*2 ~ [~ kd] ~, [~ sn]*2, [<oh hh>*12]")
:map({
kd = "c4 #11",
sn = "c4 #5",
oh = "c4 #7",
hh = "c4 #6 v0.5"
})Texture Generation
Dense Polyrhythmic Layers
-- Many euclidean patterns = complex texture
cycle("[c4(3,8)], [e4(5,13)], [g4(7,16)]")
:map(function(context, value)
return note(value)
:volume(0.3 + math.random() * 0.4)
:delay(math.random() * 0.3)
:panning(math.random() * 2 - 1)
end)Granular-Style Event Clouds
unit = "1/64",
pulse = function(context)
return math.random() > 0.7 -- Sparse random
end,
event = function(context)
local notes = scale("c6", "pentatonic").notes
return note(notes[math.random(#notes)])
:volume(0.1 + math.random() * 0.3)
:delay(math.random())
endCommon Pattern Recipes
Probability-Based Event Filtering
gate = function(context)
-- Higher probability on downbeats
local is_downbeat = (context.pulse_step - 1) % 4 == 0
local probability = is_downbeat and 0.9 or 0.3
return math.random() < probability
endCombining Structure + Chaos
-- Structured rhythm, random notes
pulse = pulse.euclidean(7, 16),
event = function(context)
return scale("c4", "minor").notes[math.random(7)]
endCycle-Based Variation
-- Alternate between patterns
event = cycle("<[c4 e4 g4] [d4 f4 a4]>")
-- Random choice
event = cycle("[c4 e4 g4]|[d4 f4 a4]|[e4 g4 b4]")Tracker-Specific Considerations
Lua Quirks
1-Based Indexing:
local notes = {"c4", "e4", "g4"}
return notes[1] -- "c4", not "e4"!Array Wrapping:
-- Use math.imod for proper 1-based wrapping
local index = math.imod(context.step, #notes)State Management
- Global state: Shared across all triggers
- Local state (in generators): Per-trigger isolation
- Use generators when you need separate state per note
Performance Tips
- Cache expensive calculations in init functions
- Use local variables
- Avoid creating garbage (tables) in inner loops
- Return
nilor{}for rests
Genre-Specific Recipes for Pattrns
Breakbeat / Jungle / Drum & Bass
Complex Break Construction
-- Euclidean-based amen break variation
local kick = pulse.euclidean(4, 16)
local snare = pulse.euclidean(3, 16, 2):reverse()
local hats = pulse.from{1,0,1,0}:repeat_n(4)
local ghost_snare = pulse.euclidean(7, 16):map(function(k, v)
return v * (math.random() > 0.6 and 0.3 or 0) -- Sparse, quiet
end)
return cycle("[kd*16], [sn*16], [gs*16], [hh*16]"):map({
kd = function(context)
return kick[math.imod(context.step,16)] and "c4 #1"
end,
sn = function(context)
local v = snare[math.imod(context.step,16)]
return v and note("c4 #2"):volume(v)
end,
gs = function(context)
local v = ghost_snare[math.imod(context.step,16)]
return v > 0 and note("c4 #2"):volume(v)
end,
hh = function(context)
return hats[math.imod(context.step,16)] and "c4 #3 v0.5"
end
})Swing & Groove
-- Triplet feel for jungle swing
unit = "1/16",
resolution = 2/3, -- Triplet subdivision
event = function(context)
-- Add subtle random timing humanization
return note("c4"):delay(0.05 * math.random())
endReese Bassline
-- Random walk bassline with jungle-style rhythm
return pattern {
unit = "1/16",
pulse = pulse.euclidean(7, 16, 3),
event = function(init_context)
local last_note = 1
local rand = math.randomstate(12345)
local bass_notes = scale("c2", "minor").notes
return function(context)
-- Occasional jumps, mostly stepwise
if rand() > 0.8 then
last_note = rand(#bass_notes)
else
last_note = math.imod(last_note + rand(-1, 1), #bass_notes)
end
return bass_notes[last_note]
end
end
}Break Chopping Workflow
1. Generate complex drum pattern with variations 2. Render to audio in Renoise 3. Use timing variations via :delay() for humanization 4. Layer multiple instances with different random seeds 5. Resample and chop in pattern editor
IDM / Experimental
Glitchy Effects via Probability
-- Irregular, glitchy rhythm
pulse = function(context)
if context.pulse_step % 7 == 0 then
return math.random()
end
-- Sudden subdivisions
if math.random() > 0.9 then
return {1, 1, 1, 1}
end
return math.random() > 0.7
endUnusual Structures (Prime Numbers)
pulse = function(context)
local primes = {2, 3, 5, 7, 11, 13, 17, 19, 23}
return table.contains(primes, context.pulse_step % 24)
endAlgorithmic Complexity (Fibonacci)
event = function(init_context)
local fib = {1, 1, 2, 3, 5, 8, 13, 21}
local scale_notes = scale("c4", "chromatic").notes
return function(context)
local index = fib[math.imod(context.step, #fib)]
return scale_notes[math.imod(index, #scale_notes)]
end
endPolymetric Madness
-- 5:7:3 polyrhythm
cycle("[c4*5]/4, [e4*7]/4, [g4*3]/4")Evolving Glitch Textures
-- Dense, chaotic event cloud
return pattern {
unit = "1/64",
pulse = function(context)
-- Clustered random triggers
local cluster = math.floor(context.pulse_step / 16) % 2
return math.random() > (cluster == 0 and 0.9 or 0.5)
end,
event = function(context)
local notes = scale("c4", "chromatic").notes
return note(notes[math.random(#notes)])
:volume(0.2 + math.random() * 0.5)
:delay(math.random())
:panning(math.random() * 2 - 1)
end
}Jazz / Improvisation
Swing Implementation
-- Classic jazz swing
unit = "1/8",
resolution = 2/3, -- Triplet subdivision = swing feel
event = sequence("c4", "e4", "g4", "b4")Walking Bassline
event = function(init_context)
local progression = {
scale("c3", "mixolydian").notes, -- C7
scale("f3", "mixolydian").notes, -- F7
scale("g3", "mixolydian").notes, -- G7
scale("c3", "major").notes -- Cmaj
}
local chord_index = 1
local step_in_bar = 0
return function(context)
step_in_bar = (step_in_bar + 1) % 4
if step_in_bar == 0 then
chord_index = math.imod(chord_index + 1, #progression)
end
local current_scale = progression[chord_index]
-- Stepwise motion through chord tones
return current_scale[math.imod(step_in_bar, #current_scale)]
end
endHarmonic Substitutions
event = function(context)
local chords = {
scale("c4", "major"):chord(1),
scale("c4", "major"):chord(5)
}
-- Randomly substitute with tritone sub
if math.random() > 0.8 then
return chord("f#4", "dom7") -- Tritone substitution
else
return chords[math.imod(context.step, #chords)]
end
endImprovisation Simulation (Lick Library)
event = function(init_context)
local lick_library = {
{48, 52, 55, 60}, -- Lick 1
{48, 50, 51, 55}, -- Lick 2
{55, 52, 48, 47} -- Lick 3
}
local current_lick = 1
local lick_position = 1
return function(context)
-- Occasionally change lick
if context.step % 8 == 0 and math.random() > 0.6 then
current_lick = math.random(#lick_library)
lick_position = 1
end
local lick = lick_library[current_lick]
local note = lick[lick_position]
lick_position = math.imod(lick_position + 1, #lick)
return note
end
endComping Patterns
-- Sparse chord voicings with syncopation
return pattern {
unit = "1/8",
resolution = 2/3, -- Swing
pulse = {0, 0.8, 0, 0.6, 0, 0, 0.9, 0},
event = function(init_context)
local voicings = {
scale("c4", "major"):chord(1, 4), -- Cmaj7
scale("c4", "dorian"):chord(2, 4), -- Dm7
scale("c4", "major"):chord(5, 4), -- G7
}
return function(context)
local chord_idx = math.floor(context.step / 8) % #voicings + 1
return note(voicings[chord_idx])
:volume(context.pulse_value)
end
end
}Industrial / Trip-Hop
Heavy, Sparse Grooves
-- 90s trip-hop style
return pattern {
unit = "1/16",
pulse = {1, 0, 0, {0, 0, 1}, 0, 1, 0, 0},
event = sequence(
note("c2"):volume(1.0), -- Kick
note("c3"):volume(0.8), -- Snare
note("g2"):volume(0.4) -- Ghost note
)
}Industrial Noise Textures
-- Sparse industrial texture layer
return pattern {
unit = "1/64",
pulse = function(context)
return math.random() > 0.95 -- Very sparse
end,
event = function(context)
return note(36 + math.random(0, 24))
:volume(0.1 + math.random() * 0.2)
:delay(math.random())
:panning(math.random() * 2 - 1)
end
}Sample Variation (Weighted Random)
event = function(context)
local samples = {0, 1, 2, 3, 4, 5}
local weights = {0.4, 0.3, 0.2, 0.05, 0.03, 0.02}
local r = math.random()
local sum = 0
for i, weight in ipairs(weights) do
sum = sum + weight
if r < sum then
return note("c4"):instrument(samples[i])
end
end
endDark, Heavy Bassline
-- Slow, menacing bass
return pattern {
unit = "1/8",
pulse = pulse.euclidean(5, 16),
event = function(init_context)
local progression = {
scale("c2", "phrygian").notes,
scale("d#2", "phrygian").notes,
}
return function(context)
local section = math.floor(context.step / 16) % 2 + 1
local notes = progression[section]
return notes[math.imod(context.step, #notes)]
end
end
}Ambient / Textural
Slow Evolution
-- Very slow, evolving texture
return pattern {
unit = "bars",
resolution = 4, -- Every 4 bars
event = function(init_context)
local evolution = 0
return function(context)
evolution = evolution + 0.1
local base_scale = scale("c3", "pentatonic major").notes
-- Notes drift upward slowly
local octave_shift = math.floor(evolution / 2) * 12
local note_choice = base_scale[math.imod(context.step, #base_scale)]
return note(note_choice + octave_shift):volume(0.3)
end
end
}Dense Polyphonic Pads
-- Overlapping chord tones
event = function(context)
local chord_notes = scale("c3", "major"):chord(
math.imod(context.step, 7), 4
)
return note(chord_notes)
:volume(0.2 + 0.1 * math.random())
:delay(math.random() * 0.5) -- Desynchronize
endGenerative Drone
-- Breathing drone with slow modulation
return pattern {
unit = "seconds",
resolution = 8, -- Every 8 seconds
pulse = function(context)
return math.random() > 0.3 -- Some rests
end,
event = function(context)
local drone_notes = {48, 55, 60, 67} -- C, G, C, G
return note(drone_notes)
:volume(0.1 + 0.1 * math.sin(context.pulse_step * 0.1))
:panning(math.sin(context.pulse_step * 0.05))
end
}Sparse Atmospheric Events
-- Rare, atmospheric note events
return pattern {
unit = "1/16",
pulse = function(context)
return math.random() > 0.92 -- Very rare triggers
end,
event = function(context)
local scale_notes = scale("c4", "phrygian").notes
-- High notes, long delays
return note(scale_notes[math.random(5, #scale_notes)])
:volume(0.2)
:delay(math.random() * 0.8)
:panning(math.random() * 2 - 1)
end
}Evolving Harmonic Field
-- Slowly shifting chord progression
event = function(init_context)
local phase = 0
local scales = {
scale("c3", "major"),
scale("c3", "dorian"),
scale("c3", "phrygian"),
scale("c3", "lydian"),
}
return function(context)
phase = phase + 0.01
-- Evolve through scales
local scale_idx = math.floor(2 + 2 * math.sin(phase))
local current_scale = scales[scale_idx]
-- Random chord from current scale
local degree = math.random(1, 7)
return current_scale:chord(degree, 3)
end
endCross-Genre Techniques
Markov Chain (Probability Matrix)
-- State-based note selection
event = function(init_context)
local transition_matrix = {
[1] = {[1]=0.1, [2]=0.3, [3]=0.4, [4]=0.2},
[2] = {[1]=0.2, [2]=0.2, [3]=0.3, [4]=0.3},
[3] = {[1]=0.3, [2]=0.3, [3]=0.1, [4]=0.3},
[4] = {[1]=0.2, [2]=0.2, [3]=0.2, [4]=0.4},
}
local scale_notes = scale("c4", "minor").notes
local current_note = 1
return function(context)
local r = math.random()
local sum = 0
for next_note, prob in pairs(transition_matrix[current_note]) do
sum = sum + prob
if r < sum then
current_note = next_note
return scale_notes[current_note]
end
end
end
endGenetic Evolution (Fitness-Based)
-- Evolve pattern toward a target
event = function(init_context)
local target_melody = {60, 64, 67, 72}
local population_size = 10
-- Initialize population
local population = {}
for i = 1, population_size do
population[i] = {}
for j = 1, 4 do
population[i][j] = 60 + math.random(0, 12)
end
end
local generation = 0
return function(context)
if context.step % 16 == 0 then
-- Evaluate fitness and evolve
generation = generation + 1
-- (Add selection, crossover, mutation logic here)
end
local best = population[1]
local note_idx = math.imod(context.step, #best)
return best[note_idx]
end
endBouncing Ball Physics
-- Physical simulation as rhythm
pulse = function(init_context)
local distance = 100
local speed = 1
local step = 0
local step_size = distance / speed
return function(context)
step = step + 1
if step >= step_size then
distance = distance * 0.8 -- Decay
step_size = distance / speed
step = 0
return 1
end
return 0
end
endRelated skills
FAQ
What is the core architecture?
Pattrns uses a Pulse (rhythm), Gate (filter), and Event (melody/harmony) pipeline that separates rhythm from melody.
What genres does it cover?
It covers breakbeat/jungle/DnB, IDM/experimental, jazz, industrial/trip-hop, and ambient styles.