Lab 1: Faust Basics and Digital Musical Instrument

Basic Synthesizer

Anatomy of a Simple Faust Program

A typical Faust program is usually divided into 3 sections:

The only required element of a Faust program is its process call. Hence,

process = 1;

is a valid Faust program always outputting 1 (DC offset).

The “hello world of computer music” (a sine wave oscillator) can be easily implemented in Faust by calling the sine wave oscillator function (os.osc):

import("stdfaust.lib");
freq = 440;
process = os.osc(freq);

stdfaust.lib gives us access here to os.osc and freq is a variable that we created to control the single parameter of os.osc which is its frequency.

Try to run this program in the Faust Web IDE by copying it and pasting it in the editor window and by clicking on the “run” button. You should hear a sound coming out of the speakers of your computer.

Currently, we’re missing an interface for our Faust program so its parameter(s) cannot be controlled in real time. We can modify the definition of freq so that its value can be changed by a slider:

freq = hslider("freq",440,50,2000,0.01);

Make this modification in the program in the web IDE and see what happens. hslider stands for “Horizontal Slider,” freq is the name of the slider in the generated interface, 440 its default value, 50 its minimum value, 2000 its maximum value, and 0.01 its precision (here every new step in the slider will increment or decrement the value of freq by 0.01). Our code now contains all the elements of a standard Faust program (lib imports, interface, and mapping/DSP) and should look like this:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
process = os.osc(freq);

Adding Musical Instrument Parameters

On “traditional” musical instruments, a musical note is configured with at least 3 parameters: its pitch, its gain/velocity and the fact that it’s on or off. Currently, our Faust program implements only one of these parameters which is the pitch (freq). To control the status of the instrument (on or off) we can add a button and multiply its output by os.osc:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gate = button("gate");
process = os.osc(freq)*gate;

Try it!

Since button outputs 1 when it is pressed and 0 when it is not pressed, multiplying it by os.osc will allow us to turn the synthesizer on and off.

Finally, a gain slider can be easily added using the same approach:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
gate = button("gate");
process = os.osc(freq)*gain*gate;

Timbre

The timbre (the sound) produced by a sine wave is quite boring. A simple way to generate sounds with a richer timbre is to use other waveforms such as square, sawtooth or triangle waves which are respectively:

os.square(freq)
os.sawtooth(freq)
os.triangle(freq)

in Faust.

Try to replace os.osc in the previous Faust program to see how these waveforms impact the timbre of the generated sound.

Envelope

The envelope of a sound describes how its gain evolves in time. It has a huge impact on how we perceive the sound. The two main kinds of envelope generators are AR (Attack immediately followed by a Release) and ADSR (Attack Decay Sustain Release). In that last case, the durations of the attack, the decay and the release are constant while sustain will depend on the status of note (on or off). The previous program can be easily modified to integrate an envelope generator:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
gate = button("gate");
envelope = en.ar(0.1,0.2,gate)*gain;
process = os.osc(freq)*envelope;

where 0.1 in en.ar is the duration of the attack in seconds and 0.2 the duration of the release,

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
gate = button("gate");
envelope = en.adsr(0.1,0.2,0.9,0.3,gate)*gain;
process = os.osc(freq)*envelope;

where 0.1 in en.adsr is the duration of the attack in seconds, 0.2 the duration of the decay, 0.9 the gain of the sustain and 0.3 the duration of the release.

Try different values for en.ar and en.adsr to see how they impact the generated sound. AR envelope generators are typically used for percussive instruments like drums, etc. while ADSR is perfect for wind instruments with a sustained tone.

Polyphony and MIDI

Faust synthesizers can be easily made polyphonic in the Web IDE by changing the number of “Poly Voices” on the left panel. Choosing 16 there will create 16 parallel polyphonic voices. In order for a synthesizer to be polyphonic, the freq, gain, and gate parameters must be declared. They are automatically mapped by Faust to MIDI note on/off events (gate), MIDI pitches (freq), and velocity (gain).

The Faust program written in the previous section is compatible with this system. Currently, the only Web browser supporting MIDI is Google Chrome. However you should be able to use your computer’s built-in keyboard to control a Faust program in Firefox (and Chrome). So if you actually want to use MIDI, you’ll have to use Chrome and a MIDI keyboard. If you don’t own one, you can use a virtual MIDI keyboard such as VMPK (Windows, Linux, MacOS) or MidiKeys (MacOS). Once again, using your computer’s built-in keyboard is an option as well, just make sure that you select the right source in the top right menu in the Faust Web IDE. If you choose this option, the “a” to “l” row are notes, the “q” to “p” row are sharps and flats, and the “z” and “x” key allow you to shift octaves up and down. Make sure to click outside of the editing area before pressing the keys to control the sound.

The DSP code written in the process line will be duplicated for each voice of the synth. Audio effects applying to all polyphonic voices can be added by declaring the effect line. Hence,

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
gate = button("gate");
envelope = en.adsr(0.1,0.2,0.9,0.3,gate)*gain;
process = os.sawtooth(freq)*envelope <: _,_;
effect = dm.zita_light;

will apply a reverb (dm.zita_light) on all poly voices of the synth by instantiating the reverb only once which will significantly save computation.

Note that <: _,_ was added to the process line to split the output of the sawtooth oscillator into 2 signals because zita_light has a stereo input.

Warning: be careful not to leave the polyphonic mode on (reset it back to Mono) as it might prevent future Faust programs from running. You also probably noticed that activating polyphony disables the user interface elements associated to the freq, gate, and gain parameters.

Shaping Sound: Subtractive Synthesis

The previous synthesizer is pretty boring, partly because the timbre of the sound it generates is constant and doesn’t evolve in time. In the “real world” sound produced by musical instruments always have an evolving timbre. A simple way to solve this problem here is to use subtractive synthesis which consists in filtering a sound with a dense harmonic content. Highpass, lowpass and bandpass filters can be used for that. Faust comes with various types of such filters:

fi.lowpass(order,freq)
fi.highpass(order,freq)
fi.resonlp(freq,q,gain)
fi.resonbp(freq,q,gain)
fi.resonhp(freq,q,gain)

For more information on each of them, please refer to the Faust libraries documentation.

The previous Faust program can be quickly modified to implement a subtractive synthesizer using a resonant lowpass filter:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
ctfreq = hslider("ctfreq",1000,50,5000,0.01) : si.smoo;
gain = hslider("gain",1,0,1,0.01);
gate = button("gate");
envelope = en.adsr(0.1,0.2,0.9,0.3,gate)*gain;
process = os.sawtooth(freq)*envelope : fi.resonlp(ctfreq,5,1)*0.3 <: _,_;
effect = dm.zita_light;

In that case, the cutoff frequency of the filter is controlled by a user interface element. Its output value is smoothed using si.smoo to prevent abrupt changes/clicks in the signal. Note that we multiplied the overall gain of the synth by 0.3 because resonlp tends to increase the gain because of its resonance. In order to dynamically change the value of this parameter, it could be directly associated to the envelope generator using some kind of mapping:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
gate = button("gate");
envelope = en.adsr(0.1,0.2,0.9,0.3,gate)*gain;
ctfreq = envelope*5000+100;
process = os.sawtooth(freq)*envelope : fi.resonlp(ctfreq,5,1)*0.3 <: _,_;
effect = dm.zita_light;

You can get very creative with subtractive synthesis by trying out different kind of filters and oscillators, combining them and mapping them in different ways.

Automatizing Things

A simple way to automatize the triggering of the synthesizer written in the previous section is to replace the value of gate currently controlled by a button by a pulse generator (ba.pulse in Faust). Its single parameter is the period in samples at which pulses are being generated. This value can be easily formatted from a BPM using the sampling rate:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
BPM = 60/hslider("BPM",60,1,1000,0.01)*ma.SR;
gate = checkbox("gate")*ba.pulse(BPM);
envelope = en.ar(0.1,0.2,gate)*gain;
ctfreq = envelope*5000+100;
process = os.sawtooth(freq)*envelope : fi.resonlp(ctfreq,5,1)*0.3 <: _,_;

Note that the button was also replaced by a checkbox which acts as a toggle here.

Iteration

To conclude this tutorial, we’re going to create 3 instances of our synth in parallel and add them together to create a chord generator:

import("stdfaust.lib");
freq = hslider("freq",440,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
third = hslider("third",0,0,1,0.01);
BPM = 60/hslider("BPM",300,1,1000,0.01)*ma.SR;
gate = checkbox("gate")*ba.pulse(BPM);
envelope = en.ar(0.1,0.2,gate)*gain;
ctfreq = envelope*5000+100;
chord(0) = 1;
chord(1) = 1.2+third;
chord(2) = 1.5;
process = sum(i,3,os.sawtooth(freq*chord(i))*envelope : fi.resonlp(ctfreq,5,1))*0.3 <: _,_;

Here, sum allows to duplicate and add 3 instances of our synth. i is a variable containing the instance number (i.e., 0, 1, 2). chord is a ratio that is used to change the value of the fundamental frequency in function of the instance number. Finally, third allows to tune up the pitch of the “third” of the chord. Note that we multiplied

Exploring the Faust Libraries

Customizing a Function From the Libraries

The Faust distribution comes with a series of libraries implementing a wide range of synthesizers, audio effects, etc. The source code of the Faust libraries can be found in the Faust Libraries repository on GitHub: https://github.com/grame-cncm/faustlibraries. Also, the documentation of the Faust libraries is available on the Faust libraries documentation website: https://faustlibraries.grame.fr/

Each .lib file contains the source code of a specific library.

All standard Faust libraries are accessible through a series of prefixes declared in stdfaust.lib. For example, to call the sine oscillator function (osc) declared in oscillators.lib, one might write:

import("oscillators.lib");
process = osc(440);

and alternatively:

import("stdfaust.lib");
process = os.osc(440);

Some functions of the Faust libraries host their own user interface. Most of these functions are declared in demos.lib and physmodels.lib. For example, a ready-to-use clarinet physical model can be found physmodels.lib and called with just 2 lines of code:

import("stdfaust.lib");
process = pm.violin_ui;

Exploring the source code of physmodels.lib, the declaration of the violin_ui function can be easily found: https://github.com/grame-cncm/faustlibraries/blob/master/physmodels.lib#L1513

As an exercise, try to write your own version of violin_ui (i.e., my_violin_ui) with custom parameter names.

Solution

Adding Vibrato

Vibrato is a crucial esthetic feature when performing with some musical instruments in western music. Moreover, in the case of sound synthesis it can help make the harmonic content of a sound more “coherent” (effect of source segregation).

Vibrato can be implemented in Faust simply by modulating the pitch of the generated sound with a sine wave oscillator (e.g., pitch + osc(vibratoFreq)*vibratoAmp).

Try to modify the previous example by adding 2 user interface elements to control the frequency and the amplitude of the vibrato. The frequency shouldn’t exceed 10Hz and a good default value is 6Hz.

Hint: don’t forget that a sine function has both positive and negative values.

Solution

Adding an Audio Effect

compressors.lib, misceffects.lib, phaflangers.lib, reverbs.lib, and vaeffects.lib contain a wide range of audio effects. Some of them have a demo version (with a built-in UI) that can be found in demos.lib. This table gives of an overview of the most standard audio effects available in the Faust libraries.

We’d like to “improve” our instrument from the previous step (violin with vibrato) with a phasor. A stereo phaser is available in demos.lib: https://github.com/grame-cncm/faustlibraries/blob/master/demos.lib#L477. Since my_violin_ui has one output and phaser2_demo has 2 inputs (stereo), we must use the split operator:

process = my_violin_ui <: my_phaser2_demo;

Try to make this modification to the previous program to see what happens!

Solution

Assignment 0: Discovering and Playing Around With Faust (Due on Apr. 7, 2021)

Assignment 1: Making Your First DMI With Faust (Due on Apr. 14, 2021)