CCRMA
Anatomically Correct Lisp


Dissecting simp.ins, an excersice in lisp anatomy

updated for clm-2

Well, here we are, this is the body:

(definstrument simp (start-time duration frequency amplitude 
		             &key (amp-env '(0 0 50 1 100 0)))
  (multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((s (make-oscil :frequency frequency))
          (amp (make-env :envelope amp-env :scaler amplitude 
                         :duration duration)))
      (run 
       (loop for i from beg to end do
         (outa i (* (env amp) (oscil s))))))))

We'll go through each word of this fragment of lisp code while trying to explain exactly what it means and how it works. Read carefully as there's a LOT of information here and most of the lisp knowledge that you'll need to be able to read, modify or create simple CLM instruments. Good luck in your exploratory tour of "simp".

An instrument definition parallels a function definition in Common Lisp. That is, the syntax and semantics (meaning) of each part of a definstrument is the same as in a defun (the Common Lisp function definition macro). The most basic description of an instrument (or function) definition is:

(definstrument name-of-instrument (list-of-arguments)
   one-or-more-lisp-forms
)

In our trivial example:

name-of-instrumentsimp
list-of-arguments start-time duration frequency amplitude &key (amp-env '(0 0 50 1 100 0))
one-or-more-lisp-forms
(multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((s (make-oscil :frequency frequency))
          (amp (make-env :envelope amp-env :scaler amplitude 
                         :duration duration)))
      (run 
       (loop for i from beg to end do
         (outa i (* (env amp) (oscil s)))))))

name-of-instrument Not much to say about the name-of-instrument part except to note that a CLM instrument definition is functionally equivalent to a Common Lisp function definition. To render a new note just call the instrument as you would a lisp function, that is: "(name-of-instrument actual-arguments)". Of course an instrument is supposed to create digital sound samples as a side effect of its execution, but that's another story.

list-of-arguments Let's talk a bit about what's inside the list-of-arguments. The example instrument defines four required arguments and one key argument.

required arguments Required arguments are defined by a list of space-separated names. The first four names in our definition (start-time duration frequency amplitude) define four required arguments. This means that in each call to the instrument we will have to provide four arguments, otherwise the lisp interpreter will complain with an appropriate error message. For example, when executing the following call to simp with just three arguments:

USER(6): (simp 0 1 440)
Error: SIMP got 3 args, wanted at least 4 args.
  [condition type: PROGRAM-ERROR]
[1] USER(7): :reset
USER(8): 

we get an error message saying that simp got only three arguments when it was expecting four. So simp (as defined above) has to be called with four arguments.

key arguments Arguments defined after the &key symbol are called key arguments (there's only one key argument defined in our simp instrument). Key arguments are optional. If they are not provided in the instrument call they take a default value if it is present in the definition of the argument (in the case of "amp-env" its default value is the list "'(0 0 50 1 100 0)"). Key arguments are added to an instrument call by preceding their name with a ":" and appending the resulting key (the name) and a value after the required parameters. For example we could change the default value of "amp-env" for a particular note by saying:

(simp 0 1 440 0.1 :amp-env '(0 0 0.01 1 1 0))

The importance of key arguments will become apparent when you realize that you can define a lot of them, provide reasonable default values and only override the ones you're interested in changing in a particular instrument call. Furthermore, key arguments are not positional and can be defined to mean something so that instrument calls are self-documenting if the names are well chosen.

actual arguments The actual arguments passed in a call to an instrument will be bound (associated with) at runtime to the symbols that define them, so that in our previous example the following bindings will exist during the execution of the simp instrument:

start-time 0
duration 1
frequency 440
amp 0.1
amp-env '(0 0 0.01 1 1 0)

This means that for that particular call of the simp instrument start-time will take the value of "0", duration will be "1" and so on and so forth.

If we were to ommit our key argument as in:

(simp 0.5 2.3 441 0.2)

then the argument bindings during the execution of simp would be:

start-time 0.5
duration 2.3
frequency 441
amp 0.2
amp-env '(0 0 50 1 100 0)

Note that amp-env reverts to the default value as defined in the instrument. In short, arguments (both required and key) are the communication channel between the score and the innards of the instrument. Whatever you want to control you have to turn into an argument to the instrument.

style note It is possible to control the innards of an instrument through what is called special variables (or in non-lisp terminology global variables). Try to avoid them. Sometimes they are the answer but most of the time you want to make the relationship of arguments to behavior of the instrument very explicit. Global variables hide behavior and make bugs difficult to find.

one-or-more-lisp-forms This is where the actual executable code resides. A lisp form is a lisp function or macro call (for the purposes of this tutorial let's assume that functions and macros are the same thing). Our simp instrument has just one form in its body. It is a multiple-value-bind:

(definstrument simp (start-time duration frequency amplitude 
		             &key (amp-env '(0 0 50 1 100 0)))
  (multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((s (make-oscil :frequency frequency))
          (amp (make-env :envelope amp-env :scaler amplitude
                         :duration duration)))
      (run 
       (loop for i from beg to end do
         (outa i (* (env amp) (oscil s))))))))

...which has the general form:

(multiple-value-bind (list-of-names) function-call
    one-or-more-lisp-forms
)

multiple values? All lisp functions return a value. The returned value can be anything, a number, a list, a string, you name it. Functions can also be made to return multiple values, that is, return more than one "thing" as a result of being executed. Which is not the same thing as, let's say, returning an array of "things"... which in itself is just one "thing"! Very confusing...
What does it do? A multiple-value-bind evaluates the function-call and binds the multiple values returned by the function to the list of names provided. It then proceeds to evaluate all the forms in its body with those bindings active. In other (non-lisp) words it creates local variables that exist only within the enclosing parenthesis of the multiple-value-bind.

In our simp instrument beg and end get bound to the beginning and ending samples of the note. That is what times->samples returns (see the documentation for times->samples in the CLM-2 manual).

So now it is time to see what's inside the multiple-value-bind. Again we find only one form in its body and it's a let:

(definstrument simp (start-time duration frequency amplitude 
		             &key (amp-env '(0 0 50 1 100 0)))
  (multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((s (make-oscil :frequency frequency))
          (amp (make-env :envelope amp-env :scaler amplitude 
                         :duration duration)))
      (run 
       (loop for i from beg to end do
         (outa i (* (env amp) (oscil s))))))))

...which has the general form:

(let ((name lisp-form)
      (name lisp-form)
      ...
      (name lisp-form))
    one-or-more-lisp-forms
)

What does it do? A let evaluates each lisp form and binds it to the name it is paired with. It then proceeds to evaluate all the forms in its body with those bindings active. In other (non-lisp) words it creates local variables that exist only within the enclosing parenthesis of the let.

There are two flavors of let. The plain let we're using in the example, and let*. The difference lies in the order in which the lisp-forms are evaluated. In the plain let all the lisp-forms are evaluated sequentially and then they are all bound in parallel with their corresponding names. In the let*, the first lisp-form is evaluated and then bound with its corresponding name, then the second form is evaluated and bound with its name and so on and so forth. You normally use let* as most of the time latter bindings depend on the values of previous ones.

In our case our let defines two bindings (or local variables):

name bound to the result of evaluating:
s (make-oscil :frequency frequency)
amp (make-env :envelope amp-env :scaler amplitude :duration duration)

And now finally AT LAST we get to some musically significant stuff! It was about time! So far we have been getting ready to do things, now is the time to do something. Anyway, these two bindings create two unit generators that will be connected together to create the digital sound samples.

A unit what? Unit generators are high level building blocks that are designed to hide complexity from the instrument builder. A single unit generator can be something as simple as a sine wave oscillator or something as complex as a Fast Fourier Transform or multitap digital filter. The point is that you don't have to deal with the internal implementation details, you just create them, connect them together and it works!

Each unit generator in CLM is defined by two functions. The first function usually goes by the name make-whatever and creates a data structure that defines the unit generator. The second function, by the name whatever, uses the data structure to create or process samples.

So s is now bound to a data structure that defines a sine wave oscillator. The frequency of the oscillator is set to the value of the instrument argument frequency by the key argument :frequency. And amp is an envelope. The shape of the envelope is defined by the amp-env key parameter and the overall scaling of the envelope (which will define the overall volume of the sine wave) is defined by the amplitude required argument.

So now we have two unit generators, an oscillator and an envelope. Time to go to the body of the let and see what is actually being done with them.

(definstrument simp (start-time duration frequency amplitude 
		             &key (amp-env '(0 0 50 1 100 0)))
  (multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((s (make-oscil :frequency frequency))
          (amp (make-env :envelope amp-env :scaler amplitude
                         :duration duration)))
      (run 
       (loop for i from beg to end do
         (outa i (* (env amp) (oscil s))))))))

The body of the let is yet again just one lisp form. And this is literally the heart of the instrument. Where the real work of calculating all those thousands of samples is done. The run macro surrounds a loop. Each time the loop is traversed a new sample is created by the code contained within the loop. The loop has an index variable (i) that takes values that go from beg (the ordinal number of the first sample of the note) to end (the number of the last sample of the note). So, in effect, the loop is executed once per output sample.

The actual body of the loop is quite simple:

(outa i (* (env amp) (oscil s))))))))

(oscil s) is a function that returns the next sample of the sine wave oscillator each time it is executed. (env amp) returns the next value of the amplitude envelope each time it is executed. Both numbers are multiplied together and the result is merged into the first (A) channel of the output soundfile by the outa macro. outa's first argument is the sample number where the sample is to merged.

End of story...

Summarizing:
(definstrument simp (start-time duration frequency amplitude 
		             &key (amp-env '(0 0 50 1 100 0)))
  (multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((s (make-oscil :frequency frequency))
          (amp (make-env :envelope amp-env :scaler amplitude
                         :duration duration)))
      (run 
       (loop for i from beg to end do
         (outa i (* (env amp) (oscil s))))))))

This code defines an instrument named simp. It is controlled through four required arguments and one optional key argument. The first two arguments define the start time of the note in seconds in the current output soundfile and its duration, also in seconds (those two arguments are latter used by the times->samples function). The third argument defines the frequency of the sine wave oscillator and the fourth the overall amplitude scaler of the output. The key argument defines the shape of the amplitude envelope of the note. So much for the semantics of the five arguments.

Four local bindings are created for the duration of the instrument call. The first two, created by the multiple-value-bind function, are the beginning and ending sample numbers of the instrument call (and are defined by the start-time and duration arguments). The last two, created by a let, define the sine wave oscillator and an envelope that will be used to control its amplitude.

The heart of the instrument, the run macro, surrounds a loop that executes once per each output sample. In the body of the loop the next sample of the oscillator is multiplied by the next value of the amplitude envelope and is merged (by outa) at sample "i" of the current output soundfile.

...stay tuned... ... more to come...


©1998 Fernando Lopez-Lezcano. All Rights Reserved.
nando@ccrma.stanford.edu