# 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