Additive Synthesis in SuperCollider

Multichannel Expansion

The following example implements simple additive synthesis in SuperCollider, suited as a starting point for more elaborate designs. Arrays can be helpful for using single UGens multiple times, which is the very idea of additive synthesis. The multichannel expansion allows the use of arrays as arguments for UGens, resulting in an array of objects with the initialization parameters given in the arrays. The following example will create five oscillators, sending their outputs to the buses 0...4:

(
{
        |pitch=100|
        SinOsc.ar(freq:pitch*[1,2,3,4,5],mul:1/[1,2,3,4,5]);
}.play;
)

s.scope(6,0);

Mixing

The single partials can be easily summed by wrapping them inside a Mix.ar() UGen. They are now all sent to the first output bus:

(
{
    |pitch=100|
    Mix.ar(SinOsc.ar(freq:pitch*[1,2,3,4,5],mul:1/[1,2,3,4,5]));
}.play;
)

s.scope(6,0);

Dynamic Allocation

For more partials and dynamic sizes, it makes sense to use the fill() method of the array class:

(

~n_part    = 30;
~add_synth = {

    |pitch=100|

    Mix.ar(
            SinOsc.ar(
                    freq:pitch*Array.fill(~n_part,{arg i; (i+1)}),
                    mul: Array.fill(~n_part,{arg i; 1/(((i)+1))})
            )
    );

}.play;
)

Exercises

Patches and Subpatches in Pure Data

Arguments

The following examples are based on patches and additional files, called abstractions. To make them work, all involved patches need to be located in the same direction (by cloning the complete repository). Arguments are passed to objects after the name, separated by a white space. The patch arguments-help.pd shows this by creating an arguments object:

/images/basics/pd-arguments-1.png

Inside an abstraction, individual arguments can be accessed with the $ operator and their index. The loadbang is executed on the object's creation, thus printing both arguments on start. This is helpful for setting initial values in patches, as shown in arguments-help. Once created, it will print the arguments to the main Pd window:

/images/basics/pd-arguments-2.png

Subpatches

Subpatches can be very helpful for creating cleaner patches without addtional abstractions and files. To create a subpatch, use the object pd with an optional string argument for naming the subpatch. They can be used like abstractions but do not require an additional file.


Graph-on-Parent

When toggling Graph-on-Parent in an object's properties, it can expose GUI elements to its parent patch. This is a good way of cleaning your patch and showing only what is needed in a performance situation. It works for both abstractions and subpatches. The example patches.pd makes use of this to create a filter subpatch with controls. The left hand audio input of the suppatch is a fixed frequency sawtooth. The right hand control input sets the Q of the filter.

/images/basics/pd-patches-1.png

On the inside, the moog~ object is used. It is not part of PD vanilla and can be installed with the flatspace ggee extensions from Deken. The red rectangle marks the area visual in the parent patch. All GUI components inside this area will be visible:

/images/basics/pd-patches-2.png

Inlets and Outlets

The patch has two inlets - one in audio rate (inlet~) and one in control rate - and two outlets, also with audio rate (outlet~) and control rate. For inlets and outlets, their horizontal order determines their order in the object when patched from the parent. Changing them can mess up the complete patching.

FIR Filters

Finite Impulse Response (FIR) filters are such digital filters which have - as the name suggests - a finite impulse response. These filters do not have an internal feedback and can be applied simply by convolving an input sequence $x[n]$ with the filter's impulse response $h[n]$ to get the output sequence $y[n]$:

For FIR filters, the impulse response $h[n]$ is a one-dimensional array containing the $N$ filter coefficients. The following example shows the coefficients, respectively the impulse response for a filter of the order $N=5$:


The Difference Equation

From the impulse response we can get the coefficients $b_n$ to write the difference equation, which is the basis for applying a filter:

$$ y[n] = b_0 x[n] + b_1 x[n-1]+ b_2 x[n-2]+ b_3 x[n-3]+ b_4 x[n-4] + b_5 x[n-5] $$

Implementation Structure

Another important representation of a filter is the implementation structure. It can be directly derived from the difference equation. Each $z^{-1}$ block represents a delay by one sample. This case shows the direct form:


Applying a Filter by Convolution

Applying the filter with the given impulse response is done by simple convolution, which is the same as passing samples from the input of the implemtation structure above to its output. The above equation is then written as follows:

$$ \begin{eqnarray} y[m] &=& x[n] * h[m]\\ &=& \sum\limits_{m=0}^{M} h[m] x(n-m)\\ &=& \sum\limits_{m=0}^{M} b_m x(n-m)\\ \end{eqnarray} $$

The Transfer Function

The difference equation can also be expressed as a sum:

$$ h[n] = \sum\limits_{n=0}^{N} b_n x[n-1] $$

The transfer function of the filter can be obtained via the z-transform:

$$ H[z] = \sum\limits_{n=0}^{N} h[n] z^{-n} $$$$ H(z) = 0.5 z^{-2} + z^{-3} + 0.5 z^{-4} $$

In the next step, all exponents are shifted by expanding with $z^{4}$:

$$ H(z) = \frac{z^2 + 2z + 1}{z^4} $$

Factor the denominator to find its roots:

$$ H(z) = \frac{(z+1)(z+1)}{z^4} $$

There is a double zero at $\mathbf{z=-1}$. As all causal FIR filters it has $N=4$ poles at the origin $z=0$.

With $z=e^{j\omega}$ the complex roots can be obtained:

$$ Z = (e^{-i\omega} +1)(e^{-i\omega} +1) $$

In the Unit Cirlce

An LTI system can be visualized in the z-plane by plotting the zeros, respectively the roots of its impulse response.

Frequency Response

The frequency response of the filter