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.