Simple L-system implementation for melody generation

I’v been reading about melody generation and stumbled to this article describing melodic interpretation of L-systems. I tested the idea with sonic pi using simple regexp magic. This idea somehow resembles use of markov chains, but instead of playing the next variation the melody is generated in multiple generations. Now its super simple, but i’m planning to implement more features as part of ziffers notation.

axioms = {"0"=> "60 0 0 60 0", "60"=>"? 60 ?" }
start = "0"

6.times {start = start.gsub(Regexp.union(axioms.keys),axioms) }
notes = start.split(" ")

notes.each do |note|
  if note=="0" then
    sleep 0.25
  elsif note=="?" then
    play rrand_i(60,80) if note=="?"
    sleep 0.25
  else
    play note.to_i
    sleep 0.25
  end
end

Have fun trying out your own rules/axioms.

3 Likes

I googled more about L-Systems and found this article about grammar based composition. I realized that my earlier implementation was more of a forward chaining rule machine than L-system. Single gsub in a loop was changing the matches multiple times in the same generation, where L-system algorithm should only change same characters once in a generation.

I created new version that uses regexp trick to escape the changed values and unescape the values after all rules have been run against the generation:

def l_system(ax,rules,gen)
  gen.times.collect do
    ax = rules.each_with_object(ax.dup) do |(k,v),s|
      s.gsub!(/{{.*?}}|(#{k.is_a?(String) ? Regexp.escape(k) : k})/) do |m|
        g = Regexp.last_match.captures
        if g[0] then
          "{{#{v}}}" # Escape
        else
          m # If escaped
        end
      end
    end
    ax = ax.gsub(/{{(.*?)}}/) {$1}
  end
end

This function creates array of the generations, which can be parsed and played as a whole:

n = l_system(" c ",{"c"=>"e","e"=>"c g c", "g"=>""},5)
n = n.flatten.join.split(" ").ring
live_loop :play do
  play n.tick
  sleep 0.25
end

You can also use regular expression as a key. Depending on the rules it might be better to play only the last generation:

n = l_system(" c ",{/[c|g]/=>"e d","e"=>"c g c"},4)
n = n[3].split(" ").ring
live_loop :play do
  play n.tick
  sleep 0.25
end

I also implemented stochastic rules and randomization support for ziffers. For rest of the examples you need to include or run ziffers.rb and ziffers_utils.rb in free buffers. You can now use zplay directly to play fractal melodies generated by the lsystem function. This example plays the generation 4:

zplay "1", rules: {"1"=>"q1324","2"=>"q5e1456","3"=>"-5342+"}, gen: 4

L-system function can also be used to produce a ring from all of the generations:

n = lsystem("12e3456",{"1"=>"[2,4]","2"=>"[1,5]","3"=>"5","4"=>"3","5"=>"[1,3]"},10).ring

live_loop :p do
  sample :bd_tek
  zplay n.tick
end

Stochastic rules can be written using “0.3%=” in the beginning of the value-pair. This example creates random degrees based on 20% probability:

zplay "1234", rules: {/[1-7]/=>"0.2%=(1..9;qqe)"}, gen: 4

Parametric extensions can be defined using regexp groups and corresponding $(1-9) pointers. Calculations inside single quotes are evaluated before the replacement. Here is example of ziffers playing fibonacci sequences as degrees:

n = lsystem "1 1", {/(\d+) (\d+)/=>"$2 '$2+$1'"}, 30
n = n.flatten.ring
live_loop :fib do
  zplay n.tick
end

Context dependent rules can be written using regex groups or lookahead etc. syntax. This example matches group next to 1 and feeds that value back to the replacement creating ascending melody:

zplay "1", rules: {/(3)1/=>"q'$1+1'1'$1+2'",/[1-7]/=>"e313"}, gen: 4

I think that combining the L-system algorithm with such degree based syntax that can also represent note lengths might be a novel idea. What do you think? Hardest part is to come up with the rules that produce nice melodies. I would love to hear what you can come up with.

1 Like