MIDI and Polyphony Support
Similarly to OSC, several Faust architectures also provide MIDI support. This allows Faust applications to be controlled from any MIDI device (or to control MIDI devices). MIDI is also the preferable way to control Polyphonic instruments.
Configuring MIDI in Faust
MIDI support can be added to any Faust program (as long as the target architecture supports it: see tables below) simply by adding the [midi:on]
metadata to the standard options
metadata:
declare options "[midi:on]";
or using the -midi
option in most of faust2xx scripts.
MIDI control is configured in Faust using metadata in UI elements. MIDI channels from 1 to 16 can be used at reception or sending time. By convention, using the channel = 0 value (or no channel setting) means "all channels". That is, at reception time, MIDI messages received on all channels can be used to control a given parameter, and at sending time, the MIDI message associated to a parameter will be sent on all channels.
MIDI metadata are decoded by a special architecture that parses incoming MIDI messages and updates the appropriate control parameters, or send MIDI messages when the UI elements (i.e., sliders, buttons, etc.) are moved.
All MIDI configuration metadata in Faust follow the following format:
[midi:xxx yyy...]
This section provides a list of the most common metadata that can be used to configure of the MIDI behavior of a Faust program.
Below, when a 7-bit MIDI parameter is used to drive a button or a checkbox, its maximum value (127) maps to 1 ("on") while its minimum value (0) maps to 0 ("off").
[midi:ctrl num]
or [midi:ctrl num chan]
Metadata
The [midi:ctrl num]
metadata assigns MIDI CC (control) to a specific UI element. When used in a slider/nentry or a bargraph, this metadata will map the UI element value to the {0, 127} range. When used with a button or a checkbox, 1 will be mapped to 127, 0 will be mapped to 0. The first [midi:ctrl num]
version can receive messages on all channels, and will send on all channels. The second [midi:ctrl num chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = hslider("foo[midi:ctrl num chan]",...);
Where:
num
: the MIDI CC numberchan
: optional, the MIDI channel number
Example
In the following example, the frequency of a sawtooth wave oscillator is controlled by MIDI CC 11. When CC11=0, then the frequency is 200Hz, when CC11=127, then the frequency is 1000Hz.
[midi:keyon midikey]
or [midi:keyon midikey chan]
Metadata
The [midi:keyon midikey]
metadata assigns the velocity value of a key-on MIDI message received on a specific midikey
to a Faust parameter. When used in a slider/nentry or a bargraph, this metadata will map the UI element value to the {0, 127} range. When used with a button or a checkbox, 1 will be mapped to 127, 0 will be mapped to 0. The first [midi:keyon midikey]
version can receive messages on all channels, and will send on all channels. The second [midi:keyon midikey chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = hslider("foo[midi:keyon midikey chan]",...);
Where:
midikey
: the MIDI key numberchan
: optional, the MIDI channel number
Example
In the following example, the frequency of a sawtooth wave oscillator is controlled by the velocity value received on key 62 when a key-on message is sent. Therefore, the frequency will only be updated when MIDI key 62 is pressed.
[midi:keyoff midikey]
or [midi:keyoff midikey chan]
Metadata
The [midi:keyoff midikey]
metadata assigns the velocity value of a key-off MIDI message received on a specific midikey
to a Faust parameter. When used in a slider/nentry or a bargraph, this metadata will map the UI element value to the {0, 127} range. When used with a button or a checkbox, 1 will be mapped to 127, 0 will be mapped to 0. The first [midi:keyoff midikey]
version can receive messages on all channels, and will send on all channels. The second [midi:keyoff midikey chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = hslider("foo[midi:keyoff midikey chan]",...);
Where:
midikey
: the MIDI key numberchan
: optional, the MIDI channel number
Example
In the following example, the frequency of a sawtooth wave oscillator is controlled by the velocity value received on key 62 when a key-off message is sent. Therefore, the frequency will only be updated when MIDI key 62 is released.
[midi:key midikey]
or [midi:key midikey chan]
Metadata
The [midi:key midikey]
metadata assigns the velocity value of key-on and key-off MIDI messages received on a specific midikey
to a Faust parameter. When used in a slider/nentry or a bargraph, this metadata will map the UI element value to the {0, 127} range. When used with a button or a checkbox, 1 will be mapped to 127, 0 will be mapped to 0. The first [midi:midikey midikey]
version can receive messages on all channels, and will send on all channels. The second [midi:midikey midikey chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = hslider("foo[midi:key midikey chan]",...);
Where:
midikey
: the MIDI key numberchan
: optional, the MIDI channel number
Example
In the following example, the frequency of a sawtooth wave oscillator is controlled by the velocity value received on key 62 when key-on and key-off messages are sent. Therefore, the frequency will only be updated when MIDI key 62 is pressed and released.
[midi:keypress midikey]
or [midi:keypress midikey chan]
Metadata
The [midi:keypress midikey]
metadata assigns the pressure (after-touch) value of a specific midikey
to a Faust parameter. When used in a slider/nentry or a bargraph, this metadata will map the UI element value to the {0, 127} range. When used with a button or a checkbox, 1 will be mapped to 127, 0 will be mapped to 0. The first [midi:keypress midikey]
version can receive messages on all channels, will send on all channels. The second [midi:keypress midikey chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = hslider("foo[midi:keypress midikey chan]",...);
Where:
midikey
: the MIDI key numberchan
: optional, the MIDI channel number
Example
In the following example, the frequency of a sawtooth wave oscillator is controlled by the pressure (after-touch) values received on key 62.
[midi:chanpress]
or [midi:chanpress chan]
Metadata
The [midi:chanpress]
metadata assigns the channel pressure value to a Faust parameter. When used in a slider/nentry or a bargraph, this metadata will map the UI element value to the {0, 127} range. When used with a button or a checkbox, 1 will be mapped to 127, 0 will be mapped to 0. The first [midi:chanpress]
version can receive messages on all channels, and will send on all channels. The second [midi:chanpress chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = hslider("foo[midi:chanpress chan]",...);
Where:
chan
: optional, the MIDI channel number
Example
In the following example, the volume of a sawtooth wave oscillator is controlled by the channel pressure values.
[midi:pgm]
or [midi:pgm chan]
Metadata
The [midi:pgm]
metadata assigns the program-change to a Faust parameter. When used in a slider/nentry or a bargraph, this metadata will use the UI element range. Only the values described in the UI element range will be used at reception, and can be sent. The first [midi:pgm]
version can receive messages on all channels, and will send on all channels. The second [midi:pgm chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = nentry("foo[midi:pgm chan]",...);
Where:
chan
: optional, the MIDI channel number
Example
In the following example, the instrument will be choosen in the {0,3} range.
[midi:pitchwheel]
or [midi:pitchwheel chan]
Metadata
The [midi:pitchwheel]
metadata assigns the pitch-wheel value to a Faust parameter. When used in a slider/nentry or a bargraph, this metadata will map the UI element value to the {0, 16383} range. When used with a button or a checkbox, 1 will be mapped to 16383, 0 will be mapped to 0. Pitch-wheel values are typically used to control tuning, mapping the {0, 16383} MIDI range to a {-semitone, semitone} range. The ba.semi2ratio
function must be used to convert semitones in a frequency multiplicative ratio. The first [midi:pitchwheel]
version can receive messages on all channels, and will send on all channels. The second [midi:pitchwheel chan]
version can receive messages on chan
only, and will send on the chan
channel only (or all channels using the chan = 0
value).
Usage
foo = hslider("foo[midi:pitchwheel chan]",...);
Where:
chan
: optional, the MIDI channel number
Example
In the following example, the bend of a sawtooth wave oscillator is controlled by the pitch-wheel in the {-2, 2} semitone range, then converted as a frequency multiplicative ratio and multiplied by a given frequency.
[midi:start]
Metadata
When used with a button or a checkbox, [midi:start]
will trigger a value of 1 when a start
MIDI message is received.
Usage
foo = checkbox("foo[midi:start]");
[midi:stop]
Metadata
When used with a button or a checkbox, [midi:stop]
will trigger a value of 0 when a stop
MIDI message is received.
Usage
foo = checkbox("foo[midi:stop]");
[midi:clock]
Metadata
When used with a button or a checkbox, [midi:clock]
will deliver a sequence of successive 1 and 0 values each time a clock
MIDI message is received (seen by Faust code as a square command signal, to be used to compute higher level information).
Usage
foo = checkbox("foo[midi:clock]");
MIDI Sync
MIDI clock-based synchronization can be used to slave a given Faust program using the metadata presented in the 3 past sections.
A typical Faust program will then use the MIDI clock stream to possibly compute the BPM information, or for any synchronization need it may have. Here is a simple example of a sinus generated which a frequency controlled by the MIDI clock stream, and starting/stopping when receiving the MIDI start/stop messages:
MIDI Polyphony Support
Polyphony is conveniently handled in Faust directly by Faust Architectures. Note that programming polyphonic instrument completely from scratch in Faust and without relying on architectures is also possible. In fact, this feature is indispensable if complex signal interactions between the different voices have to be described (like sympathetic strings resonance in a physical model, etc.). However, since all voices would always be computed, this approach could be too CPU costly for simpler or more limited needs. In this case describing a single voice in a Faust DSP program and externally combining several of them with a special polyphonic instrument aware architecture file is a better solution. Moreover, this special architecture file takes care of dynamic voice allocation and control MIDI messages decoding and mapping.
Polyphony support can be added to any Faust program (as long as the target architecture supports it) simply by adding the [nvoices:n]
metadata to the standard option
metadata where n
is the maximum number of voices of polyphony to be allocated:
declare options "[midi:on][nvoices:12]";
Standard Polyphony Parameters
Most Faust architectures allow for the implementation of polyphonic instruments simply by using a set of "standard user interface names." Hence, any Faust program declaring the freq (or key)
, gain (or vel or velocity)
, and gate
parameter is polyphony-compatible. These 3 parameters are directly associated to key-on and key-off events and have the following behavior:
- When a key-on event is received,
gate
will be set to 1. Inversely, when a key-off event is received,gate
will be set to 0. Therefore,gate
is typically used to trigger an envelope, etc. freq
is a frequency in Hz computed automatically in function of the value of the pitch contained in a key-on or a key-off message. Alternativelykey
can be used to get the raw MIDI pitch and describe the pitch to Hz conversion in the DSP code itself (for instance to implement alternative tunings).gain
is a linear gain (value between 0-1) computed in function of the velocity value contained in a key-on or a key-off message. Alternativelyvel
orvelocity
can be used to get the raw MIDI velocity and describe the velocity to gain conversion in the DSP code itself (for instance to implement alternative velocity curves).
Example: Simple Polyphonic Synthesizer
In the following example, the standard freq
, gain
, and gate
parameters are used to implement a simple polyphonic synth.
Note that if you execute this code in the Faust Online IDE with polyphony mode activated, you should be able to control this simple synth with any MIDI keyboard connected to your computer. This will only work if you're using Google Chrome (most other browsers are not MIDI-compatible).
In the next example, the standard key
, gain
, and gate
parameters are used to implement a simple polyphonic synth with alternate tuning.
Note that if you execute this code in the Faust Online IDE with polyphony mode activated, you should be able to control this simple synth with any MIDI keyboard connected to your computer. This will only work if you're using Google Chrome (most other browsers are not MIDI-compatible).
The previous example can be slightly improved by adding an envelope generator and controlling it with gain
and gate
:
Warning: Note that all the active voices of polyphony are added together without scaling! This means that the previous example will likely click if several voices are played at the same time. It is the Faust programmer's responsibility to take this into account in his code. For example, assuming that the number of active voices will always be smaller or equal to 4, the following safeguard could be added to the previous example:
process = os.sawtooth(freq)*envelope : /(4);
Configuring and Activating Polyphony
Polyphony can be activated manually in some Faust architectures using an option/flag during compilation (e.g., typically -poly
or -nvoices <num>
in the faust2xx scripts). That's also how the Faust Online IDE works where a button can be used to turn polyphony on or off.
However, the most standard way to activate polyphony in Faust is to declare the [nvoices:n]
metadata which allows us to specify the maximum number of voices of polyphony (n
) that will be allocated in the generated program.
For example, the Faust program from the previous section could be modified such that:
which when compiled running (for example):
faust2jaqt faustProgram.dsp
will generate a MIDI-controllable polyphonic synthesizer.
Audio Effects and Polyphonic Synthesizer
While audio audio effects can be added directly to the process
line of a Faust synthesizer, for example:
process = os.sawtooth(freq)*envelope : reverb;
it is not a good practice since a new instance of that effect will be created for each active voice of polyphony. The main consequence of this would be an increased CPU cost.
Similarly to process
, Faust allows for the declaration of an effect
line, which identifies an audio effect to be connected to the output of the polyphonic synthesizer.
For example, a simple reverb can be added to the previous example simply by writing:
In this case, the polyphonic part is based on process
and a single instance of the effect defined in effect
will be created and shared by all voices.
The architecture machinery then separates the instrument description itself (the process = ...
definition) from the effect definition (the effect = ...
definition), possibly adapts the instrument number of outputs to the effect number of inputs, compiles each part separately, and combines them.
Here dm.zita_light
is a stereo effect, the mono output of the os.sawtooth(freq)*envelope
expression can be split into 2 signals (but would be splitted automatically if kept as a mono output). Also, be aware that this type of construction wont be visible in the corresponding block diagram that will only show what's implemented in the process
line.
Polyphony and Continuous Pitch
Key-on and key-off MIDI messages only send the "base pitch" of the instance of a note. Hence, if only the freq
standard parameter is used to control the frequency of the synthesizer, its pitch will always be "quantized" to the nearest semitone. In order to be able to do glissandi, vibrato, etc., a variable associated to the pitch-wheel needs to be declared and must interact with the "base frequency" value retrieved from freq
as such:
f = hslider("freq",300,50,2000,0.01);
bend = ba.semi2ratio(hslider("bend[midi:pitchwheel]",0,-2,2,0.01));
freq = f*bend; // the "final" freq parameter to be used
The bend
variable is controlled by the pitch-wheel thanks to [midi:pitchwheel]
metadata, then converted in a frequency ratio using the ba.semi2ratio
function. bend
is used as a factor multiplied to the base frequency retrieved from freq
. Therefore, the default value of bend
is 0 in semitones, which corresponds to the central position of the pitch wheel, and will correspond to a multiplicative ratio of 1. A value smaller than 0 will decrease the pitch and a value greater than 0 will increase it.
While the above example will have the expected behavior, it is likely that clicking will happen when changing the value of bend
since this parameter is not smoothed. Unfortunately, regular smoothing (through the use of si.smoo
, for example) is not a good option here. This is due to the fact that instances of polyphonic voices are frozen when a voice is not being used. Since the value of bend
might jump from one value to another when a voice is being reactivated/reused, continuous smoothing would probably create an "ugly sweep" in that case. Hence, si.polySmooth
should be used in this context instead of si.smoo
. This function shuts down smoothing for a given number of samples when a trigger is activated.
Reusing the example from the previous section, we can implement a click-free polyphonic synthesizer with continuous pitch control:
Observe the usage of si.polySmooth
here: when gate=0
the signal is not smoothed, when gate=1
the signal is smoothed with a factor of 0.999 after one sample.
Complete Example: Sustain Pedal and Additional Parameters
Just for fun ;), we improve in this section the example from the previous one by implementing sustain pedal control as well as some modulation controlled by the modulation wheel of the MIDI keyboard.
Sustain pedal control can be easily added simply by declaring a sustain parameter controlled by MIDI CC 64 (which is directly linked to the sustain pedal) and interacting with the standard gate
parameter:
s = hslider("sustain[midi:ctrl 64]",0,0,1,1);
t = button("gate");
gate = t+s : min(1);
Hence, gate
will remain equal to 1 as long as the sustain pedal is pressed.
The simple synthesizer from the previous section (which is literally just a sawtooth oscillator) can be slightly improved by processing it with a dynamically-controlled lowpass filter:
MIDI CC 1 corresponds to the modulation wheel which is used here to control the cut-off frequency of the lowpass filter.
Next we can add a global master, using the MIDI CC 7 standard volume control to change its level: