Self-modifying algorithms

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

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 :innocent:

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 :slight_smile: ā€¦ 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ā€¦

@Eli I am very sad of your decision.
How can people be so sensible. Just for one word and again you say what you think with respect. Ah of course, you didnā€™t use smileys to attenuate your meaning to be a cool guy.
So hope your decision is a temporarily one.

@samaaron could have ignored this flag too and the useless story wouldnā€™t have existed.

I hope to read you again on this forum cause anybody has the right to express its thoughts. This is called liberty. We all need difference to be better people.

There is no problem to say your music sounds soulless to someone. Itā€™s just a question of feeling.

So @Eli please come back :relaxed::relaxed::relaxed::relaxed::relaxed: