Next Chapter
Previous Chapter
Table of Contents

Describing Music Algorithmically

Common Music provides many high level functions and macros that allow musical material to be algorithmically described. It is usually easiest to develop this sort of code in a text editor that supports Lisp evaluation and formatting. If your editor does not understand Lisp then you can still evaluate expressions by using copy/paste to Stella's prompt and then pressing the Return key.

Referencing Objects in Lisp Code

To reference a named object from Lisp, use the #! read macro. In the next example the variable foo is set to the Top-Level object:

Stella [Top-Level]: (setf foo #!top-level)
#<CONTAINER: Top-Level> 
Stella [Top-Level]: ,foo
#<CONTAINER: Top-Level> 
Stella [Top-Level]:
Use nth-object to reference a sub-object:

Stella [Top-Level]: (nth-object 0 foo)
#<THREAD: Pulse> 
Stella [Top-Level]: 
Note that nth-object indexing starts at 0.

Object Creation

The most basic support for algorithmic composition is a set of macros that provide a consistent interface to the creation musical objects. (A macro is a special type of Lisp function that implements its own syntax rules.)

The following links will take you to detailed documentation for each of the macros:

Creating Structure in Loops

Lisp's loop macro is very useful for creating musical structure but there are several problems to watch out for. Consider this first example, which does not work:

(merge m ()
  (loop for p in '(c4 c5 c6)
        for s from 0
        for n in '(nood1 nood2 nood3)
        do
        (algorithm n midi-note (start s amplitude .1)
          (setf note (item (intervals 0 2 3 5 7 8 in random from p)
                           :kill 10))
          (setf rhythm (item (rhythms e s 32 in random)))
          (setf duration rhythm))))
The composer wants to create three algorithms to execute inside a merge in which each algorithm generates a different series of notes based on the three different values of p. The code would work except for two problems. Since the algorithm name is a single symbol n, loop will create one algorithm named n three times, rather than creating three different algorithms. Each algorithm must have its own unique name or no name at all. Use name to create a new name for each new value of n. Our previous example now looks like:

(merge m ()
  (loop for p in '(c4 c5 c6)
        for s from 0
        for n in '(nood1 nood2 nood3)
        do
        (algorithm (name n) midi-note (start s amplitude .1)
          (setf note (item (intervals 0 2 3 5 7 8 in random from p)
                           :kill 10))
          (setf rhythm (item (rhythms e s 32 in random)))
          (setf duration rhythm))))
Unfortunately, this code will still not work correctly. In order to understand why we must first look at loop and algorithm a bit more closely.

More About Algorithms

In the last example a loop performed three iterations and created three algorithms. When these algorithms later run they will require the loop variable p. But when the loop finished executing the variables it established -- p,s and n -- are no longer defined. Put another way, the algorithms that survive the loop still reference a p variable that no longer exists; the algorithms have "outlived" a variable upon which they depend.

Lexical Closures

A lexical closure is a "bundling" of a function together with the environment in which it was created. A lexical closure permits a function to execute even when the function's external variables (lexical variables that are referenced but not locally declared by the function) no longer exist. In the preceding merge definition, Lisp automatically establishes a lexical closure around each algorithm such that p exists for the algorithms even though the loop that established the variable is gone.

However, in the preceding example the composer means for each algorithm to use a different value of p -- for the first algorithm p should be C4, for the second C5, and for the third C6. Lexical closures capture a variable binding (definition); this is not necessarily the same as the variable's value, since a setq changes the value but not the binding. Use the macro with-vars-snapshotted to ensure that each discrete value of a loop variable is captured:

(merge m ()
  (loop for p in '(c4 c5 c6)
        for s from 0
        for n in '(nood1 nood2 nood3)
        do
        (with-vars-snapshotted (p s n)
          (algorithm (name n) midi-note (start s amplitude .1)
            (setf note (item (intervals 0 2 3 5 7 8 in random from p)
                             :kill 10))
            (setf rhythm (item (rhythms e s 32 in random)))
            (setf duration rhythm)))))
The algorithms defined in the loop now work as expected.

The vars Declaration

The vars declaration may appear as the first form in an algorithm to declare local variables. These variables are (re)initialized each time the algorithm starts executing.

The syntax of vars is identical to the binding list of let: each form is either the name of a variable, or a binding list (variable value), where variable is the name of the variable and value is its initial value. A vars declaration is processed in sequential order so variables can refer to the variables "to their left" in their value statement. For example,

    (vars a (b 2) (c (* b 3)))
declares a to be nil, b to be 2 and c to be 6.

Here is a comparison of three different ways of defining a variable x for an algorithm:

  1. Use let to bind x to a random value when the algorithm is defined. Unless setf is used to set its value in the body of the algorithm x never changes value:

    (let ((x (random 10)))
      (algorithm foo midi-note ()
        (print x)
        ...)
    
  2. Use vars to bind x to a random value each time the algorithm is scheduled to run. Unless setf is used to set its value in the body of the algorithm x changes values only when the algorithm is restarted:

    (algorithm foo midi-note (length 100)
      (vars (x (random 10)))
      (print x)
      ...)
    
  3. Use let to bind x to a random value each time the algorithm body executes:

    (algorithm foo midi-note (length 100) 
      (let ((x (random 10))
        (print x)
        ...)
    
The following example uses vars to define a ritardando algorithm with a different length each time it performs. The len variable is initialized to a value between 5 and 30. This value is used as the length of a note stream. When the note stream is finished the algorithm stops. The count slot is used to compute the ritardando as it ranges in value from 0 to len-1.

(algorithm ritardando midi-note (amplitude .9)
  (vars (len (between 5 30)))
  (setf note (item (notes c4 d ef for len) :kill t))
  (setf rhythm (interpl count 0 .1 (1- len) .3))
  (setf duration rhythm))

Stella [Top-Level]: seq ritardando
Start time offset: (<cr>=None) 2
Number of times to sequence: (<cr>=1) 5
Length of pause between selections: (<cr>=None) 1

Stella [Top-Level]:
Here is a more complicated, but very elegant, example written by Tobias Kunze ( tkunze@ccrma.stanford.edu)

;;; A third-order recursive cellular automaton.
;;; The algorithm maintains three past note values 
;;; to compute each new note based on the formula:
;;;
;;; (+ last
;;;    (* (- 12 (abs x)) (if (>= x 0) -1 1))
;;;    1)
;;; 
;;; where x represents the interval from the oldest to the 
;;; second-to-oldest note.  Sounds best with a softly 
;;; reverberated percussive sound (vibe, harp or piano).  Set 
;;; length to some higher number (ca. 1000 or more) to see that 
;;; this generates up to 24 different patterns in lots of 
;;; different phrases

(algorithm cell midi-note (length 200 rhythm .1 duration .5 
                                  amplitude .5)   
  (with-past-values ((note 3 60 60 60))
    ;; convert oldest interval to inverse complement
    (let ((width (- (- (past-value note 3) (past-value note 2)))))
      (incf width (if (>= width 0) -12 12))
      ;; transpose by last note. if the new note is out of
      ;; bounds shift it up or down and increment by whole step
      ;; otherwise increment by half step
      (incf width (past-value note 1))
      (setf note 
            (cond ((< width 36) ; raise 1 to 5 octaves+2 steps
                   (+ width (* 12 (between 1 6)) 2))  
                  ((> width 98) ; lower 1 to 5 octaves-2 steps
                   (- width (* 12 (between 1 6)) -2)) 
                  (t (+ 1 width)))))))

Stella [Top-Level]: mix cell 1
Stella [Top-Level]:

Creating Structure Dynamically

Up to now we have used various constructor macros like merge, thread and algorithm to create objects while working in Lisp. These objects are then run by commands like mix to produce musical events. But what if we wanted to create structure during event processing, after mix has been envoked? A mute is particularily useful in this context because, although it execute statements, it contributes no events itself.

The next example defines a mute named Foo that prints its current count and time values as it executes:

(mute foo (length 4 rhythm .5)
  (format t "~%Count=~S, time=~S" count time))

Stella [Top-Level]: mix foo
Start time offset: (<cr>=None) <cr>

[Foo executes but no sound results]

Count=0, time=0.0
Count=1, time=0.5
Count=2, time=1.0
Count=3, time=1.5
Count=4, time=2.0

Stella [Top-Level]: 

Sprouting New Structure

A mute can create objects and schedule them to start executing at the same time or later than the current time of the mute. Use sprout to insert a new object into the current runtime environment

To avoid sprouted objects appearing in the Top-Level container specify nil as thir name. Unnamed structure is is never included in Top-Level.

Here is a example of a mute that sprouts 6 anonymous algorithms. Each time the mute executes it binds the variable off to a new pitch and the repeat facto rep to a value between 10 and 20.

(mute ma (rhythm 2)
  (let ((off (item (degrees c3 c4 c5 ) :kill 2))
        (rep (between 20 30)))
    (sprout
      (algorithm nil midi-note (rhythm .2  duration .175  
                                start (+ time (random .05))
                                amplitude .5)
        (setf note 
          (item (intervals 0 2 3 5 7 8 9 in heap 
                           from off for rep returning note)
                :kill t))))))

Stella [Top-Level]: open test.midi
Stream: #<File: "test.midi">.
Stella [Top-Level]: mix ma 0
Play file test.midi? (<cr>=Yes) 

Encapsulation

If the definition of an algorithm is placed inside the definiton of a lisp function a powerful and reusable compositional abstractions is created: each time the function is called it creates a new algorithm and passes it whatever data the composer specified as parameteres to the function. The next example uses this technique, called encapsulation, to implement a musical fractal (inspired by Sierpinski's gasket) in which each tone in a three note pattern serves as the foundataion for the same pattern to be replicated at a faster tempo. The function sierpinski is then called to create the main algorithm. When Main runs it produces an note and calls sierpinki to recursively sprout a new version of itself based on its current note and time.
(defun sierpinski (nam dur key amp rep &optional (tim 0))
  (algorithm (name nam) midi-note (start tim rhythm dur amplitude amp)
    (setf note (item (intervals 0 11 6 from key) :kill t))
    (when (> rep 1)
      (sprout (sierpinski nil (/ rhythm 3) note amp (1- rep) time)))))

Stella [Top-Level]: (sierpinski 'main 12 'c2 .5 5)
#<Algorithm: Main>
Stella [Top-Level]: mix main 0

Stella [Top-Level]: 
One interesting extenstion to the function might be for the algorithm to pass a list of intervals in addition to an offset, that way the pattern could change each time sierpinski is called.

Next Chapter
Previous Chapter
Table of Contents

Last Modified: 6-Mar-1998