Skip to content

Instantly share code, notes, and snippets.

@d0lfyn
Last active November 9, 2023 09:16
Show Gist options
  • Save d0lfyn/240cd42e8f73edfcd1b212b23f2f9be9 to your computer and use it in GitHub Desktop.
Save d0lfyn/240cd42e8f73edfcd1b212b23f2f9be9 to your computer and use it in GitHub Desktop.
Computational Organisms for Sonic Pi
##| computational organisms
##| environment
use_bpm 480;
# t = Time.new;
# use_random_seed ((t.to_i * 1000000000) + t.nsec);
use_random_seed Time.new.to_i;
# use_random_seed 0;
LOGGING = false;
##| domain
BASE_NOTE = :F2;
NUM_OCTAVES = 3;
DOMAIN = (scale BASE_NOTE, :major, num_octaves: NUM_OCTAVES);
##| notes
POSITION = 0; DURATION = 1;
PP = 0.1; P = 0.2; MP = 0.4; MF = 0.6;
##| patterns
PATTERN_MIN_START_LENGTH = 4; ##| int [1,) | min 3 recommended for stability
PATTERN_MAX_START_LENGTH = 8; ##| int [PATTERN_MIN_START_LENGTH,)
PATTERN_MAX_LENGTH = -1; ##| int [0,) | -1 for infinitely long pattern
PATTERN_EVOLUTION_FACTOR = 15; ##| int [1,) | lower means more frequent mutations
PATTERN_GROWTH_FACTOR = 2; ##| int [0,) | lower means fewer notes appended per development
##| organisms
HEALTH = 0; PATTERN = 1;
ORGANISM_GENERATION_FACTOR = 30; ##| int [1,) | -1 for no new patterns; lower means more frequent generation
ORGANISMS_MAX_NUMBER = 48; ##| int [0,) | -1 for infinitely many
ORGANISM_MAX_HEALTH = 100; ##| [0,)
ORGANISM_START_HEALTH = 100; ##| [0,ORGANISM_MAX_HEALTH];
ORGANISM_HEALTH_DEPLETION_RATE = 0.25; ##| (,)
FAVOUR_HEALTHY_FACTOR = -1; ##| int [1,) | -1 for no favouring; lower means more health is more likely to be favoured
FAVOUR_UNHEALTHY_FACTOR = 1; ##| int [1,) | -1 for no favouring; lower means less health is more likely to be favoured
FAVOUR_BRIEF_FACTOR = -1; ##| int [1,) | -1 for no favouring; lower means more brevity is more likely to be favoured
FAVOUR_LONG_FACTOR = -1; ##| int [1,) | -1 for no favouring; lower means more length is more likely to be favoured
FAVOUR_FREQUENCY_COMPATIBILITY_FACTOR = 1; ##| int [-1,) != 0 | -1 for no favouring; lower means frequency compatibility is more likely to be favoured
##| timing
set :ticks, 0;
EVENT_GAP = 0.5; ##| (0,1]
LONGEST_DURATION = 2; ##| int [1,)
LONGEST_PAUSE = 8; ##| int [2,)
PULSE = 16; ##| (0,) | best if even int
##| 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 = 0.125; ##| (0,(duration-EVENT_GAP)) | -1 for full note duration
NUM_VOICES = 3; ##| [0,5]
set :voice0Playing, nil;
set :voice1Playing, nil;
set :voice2Playing, nil;
set :voice3Playing, nil;
set :voice4Playing, nil;
MIDI_MODE = true; ##| voices on channels [1-NUM_VOICES], rhythm on [NUM_VOICES+1,(NUM_VOICES*2)+2]
MIDI_PORT = "loopmidi_port";
##| 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 :transposeNote do |pNote, pSteps|
return [(pNote[POSITION] + pSteps), pNote[DURATION]];
end
##| organisms
define :areOrganismsFrequencyCompatible do |pOrganism1, pOrganism2|
if (pOrganism1 == nil) || (pOrganism2 == nil)
return true;
end
return arePatternsFrequencyCompatible(pOrganism1[PATTERN], pOrganism2[PATTERN]);
end
define :favourOrganisms do |pOrganisms|
pool = pOrganisms.take(pOrganisms.length);
if (FAVOUR_FREQUENCY_COMPATIBILITY_FACTOR != -1) && one_in(FAVOUR_FREQUENCY_COMPATIBILITY_FACTOR)
pool = getFrequencyCompatibleOrganisms(pool);
end
if (FAVOUR_HEALTHY_FACTOR != -1) && one_in(FAVOUR_HEALTHY_FACTOR)
if !((FAVOUR_UNHEALTHY_FACTOR != -1) && one_in(FAVOUR_UNHEALTHY_FACTOR))
pool = filterOrganismsHealthy(pool);
end
elsif (FAVOUR_UNHEALTHY_FACTOR != -1) && one_in(FAVOUR_UNHEALTHY_FACTOR)
pool = filterOrganismsUnhealthy(pool);
end
if (FAVOUR_BRIEF_FACTOR != -1) && one_in(FAVOUR_BRIEF_FACTOR)
if !((FAVOUR_LONG_FACTOR != -1) && one_in(FAVOUR_LONG_FACTOR))
pool = filterOrganismsBrief(pool);
end
elsif ((FAVOUR_LONG_FACTOR != -1) && one_in(FAVOUR_LONG_FACTOR))
pool = filterOrganismsLong(pool);
end
return pool;
end
define :depleteOrganism do |pOrganism|
return [(pOrganism[HEALTH] - ORGANISM_HEALTH_DEPLETION_RATE), pOrganism[PATTERN]];
end
define :evolveOrganism do |pOrganism|
return [pOrganism[HEALTH], evolvePattern(pOrganism[PATTERN])];
end
define :filterOrganismsBrief do |pOrganisms|
pool = pOrganisms.take(pOrganisms.length);
minLength = (PATTERN_MAX_LENGTH != -1) ? (PATTERN_MAX_LENGTH * LONGEST_DURATION) : 999999999;
pool.each do |o|
totalDuration = getPatternTotalDuration(o[PATTERN]);
minLength = (totalDuration < minLength) ? totalDuration : minLength;
end
pool = pool.select {|o| getPatternTotalDuration(o[PATTERN]) == minLength};
return pool;
end
define :filterOrganismsHealthy do |pOrganisms|
pool = pOrganisms.take(pOrganisms.length);
maxHealth = 0;
pool.each {|o| (maxHealth = (o[HEALTH] > maxHealth) ? o[HEALTH] : maxHealth)};
pool = pool.select {|o| o[HEALTH] == maxHealth};
return pool;
end
define :filterOrganismsLong do |pOrganisms|
pool = pOrganisms.take(pOrganisms.length);
maxLength = 0;
pool.each do |o|
totalDuration = getPatternTotalDuration(o[PATTERN]);
maxLength = (totalDuration > maxLength) ? totalDuration : maxLength;
end
pool = pool.select {|o| getPatternTotalDuration(o[PATTERN]) == maxLength};
return pool;
end
define :filterOrganismsRange do |pOrganisms, pLower, pUpper|
return pOrganisms.select{|o| (getPatternTrough(o[PATTERN]) >= pLower) && (getPatternPeak(o[PATTERN]) <= pUpper)};
end
define :filterOrganismsUnhealthy do |pOrganisms|
pool = pOrganisms.take(pOrganisms.length);
minHealth = ORGANISM_MAX_HEALTH;
pool.each {|o| (minHealth = (o[HEALTH] < minHealth) ? o[HEALTH] : minHealth)};
pool = pool.select {|o| o[HEALTH] == minHealth};
return pool;
end
define :generateOrganism do
return [ORGANISM_START_HEALTH, generatePattern];
end
define :getFrequencyCompatibleOrganisms do |pOrganisms|
return pOrganisms.select {|o| (areOrganismsFrequencyCompatible(o, get[:voice0Playing])) &&
(areOrganismsFrequencyCompatible(o, get[:voice1Playing])) &&
(areOrganismsFrequencyCompatible(o, get[:voice2Playing]))&&
(areOrganismsFrequencyCompatible(o, get[:voice3Playing]))&&
(areOrganismsFrequencyCompatible(o, get[:voice4Playing]))};
end
define :getOrganismsToDelete do |pOrganisms|
pool = pOrganisms.take(pOrganisms.length);
if (FAVOUR_HEALTHY_FACTOR != -1) && one_in(FAVOUR_HEALTHY_FACTOR)
if !((FAVOUR_UNHEALTHY_FACTOR != -1) && one_in(FAVOUR_UNHEALTHY_FACTOR))
pool = filterOrganismsUnhealthy(pool);
end
elsif (FAVOUR_UNHEALTHY_FACTOR != -1) && one_in(FAVOUR_UNHEALTHY_FACTOR)
pool = filterOrganismsHealthy(pool);
end
if (FAVOUR_BRIEF_FACTOR != -1) && one_in(FAVOUR_BRIEF_FACTOR)
if !((FAVOUR_LONG_FACTOR != -1) && one_in(FAVOUR_LONG_FACTOR))
pool = filterOrganismsLong(pool);
end
elsif ((FAVOUR_LONG_FACTOR != -1) && one_in(FAVOUR_LONG_FACTOR))
pool = filterOrganismsBrief(pool);
end
return pool;
end
define :isOrganismAlive do |pOrganism|
return (pOrganism[HEALTH] > 0);
end
define :replenishOrganism do |pOrganism, pTime|
if (pOrganism[HEALTH] + pTime) > ORGANISM_MAX_HEALTH
return [ORGANISM_MAX_HEALTH, pOrganism[PATTERN]];
end
return [(pOrganism[HEALTH] + pTime), pOrganism[PATTERN]];
end
##| patterns
define :arePatternsFrequencyCompatible do |pPattern1, pPattern2|
return ((getPatternPeak(pPattern1) < getPatternTrough(pPattern2)) || (getPatternPeak(pPattern2) < getPatternTrough(pPattern1)));
end
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";
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 "evolving";
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);
end
end
define :generatePattern do
return developPattern([], rrand_i(PATTERN_MIN_START_LENGTH, PATTERN_MAX_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 :getPatternTotalDuration do |pPattern|
sum = 0;
pPattern.each {|n| sum += n[DURATION]};
return sum;
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 :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 :calculateHz do |pPosition|
return midi_to_hz(calculatePitch(pPosition));
end
##| timing
define :isOnDownbeat do |pTicks|
return (pTicks % PULSE) == 0;
end
##| Live
if MIDI_MODE
midi_all_notes_off port: MIDI_PORT;
end
##| variables
set :organisms, [generateOrganism];
##| functions
define :activate do |pOrganismIndex, pVoice, pInstrument|
if pOrganismIndex != nil
organisms = get[:organisms].take(get[:organisms].length);
organism = organisms[pOrganismIndex];
pattern = organism[PATTERN];
if LOGGING
puts "voice " + pVoice.to_s + " activating " + organism.to_s;
end
case pVoice
when 0
set :voice0Playing, organism;
when 1
set :voice1Playing, organism;
when 2
set :voice2Playing, organism;
when 3
set :voice3Playing, organism;
when 4
set :voice4Playing, organism;
end
if MIDI_MODE
midi_note_on :c4, vel_f: MP, port: MIDI_PORT, channel: NUM_VOICES + pVoice + 3;
else
sample :elec_tick, amp: MP;
end
isInBassRange = (calculateHz(getPatternTrough(pattern)) <= 140);
with_fx :pan, pan: rrand(-1, 1) do
with_fx :hpf, cutoff: (calculatePitch(getPatternTrough(pattern) + 5)) do
with_fx :reverb, pre_mix: (isInBassRange ? 0 : 0.2) do
with_fx :echo, amp: 0.2, decay: rrand(ECHO_DECAY_MIN, ECHO_DECAY_MAX), phase: ECHO_PHASE, pre_mix: (isInBassRange ? 0 : rrand(ECHO_MIX_MIN, ECHO_MIX_MAX)) do
for i in 0...pattern.length
n = pattern[i];
duration = n[DURATION] - ((i == (pattern.length - 1)) ? EVENT_GAP : 0);
pitch = calculatePitch(n[POSITION]);
amplitude = (((i == 0) || isOnDownbeat(get[:ticks] + 1)) ? MP : P) + (!MIDI_MODE && isInBassRange ? PP : 0);
organisms[pOrganismIndex] = replenishOrganism(organism, n[DURATION]);
set :organisms, organisms;
use_synth pInstrument;
if MIDI_MODE
midi_note_on pitch, vel_f: amplitude, port: MIDI_PORT, channel: pVoice + 1;
midi_note_on :c4, vel_f: P, port: MIDI_PORT, channel: NUM_VOICES + pVoice + 2;
else
play pitch, amp: amplitude, sustain: 0, release: ((RELEASE_DURATION == -1) ? duration : RELEASE_DURATION);
sample :elec_tick, amp: P;
end
sleep ((RELEASE_DURATION == -1) ? duration : RELEASE_DURATION);
if MIDI_MODE
midi_note_off pitch, port: MIDI_PORT, channel: pVoice + 1;
midi_note_off :c4, port: MIDI_PORT, vel_f: amplitude, channel: NUM_VOICES + pVoice + 2;
end
sleep duration - ((RELEASE_DURATION == -1) ? duration : RELEASE_DURATION);
end
end
end
end
end
if MIDI_MODE
midi_note_off :c4, port: MIDI_PORT, channel: NUM_VOICES + pVoice + 3;
end
case pVoice
when 0
set :voice0Playing, nil;
when 1
set :voice1Playing, nil;
when 2
set :voice2Playing, nil;
when 3
set :voice3Playing, nil;
when 4
set :voice4Playing, nil;
end
end
if LOGGING
puts pInstrument.to_s + " resting";
end
sleep rrand_i(1, LONGEST_PAUSE) - EVENT_GAP;
end
##| loops
live_loop :performVoice0 do
if (NUM_VOICES > 0)
sync :calculate;
nextOrganism = favourOrganisms(get[:organisms]).choose;
sync :perform;
activate(get[:organisms].index(nextOrganism), 0, :tri);
else
sleep 999999999;
end
end
live_loop :performVoice1 do
if (NUM_VOICES > 1)
sync :calculate;
nextOrganism = favourOrganisms(get[:organisms]).choose;
sync :perform;
activate(get[:organisms].index(nextOrganism), 1, :fm);
else
sleep 999999999;
end
end
live_loop :performVoice2 do
if (NUM_VOICES > 2)
sync :calculate;
nextOrganism = favourOrganisms(get[:organisms]).choose;
sync :perform;
activate(get[:organisms].index(nextOrganism), 2, :pulse);
else
sleep 999999999;
end
end
live_loop :performVoice3 do
if (NUM_VOICES > 3)
sync :calculate;
nextOrganism = favourOrganisms(get[:organisms]).choose;
sync :perform;
activate(get[:organisms].index(nextOrganism), 3, :sine);
else
sleep 999999999;
end
end
live_loop :performVoice4 do
if (NUM_VOICES > 4)
sync :calculate;
nextOrganism = favourOrganisms(get[:organisms]).choose;
sync :perform;
activate(get[:organisms].index(nextOrganism), 4, :square);
else
sleep 999999999;
end
end
live_loop :cycleLife do
sync :cycle;
organisms = get[:organisms].take(get[:organisms].length);
deleteIndices = [];
for i in 0...organisms.length
organisms[i] = depleteOrganism(organisms[i]);
if !isOrganismAlive(organisms[i])
deleteIndices.push i;
end
end
for i in deleteIndices.reverse
organisms.delete_at i;
end
set :organisms, organisms;
sleep 1;
end
live_loop :changeLife do
sync :change;
organisms = get[:organisms].take(get[:organisms].length);
if ((ORGANISMS_MAX_NUMBER == -1) || (organisms.length < ORGANISMS_MAX_NUMBER)) &&
one_in(ORGANISM_GENERATION_FACTOR)
organisms.push generateOrganism;
end
deleteIndices = [];
for i in 0...organisms.length
if organisms[i][PATTERN].length == 0
deleteIndices.push i;
end
if one_in(PATTERN_EVOLUTION_FACTOR)
organisms.push evolveOrganism(organisms[i]);
if organisms.length > ORGANISMS_MAX_NUMBER
organism = getOrganismsToDelete(organisms).choose;
if LOGGING
puts "deleting " + organism.to_s;
end
deleteIndices.push organisms.index(organism);
end
end
end
for i in deleteIndices.reverse
organisms.delete_at i;
end
if (organisms != get[:organisms])
set :organisms, organisms;
end
if LOGGING
puts organisms.length.to_s + "/" + ORGANISMS_MAX_NUMBER.to_s;
end
sleep 1;
end
live_loop :keepTime do
cue :calculate;
sleep 0.25;
cue :perform;
if isOnDownbeat(get[:ticks])
if MIDI_MODE
midi_note_on :c4, vel_f: MF, port: MIDI_PORT, channel: NUM_VOICES + 1;
else
sample :elec_tick, amp: MF;
end
elsif isOnDownbeat(get[:ticks] - (PULSE / 2))
if MIDI_MODE
midi_note_on :c4, vel_f: MP, port: MIDI_PORT, channel: NUM_VOICES + 1;
else
sample :elec_tick, amp: MP;
end
end
sleep 0.25;
cue :cycle;
sleep 0.25;
cue :change;
sleep 0.25;
set :ticks, (get[:ticks] + 1);
end
computational organisms
v0.0.8 (20210106)
by d0lfyn (twitter: @0delphini)
a development of "pattern-oriented" music, this program creates
worlds in which patterns live, evolve, and interweave
history:
v0.0.1 (20210102)
+ initial implementation
v0.0.2 (20210103)
+ rename favouring factors
+ add 2 more voices and voice number setting
v0.0.3 (20210103)
+ correct decay range documentation
+ reorder variables by order of declaration
+ rename EVOLUTION_INTERVAL to CHANGE_INTERVAL
v0.0.4 (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.5 (20210104)
+ make random seed time-dependent
+ correctly stress downbeat
+ add electric ticks to downbeats and pattern triggers
v0.0.6 (20210104)
+ revise pattern start length range
+ synchronise cycle and change loops
v0.0.7 (20210105)
+ optimise FX bypassing
+ add option to play full duration
+ add MIDI mode
- remove time transform code
+ improve system constants documentation
v0.0.8 (20210106)
+ change filtering chain for choosing note to play
+ re-introduce range filtering
+ modify rhythm dynamics
+ add missing frequency compatibility comparisons
+ separate calculation and performance stages
+ deplete organisms as intended
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment