On L-systems, dictionaries and all that

posted by on 2013.11.13, under Supercollider
13:

It’s quite a while I have not written anything here: Python, ChucK and functional programming are keeping me quite busy. 😉
Here I want to tell you about Lindenmayer systems (or L-systems) and how to use dictionaries in SuperCollider to sonify them. So, first of all, what is an L-system? An L-system is a recursive system which is obtained from an alphabet, an axiom word in the alphabet, and a set of production rules. Starting from the axiom word, one applies iteratively the production rules to each character in the word at the same time, obtaining a new word which allows the procedure to be repeated.
A classical example is the following: consider the alphabet L={A,B}, the set of rules {A -> AB, B -> A}, and axiom word W=A. This will give the following words by iteration

A
AB
ABA
ABAAB
ABAABABA

and so on.
You are probably thinking “Wait a minute: alphabets, words, formation rules? Has this all anything to do with languages?” And the answer is: “Yes! But…”, the main difference with (formal) languages being that the production rules in a formal grammar don’t need to be applied all at the same time.
Ok, all well and good, but you may ask “What is this all for?! I mean, why?!?”, and I suspect the fact that, for instance, the sequence of the length of the words in the above example gives you the Fibonacci sequence is not an appropriate answer. :) The point is that Lindenmayer came to realize that these iterative systems can describe nicely the growth of many types of plants and their branching structures. For instance, the example above describes the growth of algae. The truly surprising thing, also, is that L-systems can be used to generate all* the types of known fractals, like Mandelbrot, Koch curve, the Sierpinski triangle, etc., when the alphabet is appropriately geometrized. So. in a nutshell, L-systems are very cool things. 😉
What about dictionaries, then? Dictionaries are powerful objects in many programming languages, and are sort of arrays in which elements can be indexed with keys which are different from the index position of the element itself. They can be, for instance, strings or symbols**. Basically, they are perfect to work as mappings.
So, let’s look at the code

s.boot;

(
~path = "pathtofilefolder";

~kick = Buffer.read(s,~path ++ "sampleaddress");
~hhat = Buffer.read(s,~path ++ "sampleaddress");
~clap = Buffer.read(s,~path ++ "sampleaddress");

SynthDef(\playbuf,{arg out = 0, r = 1, buff = 0, amp = 1, t = 0;
    var sig = PlayBuf.ar(1, buff, r);
    var env = EnvGen.kr( Env.perc(0.01, 0.8), doneAction: 2);
    Out.ar(out, sig*env*amp*t!2);
}).add;

SynthDef(\mall,{arg out=0,note, amp = 1;
    var sig=Array.fill(3,{|n| SinOsc.ar(note.midicps*(n+1),0,0.3)}).sum;
    var env=EnvGen.kr(Env.perc(0.01,0.8), doneAction:2);
    Out.ar(out, sig*env*amp!2);
}).add;
)

(



var dict = IdentityDictionary[\A -> "AB", \B -> "A", \C -> "DB", \D -> "BC"]; //These are the production rules of the L-system
var word = "AC"; //Axiom word
var string_temp = "";
var iter = 10;

//These are diction for the mapping of the alphabet to "artistic" parameters: degrees in a scale, beat occurrence, etc.

var dictnotes = IdentityDictionary[\A -> 50, \B -> 55, \C -> 54, \D -> 57];
var dictkick = IdentityDictionary[\A -> 1, \B -> 0, \C -> 1, \D -> 0];
var dicthat = IdentityDictionary[\A -> 1, \B -> 0, \C -> 1, \D -> 1];
var notes=[];
var beat=[];
var beat2=[];

//This iteration generates the system recursively

iter.do({

word.asArray.do({|i|
    string_temp = string_temp ++ dict[i.asSymbol];
});

word = string_temp;
string_temp = "";
});

word.postln;

//Here we map the final system to the parameters as above

word.do({|i| notes = notes ++ dictnotes[i.asSymbol];});
word.do({|i| beat = beat ++ dictkick[i.asSymbol];});
word.do({|i| beat2 = beat2 ++ dicthat[i.asSymbol];});

notes.postln;
beat.postln;


s.record;

Pbind(*[\instrument: \mall, \note: Pseq(notes,inf), \amp: Pfunc({rrand(0.06,0.1)}), \dur: 1/4]).play(quant:32);
Pbind(*[\instrument: \mall, \note: Pseq(notes + 48,inf), \amp: Pfunc({rrand(0.06,0.1)}),\dur: 1/8]).play(quant:32 + 16);
Pbind(*[\instrument: \playbuf, \t: Pseq(beat,inf), \buff: ~kick, \dur: 1/8]).play(quant:32 + 32);
Pbind(*[\instrument: \playbuf, \t: Pseq(beat2,inf), \buff: ~hhat, \r:8, \dur: 1/8]).play(quant:32 + 32 + 8);
Pbind(*[\instrument: \playbuf, \t: Prand([Pseq([0,0,1,0],4), Pseq([0,1,0,0],1)],inf), \buff: ~clap, \r: 1,\dur: 1/4]).play(quant:32 + 32 + 8);

)

The code above is in a certain sense a “universal L-systems generator”, in the sense that you can change the alphabet, the production rules and the axiom and get your own system. A generalisation of L-systems are “probabilistic L-systems”, in which the production rules are weighted with a certain probability to happen. This is maybe the topic for another post, though. 😉
While listening to this, notice how the melodies and the beats tend to self replicate, but with “mutations”.

Audio clip: Adobe Flash Player (version 9 or above) is required to play this audio clip. Download the latest version here. You also need to have JavaScript enabled in your browser.

*I don’t know any mathematical proof of this statement.
**I have used Identity Dictionaries here, where keys are symbols.

comment

Thanks for this introduction to L-systems. One thing I found when running the code is that the lines:

word.do({|i| notes = notes ++ dictnotes[i.asSymbol];});
word.do({|i| beat = beat ++ dictkick[i.asSymbol];});
word.do({|i| beat2 = beat2 ++ dicthat[i.asSymbol];});

take quite some time to evaluate (probably because so many dictionary look-ups are required).

I made the following optimization using Pcollect:

notes = Pcollect({|i| dictnotes[i.asSymbol]}, Pseq(word.asList));
beat = Pcollect({|i| dictkick[i.asSymbol]}, Pseq(word.asList));
beat2 = Pcollect({|i| dicthat[i.asSymbol]}, Pseq(word.asList));

Pbind(*[\instrument: \mall, \note: notes, \amp: Pfunc({rrand(0.06,0.1)}), \dur: 1/4]).play(quant:32);
.
.
.

Pcollect performs a lazy evaluation (the dictionary is not looked up until a pattern requests a new value). Anyway, this seems to work well on my system.

I also discovered there is a Prewrite pattern in SC for L-systems which might be interesting.

Thanks again for the introduction to L-systems and for sharing your code.

–Corey

Corey ( 14/11/2013 at 7:50 pm )

    Thanks for the tip! This is a nice example where the concept of lazy evaluation really shines.
    Nice.

    me ( 18/12/2013 at 3:44 pm )

In order for the L-system to be correct you should include in the code the following: after word = string_temp; string_temp = “”;. With this correction the array includes the correct symbols of the appointed L-system and does not have also the previous results of each iteration.

Thanks for sharing your code.

Petros Tatsiopoulos ( 11/02/2019 at 11:23 am )

    You are right, thanks!
    Interestingly, in later iterations of the code I have on my laptop, such line appears.
    I have meanwhile corrected the code.

    me ( 05/03/2019 at 4:44 pm )

Please Leave a Reply

Current ye@r *

TrackBack URL :

pagetop