Next Chapter
Previous Chapter
Table of Contents

Describing Music Algorithmically

It is sometimes easier to compute objects than it is to create them using commands. Common Music provides many high level functions and macros that allow musical material to be algorithmically described. These forms are by definition Lisp code, so it is usually easiest to use a text editor that supports Lisp evaluation and formatting (for example, Emacs and Fred) when working in this manner. If the editor you are using does not support Lisp, then you can "evaluate" by copying text, pasting it to Stella's prompt and the pressing the Return key.

Referencing Objects in Lisp Code

To reference a named object from Lisp, use the #!name dispatch macro, wherename is the name of the object. 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 six Lisp macros that create musical objects. A macro is a special type of Lisp function that implements its own syntax rules.

The following six sections explain the syntax of each of these macros in detail.


object {class} {slot value}*                          [Macro]
Creates musical event data. class is the class of event to create. Following class comes zero or more slot initializations. Each initialization is a pair {slot value} where slot is the name of a slot in the object and value is its evaluated value.

In the next example a midi-note is created with a rhythm of .1 and a note of C4.

Stella [Top-Level]: (object midi-note rhythm .1 note 'c4)
#<MIDI C4  0.1 unset unset 0>
Stella [Top-Level]:

thread {name} ({slot value}*) {form}*                 [Macro]
Creates a thread object.name is the name for the thread. Following name comes a list holding zero or more slot initializations, as described under object macro. If no slots are to be initialized then the initialization list is written (), the empty list. Otherwise, the list can contain initializations for the following slots, which are also available for merge, algorithm and generator objects:

start {number}
A local start time (seconds) for the object.

initializer {function}
A function to call before the object begins output processing.

finializer {function}
A function to call just after the object finishes output processing.

Following the initialization list comes the body of the thread definition. Objects created inside the body will automatically become sub-objects of the new thread.

The next example creates a thread named Test holding 30 midi-note objects:

(thread test ()
  (loop repeat 30 do
    (object midi-note note (between 40 80)
                      rhythm (pickl .1 .2 .4)
                      duration .2 
                      amplitude .4)))


heap {name} ({slot value}*) {form}*                   [Macro]
Creates a heap object. A heap is a type of thread that, when called upon to produce objects, first randomly reorders them. Marco arguments are the same as for thread.


merge {name} ({slot value}*) {form}*                  [Macro]
Creates a merge object. A merge processes its sub-objects in parallel, using a scheduling queue. The sub-objects should all be containers, i.e. other threads, merges, algorithms or generators. Macro arguments are the same as for thread.

The next example creates a merge named M with two sub-threads. Sub-thread T1 holds two notes and T2 holds 10 notes.

(merge m ()
  (thread t1 () 
    (object midi-note note 'c3 rhythm .5 duration 1 amplitude .7)
    (object midi-note note 'fs3 rhythm .5 duration .5 amplitude .7))
  (thread t2 () 
    (let ((r .1)(d .2)(a .5))
      (doitems (x (notes c4 d ef f g in random for 10))
        (object midi-note note x rhythm r duration d amplitude a)))))

Stella [Top-Level]: mix m 2
Stella [Top-Level]: 

algorithm name type ({slot value}*) {form}*           [Macro]
Creates an algorithm object. An algorithm computes musical events given a class specification and a program for changing slot values as the algorithm executes. name is the name of the algorithm. class is the class of musical event that the algorithm will create. Following class comes a slot initialization list containing zero or more initializations. In addition to those initializations discussed under thread, algorithm initializations may also include entries for event class slots, and for two slots provided by the algorithm class itself:

length {integer}
The number of times the algorithm executes before stopping.

end {number}
The last start time (in seconds) for the algorithm before stopping.

An algorithm differs from a thread, merge or heap in two important ways:

  1. The events produced by an algorithm are computed during output processing, not when the algorithm is created.
  2. The events produced by an algorithm are not saved after they are computed. An algorithm actually creates only one event and then "side effects" this object each time a new event is required. Reusing a single event is many times more efficient than creating a new object each time one is needed. To generate persistent events, use the generator macro.

(algorithm pulse midi-note (length 80 rhythm .1 duration .1)
  (setf note (item (notes c4 d ef f g af bf c5 in random)))
  (setf amplitude (interpl (mod count 8) 0 .25 7 .75)))

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

generator name type ({slot value}*) {form}*           [Macro]
Creates a generator object. A generator saves the events it computes but is like an algorithm in all other respects. When a generator executes, it first checks to see if it already has events. If it does, it processes them in sequential order like a thread. If a generator does not have events, or if the generator has been set "unfrozen", the current events (if any) are thrown away and a new set will be computed and cached the next time the generator is run.

(generator g midi-note (amplitude .5)
  (setf note (item (steps 1 2 3 in random for 10 from 'c4)
                   :kill t))
  (setf rhythm (item (rhythms s e q in random)))
  (setf duration rhythm))

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

[Mixes sounds identical despite random notes]

Stella [Top-Level]: unfreeze g
G unfrozen.
Stella [Top-Level]: mix g 1

[Mix sounds different]

Stella [Top-Level]: 

Creating Structure in Loops

Lisp's loop macro is very useful for creating musical structure, but there are a few potential problems to watch out for. Consider this first example, which doesn't work:

(merge m ()
  (loop for p in '(c4 c5 c6)
        for s from 0
        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 new algorithms to run inside a merge, where each algorithm generates a different series of notes according to the three different interval stream offsets in p. The code would work except for two bugs:
  1. Since the algorithm name is a single symbol n, loop would create an algorithm named n three times, rather than creating three different algorithms. To create three algorithms, each algorithm must have its own unique name, or no name at all. Use the name function to create a new name each time the loop interates:


    name name &optional new                               [Macro]
    
    Used in place of a symbolic name to specify a name for an object. Ifnew is nil (the default), name returns name as its value. Otherwise, name serves as a root to generate a new unique name.

    Using name, 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))))
    
  2. 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. Once the loop finished executing the variables it established, p, s and n, no longer exist since these variables were only defined within the lexical scope of the loop form itself. But inside the algorithms that were defined by the loop there are references to p; the algorithms that were created by the loop process reference a p that is no longer defined. Put another way, 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 referenced but not locally declared by the function) no longer exist. In the preceding merge definition, Lisp automatically established a lexical closure around each algorithm such that p exists for the algorithms even though the loop that established the variable p is no longer in effect.

However, note 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's binding (definition); this is not necessarily the same as a variable's current value. To ensure that each individual value of a lexical variable is captured, use the macro with-vars-snapshotted:


with-vars-snapshotted ({var}+) {body}*                      [Macro]
Insures that forms in body reference a unique binding of one or more var.

(merge m ()
  (loop for p in '(c4 c5 c6)
        for s from 0
        for n in '(nood1 nood2 nood3)
        do
        (with-vars-snapshotted (p n s)
          (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 will 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.

vars {variable | (variable value)}*                         [Macro]
The syntax of the vars declaration is identical to the binding list syntax of let: each variable 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. The entries in the vars declarations are processed in sequential order, so variables can use definitions "to their left" as values. 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 named x:

  1. vars defines x to hold a single random value for the course of the algorithm's execution. x has the possibility of changing values only when the algorithm is restarted:

    (algorithm foo midi-note (length 100)
      (vars (x (random 10)))
      (print x)
      ...)
    
  2. let defines x to hold a random value each time the algorithm executes a note:

    (algorithm foo midi-note (length 100) 
      (let ((x (random 10))
        (print x)
        ...)
    
  3. let defines x to be a random value when the algorithm is defined. x never changes value thereafter:

    (let ((x (random 10)))
      (algorithm foo midi-note ()
        (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 cnt 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 structure while working in top-level Lisp. These objects were then "processed" by commands like mix to produce musical output. But what if we wanted to create structure during processing, i.e. after musical time 0? To do this we need an object to serve as our "delegate". This delegate will create new structure as part of its own processing but will otherwise be silent.


mute {name} ({slot value}*) {form}*                   [Macro]
Creates a silent algorithm. 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 be used to create objects and schedule them to start executing at or later thanthe current time of the mute. Use the spout function to add new objects into the current runtime environment


sprout object                                      [Function]
Inserts object into the scheduler, which defaults to the outermost executing merge.


Creating Anonymous Structure

To avoid sprouted objects appearing in the Top-Level container, specify nil as the name for an object when it is created. Unnamed structure is ignored by Top-Level.

Here is a example of a mute that sprouts 6 algorithms. Each time the mute executes it binds the variable off to a new pitch offset and 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) 

A More Complicated Example

Here is a final example that uses mute, sprout and loop in one fell swoop:

(mute x ()
  (setf rhythm (item (rhythms q h h. w in random)
                     :kill 2))
  (let (pitch)
    (setf pitch (note (between 100.00 300.00)))
    (loop for beg from 0 by 1.5
          repeat (+ 1 (random 4))
          do 
      (format t "Sprouting new algorithm at ~A to ~A~&" 
              (+ time beg) (+ time beg 5))
      (sprout
        (algorithm nil midi-note (start (+ time beg)
                                  end   (+ time beg 5)
                                  amplitude .25)
          (setf note (transpose pitch (+ -3 (random 7))))
          (setf rhythm (+ .05 (random .15)))
          (setf duration rhythm))))))

Stella [Top-Level]: mix x 0
Sprouting new algorithm at 0.0 to 5.0
Sprouting new algorithm at 1.0 to 6.0
[...output elided]
Sprouting new algorithm at 12.0 to 17.0
Sprouting new algorithm at 13.5 to 18.5
Play file /user/hkt/test.midi? (<cr>=Yes) 
Stella [Top-Level]:

Next Chapter
Previous Chapter
Table of Contents