by Nicky Hind
Fourier theory states that any periodic waveform can be broken down into a series of sinusoids having different frequencies, amplitudes, and phase. In wavetable synthesis the amplitude of partials are fixed, and while this is generally sufficient to give an impression of certain types of timbre (the clarinet with its energy in the odd-numbered partials, being a good example), its limitations prevent it from achieving more life-like results. This is mainly for two reasons: a) in nature, the frequencies of partials are seldom exact integer multiples of the fundamental, and b) the amplitudes of individual partials nearly always vary relative to each other during the course of a tone. Both of these effects are impossible to achieve with wavetable synthesis, and additive synthesis -- with its clear relationship to Fourier analysis -- offers a solution to these problems.
In additive synthesis, each partial is modelled by a seperate sinusoidal oscillator, thus creating the possibility for the individual specification of amplitude and frequency (and phase), and how these will evolve over time. The output of these oscillators is summed together to produce a composite waveform.
Here is a simple additive synthesis instrument which uses just 3 oscillators - it is an extension of the simple sine-wave instruments in the A Basic Introduction to CLM
(definstrument add-synth (start-time duration frequency amplitude ;; the following variables determine the frequency and amplitude ;; for each partial: the values of PARTIAL1 PARTIAL2 etc. are ;; multiplied by the value of the main FREQUENCY argument; ;; similarly the values of AMP1 AMP2 etc. are multiplied ;; by the value of the main AMPLITUDE argument. &key (partial1 1.0) (amp1 0.3) (phase1 0) (partial2 2.0) (amp2 0.3) (phase2 0) (partial3 3.0) (amp3 0.3) (phase3 0) ;; the amplitude envelope for each partial (amp-func1 '(0 0 50 1 100 0)) (amp-func2 '(0 0 50 1 100 0)) (amp-func3 '(0 0 50 1 100 0))) (let* ((beg (floor (* start-time sampling-rate))) (end (+ beg (floor (* duration sampling-rate)))) (sine1 (make-oscil :frequency (* partial1 frequency) :initial-phase phase1)) ;;phase is in radians (sine2 (make-oscil :frequency (* partial2 frequency) :initial-phase phase2)) (sine3 (make-oscil :frequency (* partial3 frequency) :initial-phase phase3)) (amp-env1 (make-env :envelope amp-func1 :scaler (* amplitude amp1) :start-time start-time :duration duration)) (amp-env2 (make-env :envelope amp-func2 :scaler (* amplitude amp2) :start-time start-time :duration duration)) (amp-env3 (make-env :envelope amp-func3 :scaler (* amplitude amp3) :start-time start-time :duration duration))) (Run (loop for i from beg to end do (outa i (+ ;; this where the output of the oscils ;; is actually summed together! (* (env amp-env1) (oscil sine1)) (* (env amp-env2) (oscil sine2)) (* (env amp-env3) (oscil sine3)))))))) |
1. Accepting all the defaults in the instrument definition, a 2 second tone at 220 Hz:
(with-sound () (add-synth 0 2 220 0.5))
2. With decreasing amplitudes for the 3 partials. (Observe how key variables are called):
(with-sound () (add-synth 0 2 220 0.5 :amp1 1.0 :amp2 0.4 :amp3 0.25))
3. With off-harmonic (ie. non-integer ratio) partials:
(with-sound () (add-synth 0 2 220 0.5 :partial1 1.0 :amp1 1.0 :partial2 1.99 :amp2 0.4 :partial3 3.01 :amp3 0.25))
4. With different envelope data for each partial. Often, in acoustic sounds,
the higher partials have
slower attack times and begin to decay sooner:
(with-sound () (add-synth 0 2 220 0.5 :partial1 1.0 :amp1 1.0 :amp-func1 '(0 0 5 1 95 1 100 0) :partial2 1.99 :amp2 0.4 :amp-func2 '(0 0 25 1 75 1 100 0) :partial3 3.02 :amp3 0.25 :amp-func3 '(0 0 40 1 60 1 100 0)))
So far, the way envelopes have been specified implies a direct mapping of the envelope shape over the duration of the sound. For example, with the envelope
'(0 0 25 1 75 1 100 0)
if the duration is 4 seconds, then the attack-time (the time taken to get from the point 0 0 to the point 25 1) will be 1 second. And similarly, the decay-time (from the point 75 1 to 100 0) will also be 1 second. If the duration was doubled to 8 seconds, then the attack- and decay-times accordingly would also double. Usually, it is advantageous to be able to specify attack- and decay-times independently of duration. CLM allows for this with the functions DIVSEG and DIVENV -- the simplest of which is DIVSEG and will suffice for the present purposes.
The basic template for DIVSEG is as follows:
(divseg <envelope function> <old attack point> <new attack point> &optional <old decay point> <new decay point>)
From this, DIVSEG will return a new envelope shape with appropriately modified break points. For example, using the envelope shape from the previous paragraph, DIVSEG could change the attack and decay break-points according to some values which we specify. Thus we could make the shape a little more square by giving the following values:
(divseg '(0 0 25 1 75 1 100 0) 25 10 75 90) => (0 0 10 1 90 1 100.0 0)
So far, so good, but what still remains to be done is to find a way to convert from attack- and decay-times which are specified in seconds, to the "x" co-ordinate of a break-point for DIVSEG to handle. Here is a solution. Since the "x-axis" co-ordinates of CLM envelopes range from 0 --100, and this is generally mapped to the duration of the sound to be computed, we must calculate what proportion of the total duration, are our attack- and decay-times, and then "normalise" them to 100. Suppose for example, we want an attack-time of 0.5'' and a duration of 5'', how would we compute the position of the attack break-point.
In terms of LISP, the following expression would compute this:
(* 100 (/ 0.5 5)) => 10.0
Or in more general terms:
(attack-point (* 100 (/ attack-time duration)))
The decay break-point can be found by first subtracting from 1, the decay-time divided by the duration, and them multiplying by 100:
(decay-point (* 100 (- 1.0 (/ decay-time duration))))
These methods can be seen in the modified version of the Add-Synth
instrument that follows.
These are further desireable refinements to the instrument, and will be covered in in part 2 of Additive Synthesis with CLM
(definstrument add-synth2 (start-time duration frequency amplitude &key (partial1 1.0) (amp1 1.0) (phase1 0) (att-time1 0.1) (dec-time1 0.1) (partial2 2.0) (amp2 0.4) (phase2 0) (att-time2 0.2) (dec-time2 0.2) (partial3 3.0) (amp3 0.25) (phase3 0) (att-time3 0.3) (dec-time3 0.3) ;; only one basic envelope function is now required ;; since attack and decay times are controlled elsewhere (amp-func '(0 0 25 1 75 1 100 0))) (let* ((beg (floor (* start-time sampling-rate))) (end (+ beg (floor (* duration sampling-rate)))) (att-point1 (* 100 (/ att-time1 duration))) (dec-point1 (* 100 (- 1.0 (/ dec-time1 duration)))) (att-point2 (* 100 (/ att-time2 duration))) (dec-point2 (* 100 (- 1.0 (/ dec-time2 duration)))) (att-point3 (* 100 (/ att-time3 duration))) (dec-point3 (* 100 (- 1.0 (/ dec-time3 duration)))) (sine1 (make-oscil :frequency (* partial1 frequency) :initial-phase phase1)) (sine2 (make-oscil :frequency (* partial2 frequency) :initial-phase phase2)) (sine3 (make-oscil :frequency (* partial3 frequency) :initial-phase phase3)) (amp-env1 (make-env :envelope (divseg amp-func 25 att-point1 75 dec-point1) :scaler (* amplitude amp1) :start-time start-time :duration duration)) (amp-env2 (make-env :envelope (divseg amp-func 25 att-point2 75 dec-point2) :scaler (* amplitude amp2) :start-time start-time :duration duration)) (amp-env3 (make-env :envelope (divseg amp-func 25 att-point3 75 dec-point3) :scaler (* amplitude amp3) :start-time start-time :duration duration))) (Run (loop for i from beg to end do (outa i (+ (* (env amp-env1) (oscil sine1)) (* (env amp-env2) (oscil sine2)) (* (env amp-env3) (oscil sine3)))))))) |
1. Accepting the defaults:
(with-sound () (add-synth2 0 2 220 0.5))