Composing with the logistic map
The following code, is an example of how to generate rhythm, set and pitch palettes, as well as maps, based on the Logistic Map (LM).
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun logistic-map (x r map n &key (exp 1.0) (offset 0.0) (round t)) (unless (and (= x 0.0) (= r 0.0)) (error "chaos: First argument should be in the [0;1] range; second ~ argument should be in the [3;4] range.")) (loop for i from 1 to n for result = (+ offset (* map (expt (setf x (* r (* x (- 1.0 x)))) exp))) collect (if round (round result) result))) (defun gen-set-palette (lower upper elements x r &key (exponent 1.0)) (let* ((range (- (note-to-midi upper) (note-to-midi lower)))) (cons (loop for i from 0 to (- elements 1) for init in (logistic-map x 4 1 elements :round nil) for leap in (logistic-map x 4 range elements) for offset = (+ (note-to-midi lower) (first (logistic-map init 4 (- range leap) 1))) collect (list i (list (remove-duplicates (mapcar 'midi-to-note (sort (loop for midi in (logistic-map init r leap 20 :exp exponent) collect (note-to-midi (+ midi offset))) #'<)))))) '(:recurse-simple-data nil)))) (defun gen-rsp (timesig smallest elements x r &key (exponent 1.0)) (loop for i from 0 to (- elements 1) for init in (logistic-map x 4 1 elements :round nil) collect (list i (let* ((rsp '()) (sum 0) (notes 0)) (loop for x in (remove 0 (logistic-map init r (* 2 smallest) 1000 ;double it to have a bipolar output :exp exponent :offset (* -1 smallest))) summing (/ 1.0 (abs x)) into tot ; accumulate while (< tot (/ (first timesig) (second timesig))) ; check if the accumulated values do ; exceed bar length do (setf sum tot) if (< x 0) ; attack if positive, rest if negative do (setf rsp (cons (list (abs x)) rsp)) else do (setf rsp (cons x rsp))) (setf rsp (cons ; when notes exceed bar length, (/ 1 ; fill the remaining part (rationalize (- (/ (first timesig) (second timesig)) sum))) rsp)) (setf rsp (list (cons timesig (reverse rsp)))) ; correct order (loop for i in (first rsp) when (numberp i) do (incf notes)) ; accumulate number of attacks (setf rsp (list rsp ':pitch-seq-palette (list ; generate pitch curve (logistic-map init r 10 notes)))))))) (let* ((seed .7342) (r 4) (bars 20) (sets 20) (seqs 20) (logistic-map-test (make-slippery-chicken '+logistic-map-test+ :ensemble '(((hand0 (piano :midi-channel 1)) (hand1 (piano :midi-channel 1)))) :tempo-map '((1 (q 60))) :set-palette (gen-set-palette 'c2 'c4 sets seed r) :set-map `((1 ,(logistic-map seed 4 (1- sets) bars))) :rthm-seq-palette (gen-rsp '(2 4) 32 seqs seed r) :rthm-seq-map `((1 ((hand0 ,(logistic-map .24324 r (1- seqs) bars)) (hand1 ,(logistic-map .34312 r (1- seqs) bars)))))))) (re-bar logistic-map-test :min-time-sig '(4 4) :auto-beam 'q) (midi-play logistic-map-test) (cmn-display logistic-map-test)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
The LM is an iterated nonlinear function which, under specific conditions, can generate chaotic behaviours.
This is its form:
x_n+1 = r * x_n * (1 – x_n).
x_n (in the [0;1] range) is the initial condition, while r is sometimes called "growth rate". r (in the [0;4] range) is the most important parameter in the LM as it determines the degree of repetition/unpredictability in the sequence of values (in the [0;1] range) generated by the function. The x_n parameter can also have significant effects for some specific values, although it will generally create different outputs while the function keeps an overall consistent behaviour depending on r.
Recently, I've composed a piece for piano in occasion of the Slippery Chicken symposium at Goldsmiths and I've decided to create it entirely using the LM as an algorithmic technique. The reason is that my research is based on feedback, and I wanted to be consistent even in the context of non-realtime systems, which is different than my usual one. Besides, the LM seemed to offer an ideal solution since, in general, my idea of music creation implies varying structures which still keep a unique identity and overall character. This, indeed, could be achieved by controlling the x and r parameters: r would determine the overall infrastructure of the piece, while x could generate different versions of the same work.
For my piece, I also decided to have nested levels of unpredictability, meaning that the output of some LM(s) would control the values of x and r of other LM(s) used for the generation of the SC data. In this example, though, most of the values are fixed and there is perhaps only one case of nested LM(s).
Here, I'm showing how the LM can be used to algorithmically generate palettes and maps.
The LOGISTIC-MAP function takes four arguments, has three keywords, and returns a list of numbers. The four arguments are: initial value (x); rate (r); mapping range (map); list length (n). The three keywords determine the exponent to which the sequence will be raised to (before being mapped to its range), an offset, and whether the values are integers or floating point numbers. The reason why I decided to add an exponent keyword is that a [0;1] range is convenient in this case as it is possible to push the sequence towards either of the two extremes, depending on exponents which are greater than 1 or between 0 and 1, without exceeding its boundaries. Please note that the sequence generated by the logistic-map function starts at x_n+1.
The GEN-SET-PALETTE function takes five arguments, has one keyword, and returns a SC set palette. The five arguments are: lowest note in the sets (lower, a symbol); highest note in the sets (upper, a symbol); the number of sets in the palette (elements); the initial value of the LM (x); the LM rate (r). The keyword sets an exponent which is then passed to the LM function. For example, using an exponent greater than 1 will push the notes in the sets towards to lower limit, whereas an exponent within 0 and 1 would push the notes towards the upper limit. Please note that, internally, there is a LM-dependent offset which shifts each set within the lower and upper limits.
The GEN-RSP function takes five arguments, has one keyword, and returns a SC rhythm sequence palette. The five arguments are: a time signature (timesig, a two-integer list); the smallest possible duration for an attack or rest (smallest, a denominator); the number of sets in the palette (elements); the initial value for the LM (x); the LM rate (r). The keyword sets an exponent which is then passed to the LM function. Internally, the mechanism is rather trivial and it maps the sequence of the LM function to a range which goes from -smallest to smallest. If the resulting value is a negative value, that is added to the set as a rest, otherwise, it is added as an attack. Some conditionals will make sure that the set is filled with the right durations, and the right number of pitch points will be generated based on the LM. For example, the argument r, here, could be used to determine the regularity of durations as well as the repetition of notes, while the exponent could be used as a density index considering that it would push the sequence towards the negative or positive side of the range.
The rest of the code is also very straightforward and it just creates a very simple example to give you a basic idea. Hopefully, the GEN-RSP will eventually become something more advanced, capable of generating tuplets and other more articulated rhythms. The GEN-SET-PALETTE, too, could easily be improved by, for example, giving the possibility to set varying limits for each set. And as I already mentioned, extending these algorithms in order to implement nested LM could also result in more interesting outputs.
I still hope that this will be useful for some of you.
Thanks to Michael and Dan for putting together this event which was a very productive and enjoyable experience with nice people and good music.