Self-modifying algorithms

The examples I have been studying so far seem to be build on a static code base and using probabilities and random function to introduce variable behavior. During my work with Markov chains the idea come up to modify the probabilities while playing. This would change the style of the piece over time.

Does anybody have experience or examples or ideas on that?

Here is a case study to demonstrate the principle (without being a real piece of music yet):

# self-modifying algorithm

use_debug false
use_bpm 80
use_random_seed 1
st = 4.0

sca = scale :C4, :phrygian
densities = (ring 1, 2, 3, 4, 5)

# Markov matrix for 8 notes of the scale
markov_matrix1 = [
  # 0*    1     2*    3     4*    5     6*    7*
  [0.0,  0.0,  1.0,  0.0,  0.0,  0.0,  0.0,  0.0],  # 0* base tone
  [1.0,  0.0 , 0.0,  0.0,  0.0,  0.0,  0.0,  0.0],  # 1
  [0.0,  0.0 , 0.0,  0.0,  1.0,  0.0,  0.0,  0.0],  # 2* third
  [0.0,  1.0 , 0.0,  0.0,  0.0,  0.0,  0.0,  0.0],  # 3
  [0.0,  0.0 , 0.0,  0.0,  0.0,  0.0,  1.0,  0.0],  # 4* fifth
  [0.0,  0.0 , 0.0,  1.0,  0.0,  0.0,  0.0,  0.0],  # 5
  [0.0,  0.0 , 0.0,  0.0,  0.0,  0.0,  0.0,  1.0],  # 6* seventh
  [0.0,  0.0 , 0.0,  0.0,  0.0,  1.0,  0.0,  0.0],  # 7* octave
]

# Markov matrix for density of notes
markov_matrix2 = [
  # 1    2    3    4    5
  [1.0, 0.0, 0.0, 0.0, 0.0], # 1
  [1.0, 0.0, 0.0, 0.0, 0.0], # 2
  [1.0, 0.0, 0.0, 0.0, 0.0], # 3
  [1.0, 0.0, 0.0, 0.0, 0.0], # 4
  [1.0, 0.0, 0.0, 0.0, 0.0], # 5
]

# Function to modify markov matrix
define :modify do |mm|
  # randomly select a cell for modification
  row = rand_i mm.length
  col = rand_i mm[0].length
  # assign new value
  mm[row][col] = 0.1
  # normalize to row_sum = 1.0
  pp = 0.0
  mm[row].each do |p|
    pp += p
  end
  mm[row].length.times do |i|
    mm[row][i] /= pp
  end
end

# function to get next index according to markov matrix mm
define :next_idx do |current_idx, mm|
  n = 0
  r = rand
  pp = 0
  mm[current_idx].each do |p|
    pp += p
    break if pp > r
    n += 1
  end
  return n
end


n = 0
d = 0
with_fx :reverb,room: 0.8, mix: 0.4 do
  live_loop :melody, auto_cue: false do
    with_synth :pulse do
      with_synth_defaults amp: 0.8, attack: 0.05, cutoff: 90 do
        ng = 4  # number of chunks of notes to play with same density
        ng.times do
          density densities[d] do
            play sca[n], release: 0.05, sustain: st/ng-0.12
            n = next_idx(n, markov_matrix1)
            sleep st/ng
          end
          d = next_idx(d, markov_matrix2)
        end
      end
    end
    modify(markov_matrix1) if tick > 2
    modify(markov_matrix2) if look%2 == 0
  end
end

The modify function introduces random modifications to the Markov matrices, but the modifications could also be more intentional, following a plan. Furthermore, other parameters could be modified as well, not just probabilities in a matrix.

1 Like

I have a thought about algorithms that modify themselves, since I have considered them in my work: Unless it is the desired effect, a freely wandering algorithm can quickly lose its sense of familiarity.

In structured music, the listener will often appreciate a return to something that they have heard before. When an algorithm is allowed to wander anywhere it wants however, it is difficult to bring the music back to a resting point.

This is a challenge that I am facing in my music, which lacks structure and memorability.

Proposed solutions: periodically insert a previously heard episode into the music stream; or keep some elements constant while other elements are allowed to change. The first solution is more like punctuated equilibrium in evolution (except evolution seldom makes an exact restatement). The second solution allows gradual evolution.

I don’t think there is a right or wrong answer. Maybe a combination of the two approaches is what sounds best! :musical_score: :notes:

This interests me. How to approach planning?

1 Like

My thinking currently is around markov matrices as a means to encode style. Therefore, one idea is to have a matrix as a starting point (like a lovely melody generator :smiling_face_with_three_hearts:) and have a second matrix as the target (less lovely melody generator :japanese_goblin:). Then use the modify function to slowly migrate the matrix from start to end using interpolation of values. Something like that. Eventually you could also go back to the start again.

The important point here is that you actually do not put everything into the code explicitely. The program should evolve by itself.

1 Like

Interesting idea. I’v done some experiments with Lindermayer string replacement algorithms and cellular automatons. This idea reminded me of simple experiment i did with cellular automatons while back. You could also create some set of simple rules that would modify the probabilities based on the initial state.

My idea with the cellular automatons was to use index of the array as degrees from 0-7 and play when the value was 1 and sleep when 0. I realized that if you use same rules with different initial state and same ruleset it creates some nice harmonies and interesting patterns:

gen = [0, 0, 1, 0, 0, 0, 0]
gen2 = [1, 0, 0, 0, 0, 0, 0]

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

define :mutate do |gen|
  next_gen = []
  gen.each_with_index do |s,i|
    left = i > 0 ? i - 1 : gen.length - 1;
    right = i < gen.length - 1 ? i + 1 : 0;
    pattern = "#{gen[left]}#{gen[i]}#{gen[right]}";
    next_gen[i] = rule[pattern];
  end
  return next_gen;
end

live_loop :organism_1 do
  print gen
  gen.each_with_index do |s,i|
    play degree i+1, :d, :dorian if s==1
    sleep 0.25
  end
  gen = mutate gen
end

live_loop :organism_2 do
  print gen2
  gen2.each_with_index do |s,i|
    play degree i+1, :d3, :dorian if s==1
    sleep 0.25
  end
  gen2 = mutate gen2
end
3 Likes

Interesting concept. It is always fascinating to hear how basic mathematical ideas finally turn into sound. Sonic Pi is a wonderful machine …

A variation would be to do the mutation not for the whole generation in a single step, but let the mutation also happen as single steps triggered by the live_loop. This would give a more gradual evolution of the generation sequence.

That would be a version with a stepwise gradual mutation. The sound is a bit more minimalistic this way, because max one note is changed per loop.

use_debug false

gen = [0, 0, 1, 0, 0, 0, 0]
gen2 = [1, 0, 0, 0, 0, 0, 0]

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

live_loop :mutate1 do
  sync :organism_1
  i = tick%gen.length
  left = i > 0 ? i - 1 : gen.length - 1;
  right = i < gen.length - 1 ? i + 1 : 0;
  pattern = "#{gen[left]}#{gen[i]}#{gen[right]}";
  gen[i] = rule[pattern];
end

live_loop :mutate2 do
  sync :organism_2
  i = tick%gen2.length
  left = i > 0 ? i - 1 : gen2.length - 1;
  right = i < gen2.length - 1 ? i + 1 : 0;
  pattern = "#{gen2[left]}#{gen2[i]}#{gen2[right]}";
  gen2[i] = rule[pattern];
end


live_loop :organism_1 do
  print gen
  gen.each_with_index do |s,i|
    play degree i+1, :d, :dorian if s==1
    sleep 0.25
  end
end

live_loop :organism_2 do
  print gen2
  gen2.each_with_index do |s,i|
    play degree i+1, :d3, :dorian if s==1
    sleep 0.25
  end
end
1 Like

I like this stepwise gradual mutation very much! It is a more process-oriented and minimalist music. I feel like I could listen to it right next to Steve Reich’s Four Organs. It’s almost meditative.

Pardon me for saying this… but why when you make these mathematical
… ‘formula’, do you have to make them sound so… soulless?

Two simple fx’s turn your bland progressions into something worth listening
to.

Throw in a couple of different synths, and it actually sounds melodius.

Eli…

EDIT: Sam has chastised me for saying ‘soulless’…The 'community; has flagged
this as ‘inappropriate’… I tend to agree, perhaps it was a bad choice of words…
perhaps ‘synthetic’ might have been a better choice…

However, if they had come to me directly with their concerns, or posted in this
topic, instead of hiding behind Sam as a moderator, I could have changed my choice
of words. Instead, I have to stand by my original wording, and step down from this
forum

If I have offended anyone, then please accept my apology.

Goodbye.

Eli.

#use_debug false

use_synth :blade

gen = [0, 0, 1, 0, 0, 0, 0]
gen2 = [1, 0, 0, 0, 0, 0, 0]

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

live_loop :mutate1 do
  sync :organism_1
  i = tick%gen.length
  left = i > 0 ? i - 1 : gen.length - 1;
  right = i < gen.length - 1 ? i + 1 : 0;
  pattern = "#{gen[left]}#{gen[i]}#{gen[right]}";
  gen[i] = rule[pattern];
end

live_loop :mutate2 do
  sync :organism_2
  i = tick%gen2.length
  left = i > 0 ? i - 1 : gen2.length - 1;
  right = i < gen2.length - 1 ? i + 1 : 0;
  pattern = "#{gen2[left]}#{gen2[i]}#{gen2[right]}";
  gen2[i] = rule[pattern];
end

with_fx :echo, mix: 0.8, phase: 0.75, decay: 4 do
  with_fx :ixi_techno do
    live_loop :organism_1 do
      use_synth [:blade, :pluck].choose
      print gen
      gen.each_with_index do |s,i|
        play degree i+1, :d, :dorian if s==1
        sleep 0.25
      end
    end
    
    live_loop :organism_2 do
      use_synth [:tech_saws, :piano].choose
      print gen2
      gen2.each_with_index do |s,i|
        play degree i+1, :d3, :dorian if s==1
        sleep 0.25
      end
    end
  end
end

2 Likes

Very nice! I appreciate your interpretation of the algorithm. It has a lot more personality to it!

As for whether these ideas are “worth listening to”… I’m sure you’ve heard the saying “garbage in, garbage out” before? If the ideas aren’t good to start with, it can be difficult (but not impossible) to dress them up. Some people like me take it too far and get rapt in ideas. I think I can be entertained for a while by a good music box LOL. I think that’s why I can appreciate Steve Reich’s Four Organs now, a piece which I used to make fun of as a musical dead end (I was younger and stupider then than I am young and still rather stupid and inept now haha (:man_facepalming:/:woman_facepalming:))

Anyways, truth be told, I am almost clueless about sound design :sweat_smile: I’m learning a great deal by observing how other Sonic Pi users (you included) make use of effects. I think it’s a beautiful thing that everyone on this forum has something unique to contribute. Some are more idea-oriented, some are more performance-oriented, and the exceptionally hard-working individuals I’ve come across create the most wonderful music. Some like me are still searching for their voice, while some are endeavouring to get rid of their voice. Everyone is at a different stage of development and has something to teach and something to learn. This community is better for it.

So thanks for your honesty and for your contribution to this discussion! :slightly_smiling_face:

— d0lfyn

2 Likes

Well, this would be another important topic: the different approches to music creation.

Different people aproach it from different ends. Some start with an idea of a new sound. They experiment with synths and settings of parameters. For them any 5 notes are good enough to explore those ideas. Other would start with a base drum and a snare and develop the idea from there. And some would focus on creating new sequences of notes or chords. To them, choosing synths and parameters initially does not matter at all, it just absorbes time they do not want to invest in this early stage.

Actually, when I saw the first post of @amiika I smiled. There was such a 100% focus on getting the translation of the 110 automaton into the notes right. There was not even one thought about sound, just the standard beep. In other posts we see how much effort people have put into the design of a sound, and then they are choosing just 3 notes, without spending any thought on things like harmony and melody.

I am definitely on the notes/melody/harmony creation side. After doing my work, I just spend another 15 minutes or so to put some synths and tweak some parameters.

So, wouldn’t it a good idea to form a team? Some would do the work on the notes, other the rhythm and finally some add all the sound bits to it? Maybe worth a try …

The only thing I would disagree with @Eli is that the piece is soulless. To me it has got a hell lot of soul. You just need to add the sound and the other voices inside your head, as you like it. For example, I heard some guitars playing it.

1 Like

Sounds really nice!

Or “Minecraft music” as my kid pointed out instantly :slight_smile:

I’m always looking something from the mathematical constructs. Symmetries or such … I don’t know what exactly, but I know when i hear it. Its something that makes the generated piece so called absolute music (which is a disputed topic and sometimes actually called ‘music without a soul or a purpose’).

For me everything is just “bits in a soup” and the minimalistic approach helps to spot the beauty within.

2 Likes

Until further proposals and examples pop up, I’ll try to summarize with a definition of what self-modifying algorithms in the context of Sonic Pi programming would mean:

  • there are loops playing the music (:= voices)
  • what the voices play and how they play it is dependent on data stored in global data structures (:= pool of parameters). Global meaning: defined outside the loops and accessible from all loops / methods in the program.
  • other loops (or methods called by loops) modify the pool of parameters (:= modifiers). The modifiers can live in independent seperate loops, or, synchronized with or called by the voices.

