Multimedia often depends on a continuous change in time. This is
certainly true for sound synthesis where amplitude envelopes describe
the value of the amplitude in time. We start our discription of the
classes for sound synthesis with a discussion of control functions.
4.3.1 The control functions
Most synthesizers define controllers. In many systems the
controllers push their values down to the synthesis processes. The
rate at which they output their values is fixed and defined by the
system. Our definition of control functions is more general and
resembles that found in Foo. These are the characteristics to which
we attach a lot of importance:
- Continuous: control functions are considered continuous
in time. They can return a value for any given time.
- No predefined control rate: Control functions do not
send there values down to the synthesis modules, but instead
their values can be requested by any other object.
- Reentrant: The value of a controller can be requested in
any time order and not necessarily with increasing time
values.
- Multi-dimensional: Control functions always return an
array of values. This allows complex control functions (for
example, partials of additive synthesis) to be manipulated as
a single object.
We defined an interface, called ControlFunction comprised of two
methods: value, and dimension. The value method
returns the value of the control function. Its argument is a time in
seconds. The dimension method returns the dimension of the
control function.
The constant control function outputs a constant array of values. The
name ``constant'' refers to the fact that its value is
time-independent, not to the fact that the user cannot change its
value. With the following functions constant controllers can be
created and their values modified:
; Create a new constant controller of dimension 2
; and values [440,880].
(define c (const 440 880))
; Set the zero'th value to 261.
(const-set! c 261 0)
The breakpoint function is a piece-wise linear function. The following
expression creates a new breakpoint function of dimension 1. At time 0
it has a value of 0, at time 0.5 value 1, and at time 3 value 0.
(bpf 1 '((0 0) (0.5 1) (3 0))))
The sine-wave controller outputs a sinusoidally varying value. It
requires a control function describing its frequency and a control
function describing its amplitude:
; A sine-wave controller with a frequency of 6Hz
; and an amplitude of 0.1
(ctrl-sin (const 6) (const 0.1))
Control functions can be combined to create more complex control
structures. ctrl-add and ctrl-mul respectively add and
multiply two control functions. The function in the following example
constructs a controller modulating a frequency value
sinusoidally. With this controller a vibrato effect can be obtained.
(define (vibrato freq mod-freq mod-amp)
(ctrl-mul (const freq)
(ctrl-add (const 1.0) (ctrl-sin (const mod-freq)
(const mod-amp)))))
4.3.2 The description of synthesis algorithms
The type SynthesisProcess is an abstract class. As discussed
in section 5.1, all defined synthesis
processes are subclasses of this class. Consider we define a class
Sinewave that inherits from SynthesisProcess and
implements the sine-wave oscillator described in section 4.2. If we neglect the initial
phase of the oscillator, we can create a new instance of the
oscillator and pass it two arguments for its creation: the frequency
curve and the amplitude envelope. The Scheme procedure to realize this
looks like this:
(sinewave <freq> <amp>)
The freq and amp arguments are continuous functions in
time describing the evolution of the frequency and amplitude,
respectively. Combining the previous definitions, we can add (and
kill) a sine-wave oscillator playing at a constant frequency typing:
(add 0 (sinewave (const 440) (const 0.1)))
(kill 0)
The evaluation of add and kill is done immediately after
the user enters the text. With the help of two additional procedures
(add-sinewave and kill-sinewave) we can schedule the add
and kill of the sinewave as follows:
(define (add-sinewave id freq amp)
(add id (sinewave (const freq) (const amp))))
(define (kill-sinewave id)
(kill id))
(event 1.0 add-sinewave 0 440 0.1)
(event 2.0 kill-sinewave 0)
We will need some way to construct more complex synthesis processes.
The sine-wave oscillator used above will not please the musical ear
for very long. Not only should developers be able to create new
synthesis processes that embody a particular synthesis technique. Also
the expert user should have the possibility to construct new synthesis
techniques from within the Scheme shell. A priori, the formal model in
the previous chapter gives no clue how to create synthesis processes.
That is exactly what we expect from our model. Indeed, we specify
only the general interface of a synthesis process. It is up to
developers of applications, interfaces, or signal processing
techniques to realize the synthesis techniques they need. It is not
the aim of our project to write extensive digital signal processing
algorithms. However, we have realized a set of basic synthesis objects
and a way of defining new synthesis algorithms. We inspired ourself
on Mathew's unit generators [Mat69]. This paradigm
is still the most used to build synthesis patches. We would like to
stress, however, that our system is not restricted to the unit
generator approach. Indeed, using the flexibility of the Scheme
language, we think any approach can be realized in our environment.
We refer, for example, to Modalys [EIC95] that also uses
a Scheme interface to define physical models.
|
|
Figure 5.2: A unit generator taking as input a number of
sound signal and returning a sound signal as output. |
We define the abstract class UnitGenerator. It provides a
basic implementation of the interface defined by
SynthesisProcess. In addition, a unit generator can take a
number of sound inputs. A filter, for example, takes one sound signal
as input and returns the filtered signal. The input can be any other
object implementing the SynthesisProcess interface. Below we
give a simple example of a sampler unit generator that is sent thru a
bandpass filter:
; We define three control functions for the filter:
; the central frequency, the bandwidth, and the gain
; of the filter.
(define fc (const 200))
(define bw (const 50))
(define gain (const 1.0))
; We create a new bandpass filter with the control functions
; defined above.
(define filter (bandpass-filter fc bw gain))
; We load the sound file ``piano.aiff'' into a sample table.
; We indicate the base frequency of the stored samples.
(define piano-snd (sound-table ``piano.aiff'' 440))
; We create a sampler synthesis process. This sampler plays
; thru the sound table. The playback frequency and amplitude
; are defined by two control functions.
(define piano (sampler (const 440) (const 1.0) piano-snd))
; Connect the piano to the 0-th input of the filter.
(connect filter 0 piano)
; Pass the filter to the synthesizer to hear the filtered piano.
(add 0 filter)
The user can create synthesis processes dynamically by defining a new
program. This program can be included in events. For example:
(define (add-piano id freq amp)
(add id (connect (bandpass-filter fc bw gain) 0
(sampler (const freq) (const amp) piano-snd))))
(event 1.0 add-piano 0 440 0.1)
(event 2.0 kill 0)
In the examples above we explicitly defined the events, event
times, event arguments, and synthesis process identification numbers.
A composer does not always wish to reason in such concrete terms. It
will be useful to introduce abstractions to work on a higher level of
music organization. Furthermore, when we define these abstractions we
will not only have to take into account the start and stop times of
the resulting synthesis process, but also their control functions and
other arguments. This fairly complicated problem of time organization
will be handled in the next chapter.
4.3.3 Useful abstractions: Synthesis techniques and voices
Consider again the example we gave in the previous section:
(define fc (const 200))
(define bw (const 50))
(define gain (const 1.0))
(define piano-snd (sound-table ``piano.aiff'' 440))
(define (add-piano id freq amp)
(add id (connect (bandpass-filter fc bw gain) 0
(sampler (const freq) (const amp) piano-snd))))
This example displays some shortcomings of the current strategy.
First, the control functions fc, bw, and gain are
defined globally and control all instances of add-piano.
Imagine the composer wants to write a piece for two piano's. Either
both piano's are controlled simultaneously by the control functions,
or all control functions have to be copied for each piano. The former
case will surely not be always the desired behavior. In the latter
case, we pollute the name space of our environment with redundant
controller names.
The second problem occurs when we want to create libraries of
synthesis techniques. Imagine we have defined a synthesis technique
called ``filtered piano''. The composer has no description, and maybe
no access, to the parameters of the synthesis technique.
To solve these problems we adapt a meta-class like strategy. We
introduce two structures, SynthesisTechnique and
SynthesisVoice. Synthesis techniques create new synthesis
voices; synthesis voices create new synthesis processes. They provide
abstraction, encapsulation, and auto-documentation for the creation of
synthesis processes. Synthesis techniques define the default parameter
values for synthesis voices. Similarly, synthesis voices define the
default parameter values for synthesis processes. Both classes allow
to inspect and override the default values. In addition, all
parameters have a name, and the user can ask a textual description for
each of them. To create new synthesis processes, a synthesis voice
requests a number of arguments. The number and type of arguments
depends on the type of the synthesis voice, but we will accept that
the first argument defines the frequency and the second argument
defines the amplitude of the synthesis process. (This will be
discussed in more detail in section
6.6.)
Users can define new synthesis techniques dynamically in the Scheme
shell, or create libraries of synthesis techniques. For example:
(define vibra_a4
(sound-table ``vibra_a4_mono.aiff'' 440.0))
; We define a procedure that creates new synthesis processes.
(define (make-vibra-process f a env)
(sampler vibra_a4 f (ctrl-mul a env)))
; We define a new synthesis technique, called ``vibraphone''.
; The synthesis technique defines three controllers ``freq'',
; ``amp'', and ``envelope''. The synthesis processes will be
; made with the make-vibra-process procedure defined above.
(define vibra-syntech
(synthesis-technique
"vibraphone" "vibraphone"
make-vibra-process 3
'(("freq" "frequency (Hz)" (const 440.0))
("amp" "amplitude (linear)" (const 0.1))
("envelope" "amplitude envelope (factor)" (const 1.0)))))
; We make a new voice from the vibra synthesis technique.
(define vibra-voice (synthesis-voice vibra-syntech))
; We override the default value of the envelope controller
; defined by the vibra synthesis technique.
(voice-set! vibra-voice "envelope" (bpf 1 '((0 0) (0.5 1) (3 0))))
; We make and add a new synthesis process with the vibra
; synthesis voice. We pass the frequency and amplitude as
; arguments.
(add 0 (voice-make vibra-voice (const 261.0) (const 0.5)))