Skip to content

Instantly share code, notes, and snippets.

@d0lfyn
Last active November 9, 2023 09:16
Show Gist options
  • Save d0lfyn/3d149e558ae3566840bbaaf53f9febb4 to your computer and use it in GitHub Desktop.
Save d0lfyn/3d149e558ae3566840bbaaf53f9febb4 to your computer and use it in GitHub Desktop.
'Computational Counterpoint' for Sonic Pi (dedicated to Steve Reich)
##| 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
##| 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
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