(definstrument simp (start-time duration frequency amplitude &key (amp-env '(0 0 50 1 100 0))) (multiple-value-bind (beg end) (get-beg-end start-time duration) (let ((s (make-oscil :frequency frequency)) (amp (make-env :envelope amp-env :scaler amplitude))) (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-instrument | simp |
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) (get-beg-end start-time duration) (let ((s (make-oscil :frequency frequency)) (amp (make-env :envelope amp-env :scaler amplitude))) (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:
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:
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:
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:
then the argument bindings during the execution of simp would be:
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.
| |||||||||||||||||||||||
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:
...which has the general form:
In our simp instrument beg and end get bound to the beginning and ending samples of the note. That is what get-beg-end returns (see the documentation for get-beg-end in the CLM 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:
...which has the general form:
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.
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.
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:
(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...
|
(definstrument simp (start-time duration frequency amplitude &key (amp-env '(0 0 50 1 100 0))) (multiple-value-bind (beg end) (get-beg-end start-time duration) (let ((s (make-oscil :frequency frequency)) (amp (make-env :envelope amp-env :scaler amplitude))) (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 get-beg-end 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
|