Self-modifying algorithms

I think there was still a bug as the last right is also the first one on the left

Hereās a new version of the code that does the mutation in more lazy fashion. I removed the extra mutation live_loops as i understand calling the mutate after the each loop does exactly the same thing. Added also some helper functions to make the code more readable.

I felt like all these changes were making the changes too slow for me, so i added some values to the initial state to kickstart it. Also used only the Rule 150 this time as I feel like when combining the different rules the melody seems bit out of sync. Also noticed that if you use Rule 150 with only 7 values it quickly turns to all zeros, so added a few extra notes.

``````use_debug false

generations = {
:gen1=>{:cells=>[0, 0, 1, 0, 0, 0, 0, 1, 1]},
:gen2=>{:cells=>[1, 0, 0, 0, 1, 0, 0, 0, 0]}
}

# Rule 110 https://en.wikipedia.org/wiki/Rule_110
rule_110 = {
"111" => 0, "110" => 1,
"101" => 1, "100" => 0,
"011" => 1, "010" => 1,
"001" => 1, "000" => 0
}

rule_150 = {
"111" => 1, "110" => 0,
"101" => 0, "100" => 1,
"011" => 0, "010" => 1,
"001" => 1, "000" => 0
}

define :cells do |gen|
generations[gen][:cells]
end

define :store_state do |gen|
generations[gen][:last] = generations[gen][:cells].dup # dup to create new object
end

define :mutate do |rule, gen, i|
store_state gen if i == 0 # Store state in first cycle
cells = generations[gen][:cells]
last_gen = generations[gen][:last]
left = i > 0 ? i - 1 : cells.length - 1;
right = i < cells.length - 1 ? i + 1 : 0;
pattern = "#{last_gen[left]}#{last_gen[i]}#{last_gen[right]}";
cells[i] = rule[pattern]
end

use_synth :pluck
use_synth_defaults release: 1.5, coef: 0.4

with_fx :reverb, mix: 0.4 do
with_fx :flanger, wave: 3, depth: 7, decay: 1.5 do
live_loop :organism_1 do
print cells(:gen1)
cells(:gen1).each_with_index do |s,i|
play (degree i+1, :d, :dorian), pan: rrand(-0.5, 0.5) if s==1
sleep 0.25
end
mutate(rule_150, :gen1, tick%cells(:gen1).length)
end

live_loop :organism_2 do
print cells(:gen2)
cells(:gen2).each_with_index do |s,i|
play (degree i+1, :d3, :dorian), pan: rrand(-0.5, 0.5) if s==1
sleep 0.25
end
mutate(rule_150, :gen2, tick%cells(:gen2).length)
end
end
end
``````

Thanks for fixing it! Yes, storing the complete state is a good idea. Then you also do not depend on the direction of parsing (left or right).

Actually, I do not mind if the piece develops slowly. We should take the time to let it slowly evolve

EDIT: your gen sequence has got length 9. It works, but note 0 will be overweighted (?) Maybe thatās why the combination of rule 110/150 didnāt sound good to you? I like org1=150 and org2=110 with starting sequence

``````generations = {
:gen1=>{:cells=>[0, 0, 1, 0, 0, 0, 0, 0]},
:gen2=>{:cells=>[1, 0, 0, 0, 0, 0, 0, 0]}
}
``````

With this state and 8 notes gen2 will be all zeros in around 1 minute. Not sure what is actually going on, but it seems that with 9 notes it runs longer, might turn to all zeros later on also.

I would not listen even myself too much about what sounds good and not. After all its just how we individually hear and feel in the moment. Im sure that some combinations of those rules would sound great together ā¦ and maybe combine note lengths from different rules as well.

Here is a convenience function to create the ruleset for any number `0 ... 255`:

``````define :create_ruleset do |n|
rules = Hash.new
return rules if n > 255
binary = "%08b" % n
8.times do |i|
key = "%03b" % i
rules[key] = binary[7-i].to_i
end
return rules
end
``````

Usage:

``````rule_110 = create_ruleset(110)
``````

Note: In the above examples there is a bug in the rule 150. It should read `"111" => 1`

1 Like

Another easy and neverending source for self-modifying or self-similar melodies are different kinds of string rewrite systems, such as lindenmayer system.

The idea is similar to cellular automata in some ways. Define set of rules and iterate them over same string over and over again. For the rules you can invent your own āvocabularyā and the meaning of things. Here is a simple example where the rules and results are interpreted as degrees and applied using gsub. You could also add more meaning and use some characters to represent rests or certain samples or anything you like:

``````rules = {
"1"=>"1 3",
"3"=>"2 4",
"4"=> "6",
"6"=>"1"
}

melody = "1 2"

live_loop :lindenmayer do
melody = melody.gsub(Regexp.union(rules.keys), rules)
print melody
melody.split(" ").each do |d|
play degree d, :e, :mixolydian
sleep 0.25
end
end
``````
3 Likes

To make the rewrite system more powerful, you can also add support for regular expressions and use function calls (or lambdas) as values:

``````
rules = {
"1"=>"1 3",
"2"=>"2 4",
/[3-7]/=>->{rrand_i(1, 7).to_s},
}

def rewrite(ax, rules)
ax.gsub(Regexp.union(rules.keys)) do |m|
v = rules.to_a.detect{|k,v| Regexp.union(k) =~ m}[1] # Hack for regexp keys
v.respond_to?(:call) ? v.call : v # Hack for lambda calls
end
end

melody = "1 2"

live_loop :lindenmayer do
melody = rewrite melody, rules
print melody
melody.split(" ").each do |d|
play degree d, :e, :mixolydian
sleep 0.25
end
end

``````

EDIT: Changed function calls to lambdas as function calls in hashes are evaluated in init.

With regular expressions and functions you could start doing different variations such as stochastic or context sensitive grammars.

2 Likes

Trying to apply the same trick again and ticking along the melody string instead of rewriting the whole string at once. The `slice` parameter can be set to e.g. 2 or 3 in order to modify by a slicing window instead of just single characters. Not sure whether this is already optimal, but it sounds quite ok.

``````use_debug false
rules = {
/(0 )+/ => ->{[0, 0, 1].choose.to_s + " "},
"1 " => "2 ",
"2 " => "4 5 6 ",
/([4-7] )/ => "8 ",
"8 " => "9 ",
/(9 )+/ => ->{["0", "x", "x"].choose + " "},
/(x )+/ => "0 ",
}

melody = "0 "

def rewrite(ax, rules)
ax.gsub(Regexp.union(rules.keys)) do |m|
v = rules.to_a.detect{|k, v| Regexp.union(k) =~ m}[1] # Hack for regexp keys
v.respond_to?(:call) ? v.call : v # Hack for lambda calls
end
end

define :modify do |mel, slice, i|
mel = mel.split(" ")
l = mel.length
i = i%l

left = i;
right = [i + slice - 1, l - 1].min;
ax = ""
mel[left..right].each do |m|
ax += (m + " ")
end
melc = rewrite(ax, rules)
melc = melc.split(" ")
#puts melc

if right+1 < l
res = left > 0 ? mel[0..left-1] : []
res += melc
res += mel[right+1..-1]
else
res = left > 0 ? mel[0..left-1] : []
res += melc
end
rres = ""
res.each do |m|
rres += (m + " ")
end
return rres
end

use_synth :pluck
use_synth_defaults release: 2.5, coef: 0.3
sca = scale :D3, :melodic_minor_asc, num_octaves: 3

with_fx :reverb, mix: 0.4 do
with_fx :flanger, wave: 3, depth: 7, decay: 2 do
live_loop :lindenmayer do
melody.split(" ").each do |s|
play sca[s.to_i] unless s == "x"
sleep 0.25
end
melody = modify(melody, 2, 2*tick)
puts melody
end
end
end
``````

EDIT: now everything is in one place: regex, slicing, blanks for numbers > 9

1 Like

Thanks! This makes experimenting so much easier

1 Like

I think you could use sub for changing just the first occurence.

Blanks can be pain but those are just characters that you can take into account ā¦ But no need for spaces or other separators until you start to do something more complex, for example rules like ā1ā=>ā135ā that could harmonize the melody and make up some chords.

1 Like

I started with sub but then introduced the slice, which requires to replace more than one, if the slice is large. Anyhow, the idea was to have a somewhat smoother development of the music but still rich in its total variation. So, having a larger string but modifying only a part of it seems a good aproach.

Ah yes, if you want to change the slice size. Nice idea.

With lindenmayer system you can of course generate larger melodies by applying the string replacement multiple times and then play only last generation, for example only the 10th generation like this:

``````rules = {
"1"=>"1 3",
"2"=>"2 4",
/[3-7]/=>->{rrand_i(1, 7).to_s},
}

def rewrite(ax, rules)
ax.gsub(Regexp.union(rules.keys)) do |m|
v = rules.to_a.detect{|k,v| Regexp.union(k) =~ m}[1] # Hack for regexp keys
v.respond_to?(:call) ? v.call : v # Hack for lambda calls
end
end

melody = "1 2"

10.times do
melody = rewrite melody, rules
end

with_synth :kalimba do
play_pattern_timed (melody.split(" ").map {|d| degree(d, :d, :major) }), [0.5,1.0]
end
``````

I tried this running on three different machines, spaced around the room, , with start times synced using OSC signals to be 2 seconds apart. Sounded really ethereal and great.

2 Likes

Yeah, thatās a very sensitive tool. I was just observing the strings in the output and whenever you feel like some note should be played more or less often thatās easy to do with the rules. Like this:

``````rules = {
/(0 )+/ => ->{["0 ", "0 ", "0 ", "1 "].choose},
"1 " => "4 5 6 ",
/([4-7] )/ => "8 ",
/(8 )+/  => ->{["4 ", "9 "].choose},
/(9 )+/ => ->{["0 ", "x ", "x "].choose},
/(x )+/ => "0 ",
}.freeze

melody = "0 1"
``````

I kicked out the 2 and instead have a drop from 8 to 4 in addition. Amazing.

Not yet tried that one, but sounds great!

Hi @Eli, although I think itās likely youāre just joking by saying someone elseās music is soulless, I think itās something we should try and avoid doing if at all possible. Subjectively negative terms like that might be meant in a constructive manner as Iām sure youāre attempting here, but they can far too easily be perceived as intending to be negative - and thatās definitely something weād really like to avoid.

Personally I didnāt feel it was soulless in the slightest and I find the whole idea deeply interesting not just on a musical level but a compositional one too.

@Eli I like your point of view. We have to fight against standard thoughts and world. Say what you feel keep going. You were polite and respectful it is the principal.

Hi Samā¦

Itās 10:50, on a Friday night, after a 60 hour week rolling out a new phone system to a Mental Health NHS Trust (4000+ phones).

I have just been diagnosed today as 3rd stage COPD, and my meds have been increased beyond what I can handleā¦

Perhaps the word I should have used was, I dont knwoā¦ āsyntheticā ? Whateverā¦

If you are going to cut me down on such a simple mistake, then thanks but no thanksā¦

Please close my account, restrict my accessā¦whatever. Iāve loved my time on the forums, and have a lot of respect
for not just you, but everyone who is trying to bring Spi up towards the best it can be.

But I will noit beā¦ treated so ācavalierlyā ? ā¦

Regards, and goodbye.

Paul Whitfield.
Eliā¦

1 Like

Hi Eli,

apologies that you feel this way, I did in no way intend to be negative towards you personally, only the tone of your post. I should be clear that your post had been flagged by the community as being possibly inappropriate, so was responding from that perspective in the nicest possible way.

Sorry to hear about your diagnosis and hope that things improve for you. Please do understand that this context isnāt necessarily known to people who read your posts and they may very well misinterpret your intentions.

Iām being clear about this not specifically towards you but for everyone reading this and towards helping set the community tone to be one tending towards constructive help rather than negativity.

Hi Sam,

āyour post had been flagged by the community as being possibly inappropriateā

Thats the thing with the Internetā¦ people hide behind their aliases.,

If these same people had posted, or simply come to me as āpeopleā and expressed their opinions,
then I could have been persuaded to apolgise (in the post) and perhaps clarified my feelings
with regard to the original post. .

However, its been left up to you, as the final moderator, to talk to me. Which Iām sure is as
painfull for you my friend as it is for me.

I;'ve edited my post to apologise to those people concernedā¦ but I will stick by my principles.

All the best Samā¦ I will never post or reply again.

/blocked

Eliā¦