I first met Julius Smith in 2010 when I was a PhD student at McGill. Julius was invited to Montreal by CIRMMT to give a distinguished lecture about his pioneering work on digital waveguide synthesis. During that visit I had the opportunity to have dinner with Julius and attend a workshop he lead using Faust. I hold Julius in very high regard — he has contributed so much to the field of computer music, including 4 excellent books — so to see him genuinely excited about a computer music language was something I took notice of.
I just fell in love with it and started using it. It’s an amazing language.
We take algorithms from the best papers we know about and type them up in Faust. Faust is such a nice, high-level language, that once you know what to type, you can probably get it down in 1 line.
— Julius Smith on Faust
Besides its conciseness, Faust is appealing for many reasons. Its block diagram oriented syntax means that if you can draw a block diagram for an algorithm, it will probably be relatively easy to implement that algorithm in Faust — think of it as a text-based counterpart to graphical patching languages like Max/MSP or Pd, but geared more towards DSP.
Probably one of the nicest things about Faust is that it outputs simple C code that can be integrated into nearly any DSP application (including embedded hardware). Faust makes this integration even easier with a suite of architecture files and command line tools that can automatically generate VST plugins and AudioUnits, or Max/MSP, Pd, and SuperCollider externals — all of this from the exact same Faust code.
In this post I will walk through the process of building an artificial reverb plugin using Faust. But first let’s listen to a sample of what we are going to build. In the following recording I activated the reverb plugin after the first few seconds, and then played a bit with the feedback gain throughout:
The architecture for this reverb is courtesy of Keith Barr, founder of Alesis and designer of the Alesis Midiverb II (used by My Bloody Valentine, Autechre, and Aphex Twin, amongst others ). As described in this blog post, Keith’s reverb architecture contains a loop of allpass filters and delays:
The big loop is great; you inject input everywhere, but take it out in only two places.... It just keeps comin’ new and fresh as the thing decays away.
— Keith Barr
Allpass filters are a staple in artificial reverb, and have been used for this purpose since as early as 1961. True to their name allpass filters pass all frequencies, but with a frequency dependent phase delay (which can be used as a crude approximation of the echoes in an acoustic space). To create more realistic artificial reverberation, several allpass filters are usually cascaded together and feedback is added to increase the echo density.
An allpass filter in Faust
The following block diagram shows an allpass filter, consisting of a feedforward and a feedback path.
Let’s translate this block diagram to Faust, starting with the feedforward path. We represent this part of the block diagram in Faust as:
feedforward(M,g) = _ <: @(M), *(-g) : + : _;
process = feedforward(10, 0.5);
This simple Faust program already introduces several important concepts. Working from left to right, we start with a wire
_ which is then split into two parallel wires
<:. The top path is passed through and M-sample delay
@(M), and the bottom path is multiplied by
*(-g). The two paths are then summed
+ giving a single output
_. The character
: is used to represent sequential composition of block diagrams, whereas the
, character is used to represent parallel composition. The
process command represents the Faust program (like “main” in C).
If we save this Faust program as
feedforward.dsp and run
faust2svg feedforward.dsp an SVG file with the following block diagram will be generated:
Now we will add in the feedback path:
// warning, this is wrong!
allpass(M,g) = (+ : _ <: @(M), *(-g)) ~ *(g) : + : _;
process = allpass(10, 0.5);
a ~ b feeds block diagram b back into block diagram a. Although the block diagram generated from this code looks correct there is an important gotcha that makes it incorrect. This is because whenever the feedback operator is used a 1-sample delay is introduced into the feedback loop. Therefore, to get the correct allpass response we must alter the Faust code to:
// accounting for 1-sample delay in feedback loop
allpass(M,g) = (+ : _ <: @(M-1), *(-g)) ~ *(g) : mem, _ : + : _;
process = allpass(10, 0.5);
I have subtracted 1 from the M sample delay to account for that fact that a 1-sample delay is added inside the feedback loop. We must also add an extra 1-sample delay to the part of the block diagram that doesn’t feedback to keep the overall delay at M-samples (this is done using the keyword
mem). That’s it — an allpass filter in 1-line of Faust code:
Keith Barr’s reverb
Now let’s take a look at Keith Barr’s reverb architecture
There are 4 sections each with 2 allpass filters and a delay, and the last section feeds back into the first section. The input is injected at the start of each section, and the output is taken at the end of each section. The Faust code for a single section is as follows
allpass(N,n,g) = (+ <: (delay(N, n), *(g))) ~ *(-g) : mem, _: +;
section((n1, n2)) = allpass(65536, n1, 0.5) : allpass(65536, n2, 0.5) : delay(65535, n1+n2);
I’ve altered the allpass filter slightly, replacing
delay from the “delay.lib” library so that the delay line length can be changed. Next we’ll want to chain several allpass sections together. But before we do, we need to take a slight detour and talk about recursion, destructuring and pattern matching. By way of example, consider the following recursive program that sums a list of numbers:
rsum((x, xs)) = x, rsum(xs) : + ;
rsum(x) = x;
process = rsum((1,2,3,4));
Notice that there are two definitions of the function
rsum. When a call to
rsum is made, Faust will compare the call to each one of these definitions (in the same order that they have been defined), and execute the first one that matches. Faust also destructures the input arguments, so that when we call
rsum((1,2,3,4)), 1 gets bound to
x and (2,3,4) gets bound to
xs. The function
rsum is called recursively until we reach the edge case
rsum(4) which matches the second (but not the first) definition. At this point the recursion ends. Faust’s block diagram for this program is:
I use this approach to chain together a set of allpass sections as follows:
allpass_chain(((n1, n2), ns), x) = _ : section((n1, n2)) <: R(x, ns), _
R(x, ((n1, n2), ns)) = _,x : + : section((n1, n2)) <: R(x, ns), _;
R(x, (n1, n2)) = _,x : + : section((n1, n2));
I have tapped each section to the output with a wire
with statement in Faust simply defines a local scope (so the function R is only available to the allpass_chain). Lastly, we add a feedback path with 1 section to the allpass chain:
feedback_slider = hslider("feedback",0.5,0.0,1.0,0.01):smooth(0.99);
primes = ffunction(int primes (int),<primes.h>,"primes");
process = procMono
ind(i) = 100 + 10*pow(2,i);
feedfwd_delays = par(i, 5, (primes(ind(i)), primes(ind(i)+1)));
feedback_delays = (primes(100), primes(101));
procMono(x) = x : (+ : allpass_chain(feedfwd_delays, x)) ~ (_,x : + : section(feedback_delays) : *(feedback_slider)) :> _;
There are a few new concepts introduced in this code. Faust has a number of GUI functions, like “hslider”, that are automatically turned into visual controls, e.g., when building a VST plugin. It is also possible to import foreign functions defined in external C code using “ffunction”. Above we set the delay lengths to prime numbers using the primes foreign function (prime number delays tend to give better echo density). Lastly, the “par” macro is used to expand a function into a parallel block diagram (with different arguments in each branch). A much more detailed discussion of these concepts can be found in Julius Smith’s excellent Faust tutorial.
That’s basically it, and it only took a few lines of code. From this code Faust can generate VSTs, AUs, Max/MSP, Pd, and SuperCollider externals (among other options) using the command line tools
faust2supercollider, respectively (assuming the appropriate include files have been simlinked to
You can grab the full source for this post, which has been augmented for stereo processing, from my github page. The release page also has pre-compiled VST, AU, Max/MSP, Pd, and Supercollider plugins (build on a Mac OS X Yosemite).
The following links include more really useful information used in the preperation of this blog post .
P.s. you can get in touch with me at: info _at_ reverberate.ca