Skip to content

Instantly share code, notes, and snippets.

@d0lfyn
Last active January 4, 2021 19:31
Show Gist options
  • Save d0lfyn/3fd36359a6a9dac49146f74fa0a60558 to your computer and use it in GitHub Desktop.
Save d0lfyn/3fd36359a6a9dac49146f74fa0a60558 to your computer and use it in GitHub Desktop.
Patterns for Sonic Pi
##| patterns
##| environment
use_bpm 480;
use_random_seed 0;
LOGGING = false;
##| domain
BASE_NOTE = :c3;
NUM_OCTAVES = 3;
DOMAIN = (scale BASE_NOTE, :major, num_octaves: NUM_OCTAVES);
##| notes
POSITION = 0;
DURATION = 1;
##| patterns
PATTERN_START_LENGTH = 4; ##| int [0,)
PATTERN_MAX_LENGTH = -1; ##| int [-1,) | -1 for infinitely long pattern
PATTERN_EVOLUTION_FACTOR = 60; ##| int [1,) != 0 | lower means more frequent mutations to pattern
PATTERN_GENERATION_FACTOR = 60; ##| int [-1,) != 0 | -1 for no new patterns, lower means more frequent pattern generation
PATTERN_GROWTH_FACTOR = 2; ##| int [0,) | lower means fewer notes appended per pattern development
PATTERN_TIME_TRANSFORM_FACTOR = 2; ##| int (0,) | lower means less transformation applied per pattern time transform
PATTERNS_MAX_NUMBER = 64; ##| int [-1,) | -1 for infinitely many patterns
DO_TIME_TRANSFORM = false;
##| timing
set :ticks, 0;
EVENT_GAP = 0.5; ##| (0,1]
LONGEST_DURATION = 2; ##| int [1,)
LONGEST_PAUSE = 8; ##| int [2,)
PULSE = 8; ##| (0,) | best if int
RHYTHM_AMP = 0.5; ##| [0,)
##| performance
ECHO_DECAY_MIN = 5; ##| (0,ECHO_DECAY_MAX]
ECHO_DECAY_MAX = LONGEST_DURATION * 10; ##| [ECHO_DECAY_MIN,)
ECHO_MIX_MIN = 0.1; ##| [0,1]
ECHO_MIX_MAX = 0.7; ##| [0,1]
ECHO_PHASE = 1; ##| [0,)
RELEASE_DURATION = 1; ##| [0,)
NUM_VOICES = 3; ##| i think 3 voices is optimal given the lack of constraints
##| Functions
##| general
define :generateDurations do
durations = [];
for i in 1..LONGEST_DURATION
durations.push i;
end
return durations;
end
define :generateMelodicIntervals do |pPosition|
intervals = [];
for i in 0..5
intervals.push i if (pPosition + i) <= getTopPosition;
intervals.push -i if (pPosition - i) >= 0;
end
return intervals;
end
define :generatePositions do
positions = [];
for i in 0..getTopPosition
positions.push i;
end
return positions;
end
define :getTopPosition do
return (DOMAIN.length - 1);
end
##| notes
define :getNextNote do |pLastNote|
return [(pLastNote[POSITION] + generateMelodicIntervals(pLastNote[POSITION]).choose),
generateDurations.choose];
end
define :getRandomNote do
return [generatePositions.choose, generateDurations.choose];
end
define :invertNote do |pNote|
return [(getTopPosition - pNote[POSITION]), pNote[DURATION]];
end
define :timeTransformNote do |pNote, pTransformation|
return [pNote[POSITION], (pNote[DURATION] * (1 + pTransformation))];
end
define :transposeNote do |pNote, pSteps|
return [(pNote[POSITION] + pSteps), pNote[DURATION]];
end
##| patterns
define :developPattern do |pPattern, pTimes|
pattern = pPattern.take(pPattern.length);
times = pTimes;
if (PATTERN_MAX_LENGTH >= 0) && (pattern.length >= PATTERN_MAX_LENGTH)
return pattern;
end
if pattern.length == 0
if LOGGING
puts "new pattern";
end
pattern.push getRandomNote;
times -= 1;
if times == 0
return pattern;
end
end
if LOGGING
puts "developing existing pattern";
end
for i in 0...times
lastNote = pattern[pattern.length - 1];
pattern.push getNextNote(lastNote);
end
return pattern;
end
define :evolvePattern do |pPattern|
if LOGGING
puts "pattern evolving";
end
if pPattern.length == 0
return developPattern(pPattern, dice(PATTERN_GROWTH_FACTOR));
end
case dice(4)
when 1
return transposePattern(pPattern, [-getPatternTrough(pPattern),
(getTopPosition - getPatternPeak(pPattern))].choose);
when 2
return developPattern(pPattern, dice(PATTERN_GROWTH_FACTOR));
when 3
return retrogressPattern(pPattern);
when 4
return invertPattern(pPattern);
##| when 5
##| if DO_TIME_TRANSFORM
##| return timeTransformPattern(pPattern);
##| else
##| return pPattern;
##| end
end
end
define :generatePattern do
return developPattern([], PATTERN_START_LENGTH);
end
define :getPatternDurations do |pPattern|
return pPattern.map {|n| n[DURATION]};
end
define :getPatternPeak do |pPattern|
highest = 0;
pPattern.each {|n| highest = (n[POSITION] > highest) ? n[POSITION] : highest};
return highest;
end
define :getPatternPitches do |pPattern|
return pPattern.map {|n| DOMAIN[n[POSITION]]};
end
define :getPatternTrough do |pPattern|
lowest = getTopPosition();
pPattern.each {|n| lowest = (n[POSITION] < lowest) ? n[POSITION] : lowest};
return lowest;
end
define :invertPattern do |pPattern|
if LOGGING
puts "inverting";
end
return pPattern.map{|n| invertNote n};
end
define :isInRange do |pPattern, pLowPos, pHighPos|
return pPattern.none? {|n| (n[POSITION] < pLowPos) || (n[POSITION] > pHighPos)};
end
define :retrogressPattern do |pPattern|
if LOGGING
puts "retrogressing";
end
result = [];
pPattern.each{|n| result.unshift(n)};
return result;
end
define :timeTransformPattern do |pPattern|
if LOGGING
puts "time transforming";
end
transformation = 0;
while transformation == 0
transformation = rrand(-1, PATTERN_TIME_TRANSFORM_FACTOR);
end
if transformation < -0.9
return [];
else
return pPattern.map {|n| timeTransformNote(n, transformation)};
end
end
define :transposePattern do |pPattern, pSteps|
if LOGGING
puts "transposing";
end
return pPattern.map{|n| transposeNote(n, pSteps)};
end
##| performance
define :calculatePitch do |pPosition|
return DOMAIN[pPosition];
end
define :performPattern do |pPattern, pInstrument|
if pPattern != nil
if LOGGING
puts "performing " + pPattern.to_s;
end
sample :elec_tick, amp: RHYTHM_AMP;
isInBassRange = (calculateHz(getPatternTrough(pPattern)) <= 140);
with_fx :pan, pan: rrand(-1, 1) do
with_fx :hpf, cutoff: (calculatePitch(getPatternTrough(pPattern) + 5)) do
with_fx :reverb, mix: (isInBassRange ? 0 : 0.2) do
with_fx :echo, amp: 0.2, decay: rrand(ECHO_DECAY_MIN, ECHO_DECAY_MAX),
phase: ECHO_PHASE, mix: (isInBassRange ? 0 : rrand(ECHO_MIX_MIN, ECHO_MIX_MAX)) do
for i in 0...pPattern.length
n = pPattern[i];
duration = n[DURATION] - ((i == (pPattern.length - 1)) ? EVENT_GAP : 0);
pitch = calculatePitch(n[POSITION]);
amplitude = (((i == 0) || isOnDownbeat(get[:ticks])) ? 0.8 : 0.4) + (isInBassRange ? 0.2 : 0);
use_synth pInstrument;
play pitch, amp: amplitude, sustain: 0, release: RELEASE_DURATION;
sleep duration;
end
end
end
end
end
end
if LOGGING
puts pInstrument.to_s + " resting";
end
sleep dice(LONGEST_PAUSE) - EVENT_GAP;
end
##| timing
define :isOnDownbeat do |pTicks|
return (pTicks % PULSE) == 0;
end
##| Live
##| variables
patterns = [generatePattern];
live_loop :performFullRangeTri do
sync :time;
if NUM_VOICES > 0
performPattern(patterns.choose, :tri);
end
end
##| live_loop :performLowRangeFM do
##| sync :time;
##| performPattern((patterns.select {|p| isInRange(p, 0, (DOMAIN.length / 2))}).choose, :fm);
##| end
##| live_loop :performHighRangePulse do
##| sync :time;
##| performPattern((patterns.select {|p| isInRange(p, (DOMAIN.length / 2), (DOMAIN.length - 1))}).choose, :pulse);
##| end
live_loop :performFullRangeFM do
sync :time;
if NUM_VOICES > 1
performPattern(patterns.choose, :fm);
end
end
live_loop :performFullRangePulse do
sync :time;
if NUM_VOICES > 2
performPattern(patterns.choose, :pulse);
end
end
live_loop :performFullRangeSine do
sync :time;
if NUM_VOICES > 3
performPattern(patterns.choose, :sine);
end
end
live_loop :performFullRangeSquare do
sync :time;
if NUM_VOICES > 4
performPattern(patterns.choose, :square);
end
end
live_loop :changeLife do
if ((PATTERNS_MAX_NUMBER == -1) ||
((PATTERNS_MAX_NUMBER >= 0) && (patterns.length < PATTERNS_MAX_NUMBER))) &&
(PATTERN_GENERATION_FACTOR != -1) && one_in(PATTERN_GENERATION_FACTOR)
patterns.push generatePattern;
end
deleteIndices = [];
for i in 0...patterns.length
if patterns[i].length == 0
deleteIndices.push i;
end
if one_in(PATTERN_EVOLUTION_FACTOR)
patterns.push evolvePattern(patterns[i]);
if patterns.length > PATTERNS_MAX_NUMBER
deleteIndices.push rand_i(PATTERNS_MAX_NUMBER);
end
end
end
for i in deleteIndices.reverse
patterns.delete_at i;
end
if LOGGING
puts patterns.length.to_s + "/" + PATTERNS_MAX_NUMBER.to_s;
end
sleep 1;
end
live_loop :keepTime do
cue :time;
if isOnDownbeat(get[:ticks])
sample :elec_tick, amp: RHYTHM_AMP;
elsif isOnDownbeat(get[:ticks] - (PULSE / 2))
sample :elec_tick, amp: RHYTHM_AMP / 2;
end
sleep 1;
set :ticks, (get[:ticks] + 1);
end
patterns
v0.0.9 (20210104)
by d0lfyn (twitter: @0delphini)
a "pattern-oriented" music generator which, over time,
creates worlds in which patterns evolve and interweave
by chance
the complexity of the piece increases quickly up
until a carrying capacity for patterns is reached,
whereafter existing patterns evolve steadily
history:
v0.0.1 (20210101)
+ initial implementation
v0.0.2 (20210101)
+ add isInRange calculation
+ add stereo FX
+ add pattern bank size limit
v0.0.3 (20210101)
+ reformulate getTopPosition
+ add time transformation (buggy and unused)
+ add system variable range documentation
+ preserve all patterns until carrying capacity reached
+ reintroduce third voice
v0.0.4 (20210102)
+ arrange function groups alphabetically
+ make code consistent
+ add means not to generate new patterns
- remove time transformation evolution case (creates redundant patterns)
+ complete documentation of global constants
+ add logging mode
+ fix pattern deletion mechanism
v0.0.5 (20210102)
+ add echo phase constant
+ optimise performance module
v0.0.6 (20210103)
+ add 2 more voices and voice number setting
v0.0.7 (20210103)
+ correct decay range documentation
+ reorder variables by order of declaration
+ rename PATTERN_CHANGE_INTERVAL to CHANGE_INTERVAL
v0.0.8 (20210104)
+ stress first note of each pattern
+ reduce echo amp
+ remove bass echo
+ amplify bass
+ add pulse and stress downbeats
+ add high-pass filter to performance chain
v0.0.9 (20210104)
+ correctly stress downbeat
+ add electric ticks to downbeats and pattern triggers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment