NoteFun: music and continuous modulation, a tutorial

Written by Michiel Borkent in collaboration with Peter Desain

Introduction

This tutorial provides information about the usage of NoteFun. This system is implemented in Lisp. We have tried to reduce the amount of programming-related issues in this article, although all the things you know about Lisp programming will probably make you a better composer in NoteFun.
To read more about the workings and implementation of NoteFun, I would like to redirect you to my internship report or the website about my work related to MMM (my internship and final thesis project)

Tutorial

Discrete musical events

In electronical composition one needs to specify elementary musical events which together constitute a musical piece. The most elementary musical events in NoteFun are a note and a pause. For example, (note :pitch 58 :duration .5) and (pause 1.5) are valid events in NoteFun. The default pitch and duration of a note are 60 and 1 respectively. This means that you can write (note) instead of the longer (note :duration 1 :pitch 60). As you can see, the order of the attributes which you specify does not matter. This is because each attribute goes along with a keyword, indicated by a colon as the first character. In fact, note has a lot more attributes which we will ignore for now, as we can use their default values for the moment. The number which indicates the pitch corresponds to semitones, whereas the middle-C is indicated by 60.

In composition, like the word indicates itself, we constitute a work out of more basic objects than the work itself. In our case this means that we would want to make groups of notes that are somehow related. In NoteFun you can make a group of notes in two ways. One way is to make a sequence of notes and pauses. For example, we could think of the first line of Are you sleeping (in dutch this song is known as Vader Jacob) as a time-wise sequence of the notes c,d,e and again c. In NoteFun we could compose this as

 (seq	(note)
     	(note :pitch 62)
     	(note :pitch 64)
     	(note)) 
 
seq can take any number of notes and groups of notes. For example, we could repeat the first line of Are you sleeping twice, just like in the song itself:
(seq (seq (note :pitch 60)
          (note :pitch 62)
          (note :pitch 64)
          (note :pitch 60))
     (seq (note :pitch 60)
          (note :pitch 62)
          (note :pitch 64)
          (note :pitch 60)))
You already can see right now that composition of the complete version of Are you sleeping would result in a long and unreadable list of 'seqs' and 'notes'. This problem can be overcome by making a reference to an expression. For example, we could name the first line of our song line1. This is done by the following:
(setq line1 (seq (note)
                 (note :pitch 62)
                 (note :pitch 64)
                 (note)))
Now we can represent the double repetition of line1 as:
(seq line1 line1)
which of course looks a lot cleaner. The second way of grouping notes is ordering them parallel in time. This can be done with sim which stands for simultaneous. For example a major-C triad can be written as
(sim (note) (note :pitch 64) (note :pitch 67))
and the second inversion of the major-G triad as
(sim (note :pitch 59) (note :pitch 62) (note :pitch 67))
To abbreviate these chords we write:
(setq c-chord (sim (note) (note :pitch 64) (note :pitch 67)))
(setq g-chord (sim (note :pitch 59) (note :pitch 62) (note :pitch 67)))
Note that with our little bit of references we can already write a simple yet nice arrangement for the beginning of Are you sleeping:
(setq line1-arrang (seq c-chord g-chord c-chord c-chord))
(setq ays (sim (seq line1 line1) (seq line1-arrang line1-arrang)))
(play ays)
With play we send our composition to a connected midi-synthesizer which then interprets it into sounds. If play is somewhat problematic in your set-up, you might want to use save instead, which lets you save the musical data as a standard midi file which you can later use in whatever manner you like. To hear the example above, listen to this sample:

As you can hear, the melody and the chords get a bit in each others way. It would be nice if we could transpose line1-arrang one octave lower. No problem. We do it with the following construction:

(setq line1-arrang-transposed (trans -12 line1-arrang))
(setq ays (sim (seq line1 line1) 
               (seq line1-arrang-transposed 
                    line1-arrang-transposed)))
(play ays)
trans takes a number (to be more general, trans takes not only a number but also a GTF) and a musical object which is decreased or increased in pitch, according to the number. The number represents a number of semitones as you might have guessed. -12 then indicates "one octave lower". To hear the example above click here:

The standard program number (which indicates an instrument in the General Midi scheme) of a note is 80. It would be nice if we could use one instrument for melody and a different one for the rest of the arrangement. Let us change ays in that we have piano for the melody and strings for the chords. First let us explain how to change the instrument for a single note and after that how to change it for a group of notes. Actually the first can also be done by passing a keyword with a value to note:

(note :program 49)
The above note sounds as a middle C of one second long, played by a string ensemble. It would be a tedious job to specify the non-default instrument per note. That is why we will now introduce for-all-notes that lets us express that all notes in the chords must be played by the string ensemble:
(setq line1-arrang (for-all-notes :program 49 line1-arrang))
As you might know, two different instruments cannot be played at the same midi channel. That is why we have to explicitly specify that the chords have to be played at an other channel than the melody, which will be played at the default channel (number 0).
(setq line1-arrang (for-all-notes :channel 1 line1-arrang))
Please note that when binding a variable to an expression in which the same variable is used can be tricky. Suppose we had written this:
(setq line1-arrang (trans -12 line1-arrang))
After executing this for the fist time line1-arrang will be transposed one octave down. After you execute the same code again, line1-arrang will be transposed one more octave down. This might be obvious but it is recommended to use these kinds of constructs with special attention. Finally we are ready to play the melody and the chords together, both using their own instruments. Note that we have to evaluate the following expression again:
(setq ays (sim (seq line1 line1) (seq line1-arrang line1-arrang)))
before we can use ays as the new composition with multiple instruments, because ays has to be build up with the new line1-arrang. To hear the example above click here: One more remark on for-all-notes is that you can pass multiple keyword/argument pairs at once. We could have done the last two operations on line1-arrang in this shorter way:
(setq line1-arrang (for-all-notes :channel 1 :program 49 line1-arrang))
A very interesting operation which will bring us to the continuous extension of this still discrete world of notes is stretch. You might have had the feeling that ays is still a bit too slowly played. Wouldn't it be nice to shorten the whole musical object at once, instead of specifying a shorter duration for each note and building a new ays? Then try this:
(play (stretch .5 ays))
It hopefully sounds a lot better. To hear it click here:

Extension towards continuous control

Simple GTFs

Now we will show how to use functions of start, duration and time to control a notes internal workings. A nice example to begin with is a glissando from one note to another. Let us have two notes with a pause in between, which we will later replace with the glissando.
(draw (seq (note) (pause 1) (note :pitch 62)))
draw draws a piano roll representation of a musical object on the screen, as you can see in, e.g., Figure 1.
Figure 1:
(seq (note) (pause 1) (note :pitch 62))
The center of the notes height indicates the pitch and the height itself indicates that amplitude. As you can see, the pitch and the amplitude of both notes remain constant throughout the notes duration. Now we will put a note in between of these notes which represents a glissando from the first to the second note. A glissando's pitch glides linearly from the pitch of the first note to the pitch of the second note. You see that we need a construct that enables us to change an aspect of a note throughout its duration. This is what we call a GTF, a generalized time function. Now we will give the code with a glissando instead of a pause (see Figure 2):
(draw (seq (note) (note :pitch (glissando 60 62)) (note :pitch 62)))
Figure 2:
(seq (note) (note :pitch (glissando 60 62)) (note :pitch 62))
You can read (note :pitch (glissando 60 62)) as: a note that has a line from 60 to 62 spread over its duration as a pitch. This means that, if you will change the notes duration, the glissando will adapt to the new duration and it will still be a glissando from 60 to 62. To demonstrate this, we will shrink the whole musical object (see Figure 3).
(draw (stretch .5 (seq 
                   (note) 
                   (note :pitch (glissando 60 62)) 
                   (note :pitch 62))))
Figure 3:
shrinked example of figure 2
Apparently, the glissando behaves well under transformation, since this is how we intend a transformed glissando to behave. A different transformational behavior is shown in the vibrato example. Suppose we want to add a nice vibrato to the first note. We can do that as follows (also see Figure 4):
(draw (seq (note :pitch (vibrato :around 60 :period .2 :amplitude .4)) 
           (note :pitch (glissando 60 62)) 
           (note :pitch 62)))
Figure 4:
altered example of figure 3
It might be self-explanatory that the keyword around indicates around which pitch the periodic deviation moves. The amplitude indicates how much the deviation from the notes pitch will be (vibrato has got more keywords which you can check out in the source code). When we shrink this object, we want the vibrato of the first note not to shrink. Instead we want the amount of periods to decrease. If the vibrato would shrink the note would sound totally different and not the way we intend to. Now let us shrink the object again (see Figure 5):
(draw (stretch .5 (seq (note :pitch (vibrato :around 60 
                                             :period .2 
                                             :amplitude .4)) 
                       (note :pitch (glissando 60 62)) 
                       (note :pitch 62))))
Figure 5:
stretched example of figure 4
As you can see, vibrato wonderfully behaves under this transformation like we intend to.
To conclude this subsection we will give a 3D plot in which you can see the behavior of a vibrato and a glissando within all possible durations from the interval [0,3]. In Figure 6 ~\ref{fig:vibratograph} you see how the vibrato behaves in time for all durations. The NoteFun code for the vibrato is (vibrato :period 1). In Figure 7 ~\ref{fig:glissandograph} you see that the glissando goes from 60 to 62 in a linear way according to the durations. The NoteFun code for the glissando is (glissando 60 62).
Figure 6:
overview of a vibrato of various durations
Figure 7:
overview of a glissando of various durations
In the next section we will introduce four methods of creating new GTFs.

Complex GTFs: creating GTFs out of other GTFs

Now we will introduce tools with which you can make complex GTFs from simpler ones. One is called concat-gtf and like its name says, you can concatenate GTFs with it. With the ones we already saw we can e.g., make a concatenation of a vibrato and a glissando and apply it to one notes pitch. We will have to specify what portion of a duration will be dedicated to what GTF. This is done by means of absolute, relative and proportional who all indicate a point in time within the duration of a note in their own way. The argument list of concat-gtf is built up from alternating GTFs and indications of time. An example of the use is given here (also see Figure 8):
(draw (note :pitch (concat-gtf (glissando 60 62) 
                               (proportional .5) 
                               (vibrato :around 62 :period .2))))
Figure 8:
example of concat-gtf
You might wonder what (proportional .5) stands for. It means that what is before this expression may take half (hence .5) of the duration of the musical object, in this case a note, and what is after this expression may take the other half. This means that (glissando 60 62) in this case may take up half a second, since the default duration of a note is 1 second, and (vibrato :around 62 :period .2) may take the rest which is also half a second.
If we transform the note by means of stretch the durations of the GTFs will transform proportionally with the new duration. Instead of proportionally determined points in time you can also use relative points and absolute points. Relative means in this case: seen from the start of the note. Absolute means: seen from the start of the whole piece. Let us give a complex example to demonstrate how relative and absolute behave under transformation (also see Figure 9):
 
1 (draw (seq (note :duration 4 :pitch (concat-gtf 
2                               (glissando 60 62) 
3                               (proportional .25) 
4                               (vibrato :around 62 :period .2)
5                               (relative 1.8)
6                               (glissando 62 64)
7                               (absolute 3.5)
8                               (glissando 64 60)))
9            (note :duration 5 :pitch (concat-gtf 
10                              (glissando 60 62) 
11                              (proportional .25) 
12                              (vibrato :around 62 :period .2)
13                              (relative 1.8)
14                              (glissando 62 64)
15                              (absolute 7.5)
16                              (glissando 64 60))))
17      :y-min 58 :y-max 65)
Figure 9:
example of concat-gtf

You see this quite a complex beast. In this example there are two notes in a sequence. The first has a duration of 4 seconds and the second has a duration of 5 seconds. You can see that (proportional .25) (on line 3 and 11 of the above code fragment) indicate different points counted from the starts of the notes, as it stands for 4 x .25 (=1) in the first note and 5 x .25 (= 1.25) in the second note. The expression (relative 1.8) indicates the same point in time counted from the starts of the notes, namely 1.8 seconds after the start. This always holds, even when a note has been transformed. The expression (absolute 3.5) indicates the point in time that is 3.5 seconds after the start of the entire musical object, in this case a sequence holding the two notes. (absolute 7.5) of course indicates the point in time 7.5 seconds after the start of the entire musical object.
To hear the sound produced in this last example, click here:

Now answer these questions for yourself before trying out the encoding of the problems in NoteFun:

In both cases, first try to calculate the new points in time where one GTF ends and another begins. Let us call those points switch points from now on. Afterwards look at the corresponding pictures and see if your guess was correct.

Now for a malicious use of concat-gtf, have a look at what happens here (also see Figure 10):
(draw (note :pitch 			1
            (concat-gtf 		2
             (glissando 60 62) 		3
             (absolute .5)		4
             (glissando 62 60)		5
             (absolute .25)		6
             (glissando 60 62)		7
             (absolute 1.5)		8
             (glissando 60 62))))	9
Figure 10:
example of malicious use of concat-gtf
The glissando on line 3 takes half the note, as you can expect. But what must be the behavior of the glissando on line 5, since the point in time indicates by line 6 is before the point in time indicated by line 4... ? concat-gtf prints a warning on the screen saying: "Times being re-arranged!'' concat-gtf checks if the successive switch points of its argument list are monotonously increasing. Also it check whether all the switch points remain between the bounds of the start and end time of the note. If not, it changes the switch points so they will obey these two properties.
In our last example concat-gtf changes the switch point on line 6 into (absolute .5) (first property) and the switch point on line 8 into (absolute 1) (second property). The result is that the glissando of line 5 will not appear since it has a time interval of exactly 0 seconds. The glissando of line 5 will appear on the interval [.5,1]. The glissando of line 9 will not appear since it has a time interval of 0 seconds. In other words, the last example is equivalent to the following code:
(draw (note :pitch 
            (concat-gtf 
             (glissando 60 62) 
             (absolute .5)
             (glissando 60 62))))
Try this out and see for your self.
We conclude this subsection with a 3D graph of a complex GTF with duration from the interval [0,3] plotted in time. See Figure 11. The NoteFun code for this GTF is:
(concat-gtf 
 (glissando 60 62) 
 (proportional .25)
 (vibrato :around 62 :period .2)
 (relative 1.8)
 (glissando 62 64)
 (absolute 3.5)
 (glissando 64 60))
Figure 11:
3d overview of a complex GTF

Intermezzo: guide to a wider application of GTFs

Note that until now we have only used GTFs in combination with the pitch of a note. To stimulate your creativity we must remark that GTFs can also be used in combination with a lot of other possible features of the note like amplitude and midi related parameters like brightness and pan. To simulate a fade-out you can use a GTF that goes from 1 to 0, where 1 indicates full volume and 0 indicates total silence (also see Figure 12):
(draw (note :amplitude (ramp 1 0)))
Figure 12:
note fade out
To hear the sound produced in this last example, click here:

As you might guess ramp is similar to glissando. In fact, it is actually the same function. It only has a more general name and unlike glissando its name will inspire us to use it for all kinds of things, like here for amplitude.

To make use of the midi controller related features of note, you can add the synthesizer specific controller number and the range in which you want to control it yourself. The code for the controllers that we have put in for our own synthesizer (A Yamaha MU90R) looks like this:

(add-midi-controller :pan 10 -1 0 1)
(add-midi-controller :balance 8 0 .5 1)
(add-midi-controller :timbre 71 0 .8 1)
(add-midi-controller :brightness 74 0 .8 1)
You can make up any keyword your want for your specific controller. The syntax for add-midi-controller is
(add-midi-controllers 
	<keyword>
	<midi control number> 
	<minimal-value>
	<default-value>
	<maximum-value>)
For a panning from left to right within a note we could write:
(note :pan (ramp -1 1))
You can remove a midi controller by executing (remove-midi-controller ). For example, if we want to remove the controller for brightness, we write: (remove-midi-controller :brightness). To get a list of all currently available midi controllers you write: (show-midi-controllers).

Complex GTFs continued

Another tool for merging simple GTFs into a complex one is switch-gtf. It works slightly different than concat-gtf but has the same way of calling. Instead of concatenating GTFs it switches between GTFs at certain switch points and the GTFs are all spanned over the notes entire duration. Let us give an example based on two glissando's, one going up and one going down. Within the note we will switch between them using switch-gtf (also see Figure 13):
(draw (note :duration 3 
            :pitch (switch-gtf (glissando 60 65) 
                               (proportional .25) 
                               (glissando 65 60) 
                               (proportional .5)
                               (glissando 60 65) 
                               (proportional .75)
                               (glissando 65 60))))
Figure 13:
note fade out
To hear the sound produced in this last example, click here:

switch-gtf corrects badly successive switch points in the same way as concat-gtf does.

Another way of building new GTFs from other ones is called time warping. Instead of going through a GTF from left to right in time, we can e.g., do it from right to left; from the beginning to the end. This operation can be done with a time warp that we called time-reverse. Its use is illustrated in the following example.

(setq some-gtf (concat-gtf 
                (glissando 60 62) 
                (proportional .25) 
                (vibrato :around 62 :period .2)
                (proportional .5)
                (glissando 62 64)
                (proportional .75)
                (glissando 64 65)))

(draw (note :pitch some-gtf) :y-max 66)
(draw (note :pitch (warp-gtf some-gtf (reverse-time)) :y-max 66)
First we make a reference to a GTF to be able to write more readable code. We use the name some-gtf as the reference name. Then we draw the result of using some-gtf as a notes pitch (see Figure 14). Lastly we draw the result of a transformed version of some-gtf by means of the time warp time-reverse (see Figure 15).
Figure 14
some GTF
Figure 15
some GTF
To hear the sound produced in example illustrated by Figure 14, click here:
To hear the sound produced in example illustrated by Figure 15, click here:

Another handy time warp is make-time-cyclic. make-time-cyclic has one argument indicating a point time within the duration of the note by means of absolute, relative and proportional. I will show the use of make-time-cyclic by means of an example (see Figure 16):
(draw (sim (note :duration 3 
                 :pitch (warp-gtf (glissando 62 65) 
                                  (make-time-cyclic (proportional .5))))
           (note :channel 1 :duration 3 
                 :pitch (warp-gtf (glissando 60 62) 
                                  (make-time-cyclic (relative .5)))))
      :y-max 66)
Figure 16
Note with a glissando which is transformed by a time warp, as a pitch
Note that we have to set an other channel for either one of the notes, to make the example sound correctly. To hear the sound produced in this last example, click here:

By now it might be clear how make-time-cyclic works. The point in time indicated by the time switch is the period length which indicates after what amount of time the GTF should start over again as if it were from the start.

There are more facilities that let us create GTFs out of other ones. One is called mix-gtf. By means of an example we will show its use (see Figure 17):

(draw (note :duration 3 :pitch 
            (mix-gtf 
             (glissando 60 70) 
             1 
             (sine-gtf :period 2 :amplitude 1) 1))
      :y-max 75)
Figure 17
Example of mix-gtf
The syntax in which you can use mix-gtf is: (mix-gtf [<gtf> <factor>]*). The factors in this expression can be real numbers or GTFs (the notation we have used for the argument list must be read as a regular expression in which <gtf> and <factor> can be arbitrary GTFs and numbers. The same holds for successive examples in which this notation is used.). The result of this expression will be a gtf which is the weighted result of the succeeding gtfs and factors in the argument list. Our last example will be a gtf which is the addition of a glissando from 60 to 70 and a sine with an amplitude of 1 and a period of 2 seconds. Check Figure 18 and see if it looks like you expect. Because of the fact that we can either fill in a real number or a GTF as the factor, we are able to express things like: "let the impact of a GTF run from 2 to 0 along with the duration of the note". As follows:
(draw (note :duration 3 :pitch 
            (mix-gtf (ramp 60 70) 
                     1 
                     (sine-gtf :amplitude 1) 
                     (ramp 2 0)))
      :y-max 75)
The first GTF is kept constant (the succesive factor is 1) but the second GTF is multiplied by a ramp from 2 to 0. What we will expect is a ramp with a periodic deviation of which the amplitude will decrease as we go through the note in time. You can see the result in Figure 18.
Figure 18
Second example of mix-gtf
Another similar combinator of GTFs is interpolate-gtf. As its name suggests, it interpolates between two GTFs. To make this clear let us interpolate between a glissando and a vibrato:
(draw (note :duration 3 :pitch (interpolate-gtf (glissando 60 70) (vibrato))) 
      :y-max 70)
Figure 19
Example of interpolate
The interpolation can be defined in terms of mix-gtf:

(interpolate-gtf gtf1 gtf2) = (mix-gtf gtf1 (ramp 1 0) gtf2 (ramp 0 1))

To give more elementary operations on GTFs we will introduce gtf+ and gtf*. After that we will introduce a more general function with which you can make your own operation on GTFs.

A GTF that is the sum of multiple other GTFs can be made with gtf+ with the following syntax: (gtf+ [<gtf | number>]*). For example, we can add a glissando and another glissando so that they will compensate each other, resulting in a constant pitch:
(note :pitch (gtf+ (glissando 30 40) (glissando 30 20)))
The result is just a note with a constant pitch of 60.

The next example demonstrates the use of gtf* within the use of gtf+.

(draw (note :pitch (gtf+ 60 (gtf* (ramp 0 1) (sine-gtf :period .2)))))
Figure 20
Example of gtf+ and gtf*
The result is a note with a with-time-increasing periodic deviation from pitch 60 (see Figure 20).

If you require more operators on GTFs you can define them yourself using time-fun-compose. For example, if we would like to make the operator for division on GTFs we write: (time-fun-compose #'/ [<gtf>]*)

More close to Lisp, a usable definition could be called gtf/ and would look like this:
(defun gtf/ (gtf1 gtf2 &rest args)
  (apply #'time-fun-compose #'/ gtf1 gtf2 args))
We can use gtf/ from now on as demonstrated in the following example (see Figure 21):
(draw (note :duration 2
            :pitch (gtf+ 60 (gtf/ (sine-gtf :amplitude 5) (ramp 2 10)))))
Figure 21
Example of gtf/ embedded in a gtf+*

GTFs created from home-made functions

In this section we will describe how one can build a GTF based on a mathematical function, like the sine function. This way of building gives one more degree of freedom of customizing your own GTFs as you will not be only limited to the tools we provided before. Instead you can come up with any unary function, provided that they give a sensible result, you wish. We will denote these functions with simple functions in what follows. You can specify how it should behave under transformation. Like we saw with the glissando, some functions should transform in a proportionally stretching way. Other functions, similar to the vibrato, should not be stretched. We call the creation of a GTF out of a simple function lifting. An equivalent of the glissando might be a linear function, like (lambda(x) x). Suppose we want to have a simple function that corresponds to a glissando from 60 to 70. Note that we do not know the duration of the note, to which the function will be applied, since it can be any positive real number. For now we will just choose a function that goes from 60 to 70 in an interval of 1 second:
(setq linfunc #'(lambda(x) (+ 60 (* 10 x))))
Now look at the following example (see Figure 22):
(draw (note :duration 2 :pitch (lift linfunc (proportional-lift 0 1)))
      :y-max 72)
Figure 22
Example of a simple function lifted proportionally
What does (lift linfunc (proportional-lift 0 1)) mean? Informally it means, select the function linfunc on the interval [0,1 and stretch it according to the duration of a note.
More general, the syntax for lift is:

(lift <lift-function>).

proportional-lift is a lift function that selects an interval out of a simple function. Try to modify the duration of the note in the last example and see that its pitch still runs from 60 to 70. This is how we want a glissando to behave under transformation.

Another lift function is relative-lift. It does not help us to stretch a selection out of a function but it lets us express something else. Let us try it out as a variation on our last example.
(draw (note :duration 2 :pitch (lift linfunc (relative-lift 0 1)))
      :y-max 72)
Figure 23
Example of a simple function lifted relatively
The behavior as result of this GTF is not the one we intend for a glissando, as you can see in Figure~\ref{fig:lift-relative-1}. But maybe you see where we \emph{do} want to go with our example, because this is the kind of behavior we want to assign to a sine function to make it behave like a vibrato, for example. The arguments of relative-lift do not indicate an interval, but a starting point and an x-scale. This makes it easy to create a nice vibrato out of a sine. Like this (see Figure~\ref{fig:lift-relative-2}):
(draw (note :duration 2 :pitch (gtf+ 62 (lift #'sin (relative-lift 0 Pi)))
      :y-max 72))


Now try to modify the duration of the note from the last example again and see what happens! Is it the behavior you desire from a vibrato?

The last lift function for simple functions we will discuss is absolute-lift. It is different from relative-lift in that it does not see the notes start as the starting point ( time = 0 ) for the GTF, but it uses the start of the whole musical object as the starting point of the GTF. Let us illustrate this with a vibrato that is lifted using absolute-lift and that is used over a group of notes.
(draw (seq (sim 
            (seq (note :duration 2 :pitch (gtf+ 60 lifted-sine))
                 (note :duration 1 :pitch (gtf+ 57 lifted-sine)))
            (seq (note :pitch (gtf+ 63 lifted-sine))
                 (note :pitch (gtf+ 64 lifted-sine))))
           (note :duration 2 :pitch (gtf+ 65 lifted-sine)))
      :y-min 55
      :y-max 70)
Figure 24
A group of notes, all with the same lifted sine function by use of <tt>absolute-lift</tt>
Because the starting point of the whole group of notes is taken as time = 0 you can see the sine wave continuously running through all notes, as if it does not know where one note ends and another begins.

Lifting tables

Another method of creating GTFs is by means of tables. You can specify a table with key and value pairs and lift it as if it were a mathematical function. An example is given below.
(setq my-table '(
             (0.0 60)
             (.1 61.4)
             (.2 62.3)
             (.3 61.2)
             (.7 65)
             (1 60)))
With the help of table-fun we can make a simple function out of a table:
(setq tablefunction (table-fun my-table))
Since tablefunction now is a simple function, we can use it in the same way as any other simple function. Hence we can lift it with the same constructions as demonstrated in the previous subsection. The function is made by a lineair interpolation between the values of the table, as demonstrated in the following code (see Figure 25):
(draw (seq (note :duration 2 
                 :pitch (lift tablefunction (proportional-lift 0 .5)))
           (note :duration 2 
                 :pitch (lift tablefunction (relative-lift 0 .5)))) 
      :y-max 66)
Figure 25
Example of a table, converted to a simple function and lifted

Creation of GTFs by writing your own Lisp code

In this last subsection on the creation of GTFs we will address you to write your own Lisp code as a means of creating your own GTFs. In order to do that we have to go into details of how a GTF is built up. We will do this, like you are used to in this tutorial, by means of example. First we will show how an existing GTF is built up. After that we will write a new one from scratch.

Do you remember the glissando GTF from all the previous examples? As we already said it is the same function as the ramp function. Here is its encoding in Lisp:
1(defun ramp (from to)  
2 (gtf (start duration)
3    (tf(time)
4      (let ((progress (/ (- time start) duration)))
5        (+ from (* progress (- to from)))))))
In general a GTF is built up using the construction
(gtf (start duration) 
  (... 
   (tf (time) (...))...)
ramp returns a function of the type R -> R+ -> (R+ -> R+). The constructions gtf and tf work like normal functional abstraction. They are adapted versions of Lisps lambda, enhanced for our purpose here. gtf creates a function of the type R+ -> R+ -> (R+ -> R+) and tf creates a funtion of type R+ -> R+. A GTF makes use of three important variables time, start and duration. The variable time indicates the time counted from the start of an entire musical object; start indicates the time at which the note in which the GTF will be used will begin; duration represents the time interval on which a note will be played. The dots on the second line of the general code are intended to consist of calculations with start and duration. Calculations that involve also time can only occur inside the tf construction. This two layer way of building a GTF is chosen because of efficiency reasons which we will not have to discuss on this website (look at my internship report for more on this). Because all of the calculations that we need in ramp involve time, they have to occur inside the tf.

Now let us look how ramp works. On line 4 of its code an expression with time, start and duration is bound to progress. If we look closer to the expression we see that it indicates the fraction of the duration that has already passed: "current time minus start time" divided by "total duration of the note". At the very start of the note, progress will have value 0. At the very end of the note "time minus start" will equal duration and therefore progress will have the value of 1. Now, with (ramp ) we want to have a GTF that is a linear function from to on the interval [start,start+duration]. Note that the expression
(+ from (* progress (- to from))))))) returns the right value on each point of time on in the interval. Now that we have seen the implementation of ramp it may inspire us to make a different GTF. Wouldn't it be funny to make a GTF that gives random values from a certain range? In Lisp (random ) returns a random floating point number from the range [0,n[ (n self is excluded) when n is a floating point number. Hence (random 1.0) returns a floating point number between 0 and 1. The Lisp expression (+ min (* (- max min) (random 1.0))) then returns a random value between min and max. This is all we have to return, so the code for random-gtf will be quite simple:
(defun random-gtf (min max)
  (gtf (start duration)
    (tf (time)
      (+ min (* (- max min) (random 1.0))))))
Let us see how random-gtf behaves as a pitch GTF:
(play-and-draw (note :pitch (random-gtf 61 62)))


Look at Figure 26 for the result!
Figure 26
Example of the use of random-gtf
To hear the sound illustrated by Figure 26, click here:
Of course we can also use random-gtf to make small random deviations from other GTFs like in the following example (see Figure 27):
(draw (note :duration 2 
            :pitch (gtf+ (vibrato :around 61) 
                         (random-gtf 0 .5))))
Figure 27
Example of the use of random-gtf
To hear the sound illustrated by Figure 27, click here:

Case study: ADSR

To conclude this tutorial, we will give you a description of how to build a GTF that behaves like an instrument. This GTF will be build up in analogy of a concept in the synthesizer world, namely ADSR. ADSR stands for attack, decay, sustain and release. The idea is that when a note starts, the volume of the note will go from 0 to a certain attack level. Immediately after reaching the attack level the volume will go down in a certain amount of time, till it reaches the sustain level. This level will be maintained for a certain length of time. Just before the end of the note (think of a key being released) the level will go from sustain level to 0 again. This is illustrated in Figure 28. We will discuss a simplified version of this idea for the sake of didactics and the length of this tutorial.
Figure 28
Concept of ADSR
From the above description of ADSR it might be evident that we need four basic GTFs which we can concatenate later by means of concat-gtf. Conceptually this may look something like this:
1(concat-gtf
2 attack function
3 time switch
4 decay function
5 time switch
6 sustain function
7 time switch
8 release function)
We need a GTF for going from 0 to an attack level in a certain amount of time. This amount of time does not in- or decrease with the length of a note for most instruments (loosely speaking); it is a constant portion of time. Therefore, the attack function, let us call it attack-fun, must precede a time switch that is made with relative. The same goes for the decay function, which we will call decay-fun and its succeeding time switch. The sustain, named sustain-fun, does depend on the duration of the note. The portion of time needed for the release, let us call it release-fun, from sustain level to 0 is again a constant portion. With this information we can make the concept a bit more concrete:
1 (concat-gtf
2 attack-fun
3 (relative ...) 
4 decay-fun
5 (relative ...)
6 sustain-fun
7 (relative `end time minus' ...)
8 release-fun)
Let us specify the different functions a little more. We propose the following as an attack function:
(defun attack-fun (begin end)
  (gtf+ begin
   (gtf* (- end begin) (lift (sine 4) (proportional-lift 0 1)))))
The function attack-fun expects a begin and end value. In the following example you get an impression of how it works (see Figure 29):
(draw (sim (note :pitch (attack-fun 60 65))
           (note :duration 2 :pitch (attack-fun 62 70)))
      :y-max 72)
Figure 29
Example of attack-fun
For the decay function we propose the following:
(defun decay-fun (begin end)
  (time-fun-compose #'- begin
                    (gtf* (- begin end) (lift (sine 4) (proportional-lift 0 1)))))
We illustrate this function with the following example (see Figure 30):
(draw (sim (note :pitch (decay-fun 65 62))
           (note :duration 2 :pitch (attack-fun 70 64)))
      :y-max 72)
Figure 30
decayfun.png
The sustain function can be really simple. It just has to remain constant at a certain level:
(defun sustain-fun (volume)
  (gtf (start duration)
    (tf (time)
    volume)))
As a release function we can take the same function as the decay function:
(defun release-fun (begin end)
  (decay-fun begin end))
Finally we are ready to try out our ADSR. We build a function around it in which you can specify the different levels and time intervals.
1 (defun adsr (attack-time decay-time release-time attack-level sustain-level)
2  (let ((att-time (relative attack-time))
3         (dec-time (relative (+ attack-time decay-time)))
4         (rel-time (fun-funcall #'- #'end-time release-time)))
5     (concat-gtf (attack-fun 0 attack-level)
6                 att-time 
7                 (decay-fun attack-level sustain-level)
8                 dec-time
9                 (sustain-fun sustain-level)
10                rel-time
11                (release-fun sustain-level 0)
12                )))
The expression (fun-funcall #'- #'end-time release-time) evaluates to the end time of the note minus the time interval indicated by release-time. Because end-time and release-tim are not normal numbers (actually they are functions) we have to use fun-funcall in this case.

Now let us try out adsr. We define two different ADSRs named inst1 and inst2 as follows and use both in a group of notes:
(setq inst1 (adsr .1 .4 .2 .9 .7))
(setq inst2 (adsr .2 .2 .4 .4 .2))

(draw
 (sim
  (for-all-notes :amplitude inst1 
                 (seq  (note :pitch 62 :duration 1) 
                       (pause 1)
                       (note :pitch 63 :duration 2)))
  (for-all-notes :channel 1 :amplitude inst2 
                 (seq (note :pitch 63 :duration 2)
                      (note :pitch 62 :duration 1.5)))))
Figure 31
Example of ADSR


To hear the example above, listen to this sample:

You see that the only the length of the sustain phases depend on the lengths of the notes. The other phases are of constant length, no matter what the lengths of the notes are.

In the following graphs we show both ADSRs plotted in time against durations of lenghts from the interval [0,3] in seconds.
Figures 32 (left) and 33 (right)
The GTF <tt>(adsr .1 .4 .2 .9 .7)</tt> plotted for durations from 0 to 3 seconds The GTF <tt>(adsr .2 .2 .4 .4 .2)</tt> plotted for durations from 0 to 3 seconds

Some additional notes

All the sounds were recorded using a Yamaha MU90R synthesizer.
If you find any mistakes on this page, please report them to me. Also other comments are welcome at michielborkent@gmail.com.