Last active
November 9, 2023 09:16
-
-
Save d0lfyn/3d149e558ae3566840bbaaf53f9febb4 to your computer and use it in GitHub Desktop.
'Computational Counterpoint' for Sonic Pi (dedicated to Steve Reich)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
##| computational organisms (computational counterpoint, dedicated to Steve Reich) | |
##| v0.0.3 (20210103) | |
##| 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 | |
##| environment | |
use_bpm 480; | |
use_random_seed 19361003; | |
LOGGING = false; | |
##| domain | |
BASE_NOTE = :c3; | |
NUM_OCTAVES = 4; | |
DOMAIN = (scale BASE_NOTE, :major, num_octaves: NUM_OCTAVES); | |
##| notes | |
POSITION = 0; | |
DURATION = 1; | |
##| patterns | |
PATTERN_START_LENGTH = 6; ##| int [0,) | |
PATTERN_MAX_LENGTH = -1; ##| int [-1,) | -1 for infinitely long pattern | |
PATTERN_EVOLUTION_FACTOR = 45; ##| int [1,) | lower means more frequent mutations to pattern | |
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 | |
DO_TIME_TRANSFORM = false; | |
##| organisms | |
VITALITY = 0; | |
PATTERN = 1; | |
ORGANISM_GENERATION_FACTOR = 45; ##| int [-1,) | -1 for no new patterns, lower means more frequent pattern generation | |
ORGANISMS_MAX_NUMBER = 48; ##| int [-1,) | -1 for infinitely many organisms | |
ORGANISM_MAX_VITALITY = 200; ##| [0,) | |
ORGANISM_START_VITALITY = 200; ##| [0,ORGANISM_MAX_VITALITY]; | |
ORGANISM_VITALITY_DEPLETION_RATE = 2; ##| (,) | |
FAVOUR_LIVELY_FACTOR = 4; ##| int [-1,) != 0 | -1 for no favouring, lower means more vitality is more likely to be favoured | |
FAVOUR_INFIRM_FACTOR = -1; ##| int [-1,) != 0 | -1 for no favouring, lower means less vitality is more likely to be favoured | |
FAVOUR_BRIEF_FACTOR = -1; ##| int [-1,) != 0 | -1 for no favouring, lower means more brevity is more likely to be favoured | |
FAVOUR_LONG_FACTOR = -1; ##| int [-1,) != 0 | -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 | |
CHANGE_INTERVAL = 5; ##| (0,) | lower means more frequent pattern changes (i.e. evolution, generation, growth) | |
EVENT_GAP = 0.5; ##| (0,1] | |
LONGEST_DURATION = 2; ##| int [1,) | |
LONGEST_PAUSE = 8; ##| int (1,) | |
##| 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 = 4; | |
set :voice0Playing, nil; | |
set :voice1Playing, nil; | |
set :voice2Playing, nil; | |
set :voice3Playing, nil; | |
set :voice4Playing, nil; | |
##| 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 | |
##| organisms | |
define :areOrganismsFrequencyCompatible do |pOrganism1, pOrganism2| | |
if (pOrganism1 == nil) || (pOrganism2 == nil) | |
return true; | |
end | |
return arePatternsFrequencyCompatible(pOrganism1[PATTERN], pOrganism2[PATTERN]); | |
end | |
define :chooseOrganism do |pOrganisms| | |
pool = pOrganisms; | |
if (FAVOUR_FREQUENCY_COMPATIBILITY_FACTOR != -1) && one_in(FAVOUR_FREQUENCY_COMPATIBILITY_FACTOR) | |
pool = getFrequencyCompatibleOrganisms(pOrganisms); | |
end | |
if (FAVOUR_LIVELY_FACTOR != -1) && one_in(FAVOUR_LIVELY_FACTOR) | |
if !((FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR)) | |
pool = filterOrganismsLively(pool); | |
end | |
elsif (FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR) | |
pool = filterOrganismsInfirm(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.choose; | |
end | |
define :depleteOrganism do |pOrganism| | |
return [(pOrganism[VITALITY] - ORGANISM_VITALITY_DEPLETION_RATE), pOrganism[PATTERN]]; | |
end | |
define :evolveOrganism do |pOrganism| | |
return [pOrganism[VITALITY], evolvePattern(pOrganism[PATTERN])]; | |
end | |
define :filterOrganismsBrief do |pOrganisms| | |
pool = pOrganisms; | |
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 :filterOrganismsInfirm do |pOrganisms| | |
pool = pOrganisms; | |
minVitality = ORGANISM_MAX_VITALITY; | |
pool.each {|o| (minVitality = (o[VITALITY] < minVitality) ? o[VITALITY] : minVitality)}; | |
pool = pool.select {|o| o[VITALITY] == minVitality}; | |
return pool; | |
end | |
define :filterOrganismsLively do |pOrganisms| | |
pool = pOrganisms; | |
maxVitality = 0; | |
pool.each {|o| (maxVitality = (o[VITALITY] > maxVitality) ? o[VITALITY] : maxVitality)}; | |
pool = pool.select {|o| o[VITALITY] == maxVitality}; | |
return pool; | |
end | |
define :filterOrganismsLong do |pOrganisms| | |
pool = pOrganisms; | |
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 :generateOrganism do | |
return [ORGANISM_START_VITALITY, generatePattern]; | |
end | |
define :getFrequencyCompatibleOrganisms do |pOrganisms| | |
return pOrganisms.select {|o| (areOrganismsFrequencyCompatible(o, get[:voice0Playing])) && | |
(areOrganismsFrequencyCompatible(o, get[:voice1Playing])) && | |
(areOrganismsFrequencyCompatible(o, get[:voice2Playing]))}; | |
end | |
define :getOrganismToDelete do |pOrganisms| | |
pool = pOrganisms; | |
if (FAVOUR_LIVELY_FACTOR != -1) && one_in(FAVOUR_LIVELY_FACTOR) | |
if !((FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR)) | |
pool = filterOrganismsInfirm(pool); | |
end | |
elsif (FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR) | |
pool = filterOrganismsLively(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.choose; | |
end | |
define :isOrganismAlive do |pOrganism| | |
return (pOrganism[VITALITY] > 0); | |
end | |
define :replenishOrganism do |pOrganism, pTime| | |
if (pOrganism[VITALITY] + pTime) > ORGANISM_MAX_VITALITY | |
return [ORGANISM_MAX_VITALITY, pOrganism[PATTERN]]; | |
end | |
return [(pOrganism[VITALITY] + pTime), pOrganism[PATTERN]]; | |
end | |
##| patterns | |
define :arePatternsFrequencyCompatible do |pPattern1, pPattern2| | |
p1Below = (getPatternPeak(pPattern1) < getPatternTrough(pPattern2)); | |
p1Above = (getPatternPeak(pPattern2) < getPatternTrough(pPattern1)); | |
return (p1Below || p1Above); | |
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 | |
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 :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 :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 :calculateHz do |pPosition| | |
return midi_to_hz(calculatePitch(pPosition)); | |
end | |
##| Live | |
##| 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 "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; | |
else | |
puts "only 5 voices exist"; | |
end | |
patternTrough = getPatternTrough(pattern) | |
with_fx :pan, pan: rrand(-1, 1) do | |
with_fx :reverb, mix: ((calculateHz(patternTrough) > 150) ? 0.2 : 0) do | |
with_fx :echo, amp: 0.5, decay: rrand(ECHO_DECAY_MIN, ECHO_DECAY_MAX), | |
phase: ECHO_PHASE, mix: 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]); | |
organisms[pOrganismIndex] = replenishOrganism(organism, n[DURATION]); | |
set :organisms, organisms; | |
use_synth pInstrument; | |
play pitch, amp: 0.5, sustain: 0, release: RELEASE_DURATION; | |
sleep duration; | |
end | |
end | |
end | |
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; | |
else | |
puts "only 5 voices exist"; | |
end | |
end | |
if LOGGING | |
puts pInstrument.to_s + " resting"; | |
end | |
sleep rrand_i(1, LONGEST_PAUSE) - EVENT_GAP; | |
end | |
##|loops | |
live_loop :evolve do | |
organisms = get[:organisms].take(get[:organisms].length); | |
if ((ORGANISMS_MAX_NUMBER == -1) || | |
((ORGANISMS_MAX_NUMBER >= 0) && (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 = getOrganismToDelete(organisms); | |
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 | |
set :organisms, organisms; | |
if LOGGING | |
puts organisms.length.to_s + "/" + ORGANISMS_MAX_NUMBER.to_s; | |
end | |
sleep CHANGE_INTERVAL; | |
end | |
live_loop :performFullRangeTri do | |
sync :time; | |
if NUM_VOICES > 0 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 0, :tri); | |
end | |
end | |
live_loop :performFullRangeFM do | |
sync :time; | |
if NUM_VOICES > 1 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 1, :fm); | |
end | |
end | |
live_loop :performFullRangePulse do | |
sync :time; | |
if NUM_VOICES > 2 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 2, :pulse); | |
end | |
end | |
live_loop :performFullRangeSine do | |
sync :time; | |
if NUM_VOICES > 3 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 3, :sine); | |
end | |
end | |
live_loop :performFullRangeSquare do | |
sync :time; | |
if NUM_VOICES > 4 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 4, :square); | |
end | |
end | |
live_loop :keepTime do | |
cue :time; | |
sleep 1; | |
end | |
live_loop :cycleLife do | |
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 | |
sleep 1; | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
##| computational counterpoint (dedicated to Steve Reich) | |
##| environment | |
use_bpm 480; | |
use_random_seed 19361003; | |
LOGGING = false; | |
##| domain | |
BASE_NOTE = :c3; | |
NUM_OCTAVES = 4; | |
DOMAIN = (scale BASE_NOTE, :major, num_octaves: NUM_OCTAVES); | |
##| notes | |
POSITION = 0; | |
DURATION = 1; | |
##| patterns | |
PATTERN_START_LENGTH = 6; ##| int [0,) | |
PATTERN_MAX_LENGTH = -1; ##| int [-1,) | -1 for infinitely long pattern | |
PATTERN_EVOLUTION_FACTOR = 300; ##| int [1,) | lower means more frequent mutations to pattern | |
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 | |
DO_TIME_TRANSFORM = false; | |
##| organisms | |
VITALITY = 0; | |
PATTERN = 1; | |
ORGANISM_GENERATION_FACTOR = 300; ##| int [-1,) | -1 for no new patterns, lower means more frequent pattern generation | |
ORGANISMS_MAX_NUMBER = 48; ##| int [-1,) | -1 for infinitely many organisms | |
ORGANISM_MAX_VITALITY = 200; ##| [0,) | |
ORGANISM_START_VITALITY = 200; ##| [0,ORGANISM_MAX_VITALITY]; | |
ORGANISM_VITALITY_DEPLETION_RATE = 2; ##| (,) | |
FAVOUR_LIVELY_FACTOR = 4; ##| int [-1,) != 0 | -1 for no favouring, lower means more vitality is more likely to be favoured | |
FAVOUR_INFIRM_FACTOR = -1; ##| int [-1,) != 0 | -1 for no favouring, lower means less vitality is more likely to be favoured | |
FAVOUR_BRIEF_FACTOR = -1; ##| int [-1,) != 0 | -1 for no favouring, lower means more brevity is more likely to be favoured | |
FAVOUR_LONG_FACTOR = -1; ##| int [-1,) != 0 | -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 = 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 = 4; | |
set :voice0Playing, nil; | |
set :voice1Playing, nil; | |
set :voice2Playing, nil; | |
set :voice3Playing, nil; | |
set :voice4Playing, nil; | |
##| 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 | |
##| organisms | |
define :areOrganismsFrequencyCompatible do |pOrganism1, pOrganism2| | |
if (pOrganism1 == nil) || (pOrganism2 == nil) | |
return true; | |
end | |
return arePatternsFrequencyCompatible(pOrganism1[PATTERN], pOrganism2[PATTERN]); | |
end | |
define :chooseOrganism do |pOrganisms| | |
pool = pOrganisms; | |
if (FAVOUR_FREQUENCY_COMPATIBILITY_FACTOR != -1) && one_in(FAVOUR_FREQUENCY_COMPATIBILITY_FACTOR) | |
pool = getFrequencyCompatibleOrganisms(pOrganisms); | |
end | |
if (FAVOUR_LIVELY_FACTOR != -1) && one_in(FAVOUR_LIVELY_FACTOR) | |
if !((FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR)) | |
pool = filterOrganismsLively(pool); | |
end | |
elsif (FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR) | |
pool = filterOrganismsInfirm(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.choose; | |
end | |
define :depleteOrganism do |pOrganism| | |
return [(pOrganism[VITALITY] - ORGANISM_VITALITY_DEPLETION_RATE), pOrganism[PATTERN]]; | |
end | |
define :evolveOrganism do |pOrganism| | |
return [pOrganism[VITALITY], evolvePattern(pOrganism[PATTERN])]; | |
end | |
define :filterOrganismsBrief do |pOrganisms| | |
pool = pOrganisms; | |
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 :filterOrganismsInfirm do |pOrganisms| | |
pool = pOrganisms; | |
minVitality = ORGANISM_MAX_VITALITY; | |
pool.each {|o| (minVitality = (o[VITALITY] < minVitality) ? o[VITALITY] : minVitality)}; | |
pool = pool.select {|o| o[VITALITY] == minVitality}; | |
return pool; | |
end | |
define :filterOrganismsLively do |pOrganisms| | |
pool = pOrganisms; | |
maxVitality = 0; | |
pool.each {|o| (maxVitality = (o[VITALITY] > maxVitality) ? o[VITALITY] : maxVitality)}; | |
pool = pool.select {|o| o[VITALITY] == maxVitality}; | |
return pool; | |
end | |
define :filterOrganismsLong do |pOrganisms| | |
pool = pOrganisms; | |
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 :generateOrganism do | |
return [ORGANISM_START_VITALITY, generatePattern]; | |
end | |
define :getFrequencyCompatibleOrganisms do |pOrganisms| | |
return pOrganisms.select {|o| (areOrganismsFrequencyCompatible(o, get[:voice0Playing])) && | |
(areOrganismsFrequencyCompatible(o, get[:voice1Playing])) && | |
(areOrganismsFrequencyCompatible(o, get[:voice2Playing]))}; | |
end | |
define :getOrganismToDelete do |pOrganisms| | |
pool = pOrganisms; | |
if (FAVOUR_LIVELY_FACTOR != -1) && one_in(FAVOUR_LIVELY_FACTOR) | |
if !((FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR)) | |
pool = filterOrganismsInfirm(pool); | |
end | |
elsif (FAVOUR_INFIRM_FACTOR != -1) && one_in(FAVOUR_INFIRM_FACTOR) | |
pool = filterOrganismsLively(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.choose; | |
end | |
define :isOrganismAlive do |pOrganism| | |
return (pOrganism[VITALITY] > 0); | |
end | |
define :replenishOrganism do |pOrganism, pTime| | |
if (pOrganism[VITALITY] + pTime) > ORGANISM_MAX_VITALITY | |
return [ORGANISM_MAX_VITALITY, pOrganism[PATTERN]]; | |
end | |
return [(pOrganism[VITALITY] + pTime), pOrganism[PATTERN]]; | |
end | |
##| patterns | |
define :arePatternsFrequencyCompatible do |pPattern1, pPattern2| | |
p1Below = (getPatternPeak(pPattern1) < getPatternTrough(pPattern2)); | |
p1Above = (getPatternPeak(pPattern2) < getPatternTrough(pPattern1)); | |
return (p1Below || p1Above); | |
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 | |
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 :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 :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 :calculateHz do |pPosition| | |
return midi_to_hz(calculatePitch(pPosition)); | |
end | |
##| timing | |
define :isOnDownbeat do |pTicks| | |
return (pTicks % PULSE) == 0; | |
end | |
##| Live | |
##| 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 "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; | |
else | |
puts "only 5 voices exist"; | |
end | |
sample :elec_tick, amp: RHYTHM_AMP; | |
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, 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...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])) ? 0.8 : 0.4) + (isInBassRange ? 0.2 : 0); | |
organisms[pOrganismIndex] = replenishOrganism(organism, n[DURATION]); | |
set :organisms, organisms; | |
use_synth pInstrument; | |
play pitch, amp: amplitude, sustain: 0, release: RELEASE_DURATION; | |
sleep duration; | |
end | |
end | |
end | |
end | |
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; | |
else | |
puts "only 5 voices exist"; | |
end | |
end | |
if LOGGING | |
puts pInstrument.to_s + " resting"; | |
end | |
sleep rrand_i(1, LONGEST_PAUSE) - EVENT_GAP; | |
end | |
##| loops | |
live_loop :performFullRangeTri do | |
sync :time; | |
if NUM_VOICES > 0 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 0, :tri); | |
end | |
end | |
live_loop :performFullRangeFM do | |
sync :time; | |
if NUM_VOICES > 1 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 1, :fm); | |
end | |
end | |
live_loop :performFullRangePulse do | |
sync :time; | |
if NUM_VOICES > 2 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 2, :pulse); | |
end | |
end | |
live_loop :performFullRangeSine do | |
sync :time; | |
if NUM_VOICES > 3 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 3, :sine); | |
end | |
end | |
live_loop :performFullRangeSquare do | |
sync :time; | |
if NUM_VOICES > 4 | |
activate(get[:organisms].index(chooseOrganism(get[:organisms])), 4, :square); | |
end | |
end | |
live_loop :cycleLife do | |
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 | |
sleep 1; | |
end | |
live_loop :changeLife do | |
organisms = get[:organisms].take(get[:organisms].length); | |
if ((ORGANISMS_MAX_NUMBER == -1) || | |
((ORGANISMS_MAX_NUMBER >= 0) && (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 = getOrganismToDelete(organisms); | |
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 | |
set :organisms, organisms; | |
if LOGGING | |
puts organisms.length.to_s + "/" + ORGANISMS_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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
computational organisms (computational counterpoint, dedicated to Steve Reich) | |
v0.0.5 (20210104) | |
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 | |
+ change octaves and voices to 3 | |
v0.0.5 (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