The so far known two examples above got as pool of parameters either the markov matrix, or, the generation sequence. The modifiers are implemented as seperate methods or loops. The markov matrix example uses a modifier with random elements (choosing the matrix cell at random), the 110 rule example uses a deterministic modifier (automaton).

For those who are new to cellular automatons, there are different types of rulesets. This example was using specific rule 110 from one-dimensional realm. In this realm there are 255 possible rules that can be used to make infinite amount of compositions when combined with different initial states.

Different rules can be divided to 4 different classes depending on their behavior (as categorized by Wolfram in his book a New kind of science). This book is free on his page and chapters can be downloaded separately as pdf. A must read for any interested in generative algorithmic compositions. While reading keep in mind that it’s not a “scientific” publication and there is a LOT of stuff borrowed from different sources, which is nice to know if you are keen to explore some ideas further.

Anyway, different types of rules makes out different kinds of constructs. Class 1 and 2 are more stable and thus creates more repeats in notes. Class 3 creates random structures and are also used as random number generators. Class 4 is the most interesting as it creates rather stable structures but also randomly creates variations or chaotic sections. You can spend rest of your life exploring these, but beware that you can also get mad and obsessed with these as Wolfram :wink:

2 Likes

Good idea to try other automatons as well. Here is the program with a flexibility to assign rules to organisms. And, @Eli, with synths and fx!!!

use_debug false

gen1 = [[0, 0, 1, 0, 0, 0, 0], 0]
gen2 = [[1, 0, 0, 0, 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 :mutate do |rule, gen, i|
  gs = gen[0]  # gen sequence
  left = gen[1]  # last unmodified left element
  right = i < gs.length - 1 ? i + 1 : 0;
  pattern = "#{left}#{gs[i]}#{gs[right]}";
  gen[1] = gs[i]  # remember unmodified element as left element for next iteration
  gs[i] = rule[pattern]; # modify
end


live_loop :mutate1 do
  sync :organism_1
  mutate(rule_150, gen1, tick%gen1[0].length)
end

live_loop :mutate2 do
  sync :organism_2
  mutate(rule_110, gen2, tick%gen2[0].length)
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 gen1[0]
      gen1[0].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
    end
    
    live_loop :organism_2 do
      print gen2[0]
      gen2[0].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
    end
  end
end

EDIT: Fixed a bug, see thread below

1 Like

I think that you are slightly varying the “rules” of the cellular automata as you are modifying the same generation. Changes should not affect the current generation at hand. I mean when you are ticking and next time looking left it should be the older generation and not the change you made last time … that’s why I was using the new array for the next generation.

Can you explain what you mean? I think the result of the mutation in the loop is exactly the same as if it would be done inside a .each loop, isn’t it? The generation is only modified with the assigned ruleset and from step to step there is no modification of the generation sequence. The state is preserved. Hmm… ?

I mean, its different result when you start messing with the current generation. Like creating a Hulk using radioactive gamma bomb vs. modifying dna and giving birth to cute baby hulk :joy:

Here’s an example that does the both:

rule = {
  [1,1,1] => 0, [1,1,0] => 1,
  [1,0,1] => 1, [1,0,0] => 0,
  [0,1,1] => 1, [0,1,0] => 1,
  [0,0,1] => 1, [0,0,0] => 0
}

define :mutate_same_gen do |gen|
  gen.each_with_index do |s,i|
    left = i > 0 ? i - 1 : gen.length - 1;
    right = i < gen.length - 1 ? i + 1 : 0;
    pattern = [gen[left], s, gen[right]]
    gen[i] = rule[pattern]
  end
  return gen;
end

define :mutate_next_gen do |gen|
  next_gen = []
  gen.each_with_index do |s,i|
    left = i > 0 ? i - 1 : gen.length - 1;
    right = i < gen.length - 1 ? i + 1 : 0;
    pattern = [gen[left], s, gen[right]]
    next_gen[i] = rule[pattern]
  end
  return next_gen;
end

gen = [0, 0, 1, 0, 0, 0]

print mutate_same_gen gen
print mutate_next_gen gen

Thanks, got it. I fixed it in the post above. The gen1 array now contains also the last unmodified left element in order to apply the rule correctly.

On a different tack, if you think of a Markov chain (or a Finite State Machine) as a series of allowed transitions, you could make one with some musical heuristics, such as “the IV chord can resolve to the I chord or go to the vi chord”, “the vi chord can go to the IV chord or to the ii chord, and change the measure length to 2 beats”, etc, so have a random pattern that makes some kind of harmonic and rhythmic sense.

Exactly. See Harmonic random walk in major.

1 Like