Last active
November 9, 2023 09:16
-
-
Save d0lfyn/240cd42e8f73edfcd1b212b23f2f9be9 to your computer and use it in GitHub Desktop.
Computational Organisms for Sonic Pi
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 | |
##| 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 |
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 | |
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