previous up next
4.3 The description and control of sound synthesis

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
 
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)))

previous up next