0% found this document useful (0 votes)
56 views

Faust Soft Computing

This paper presents some syntactical and semantic aspects of FAUST, a programming language for real-time sound processing and synthesis. FAUST combines functional programming and block diagram composition. It is based on a block diagram algebra that represents programs as trees rather than graphs. This allows FAUST to have a well-defined formal semantics and be compiled into efficient code. The paper describes the block diagram algebra, which is independent of FAUST, and how it represents programs through composition operators like sequential, parallel and split composition. It also outlines how the paper will provide a concrete example using Karplus-Strong synthesis and describe FAUST's compiler.

Uploaded by

Matt Wall
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
56 views

Faust Soft Computing

This paper presents some syntactical and semantic aspects of FAUST, a programming language for real-time sound processing and synthesis. FAUST combines functional programming and block diagram composition. It is based on a block diagram algebra that represents programs as trees rather than graphs. This allows FAUST to have a well-defined formal semantics and be compiled into efficient code. The paper describes the block diagram algebra, which is independent of FAUST, and how it represents programs through composition operators like sequential, parallel and split composition. It also outlines how the paper will provide a concrete example using Karplus-Strong synthesis and describe FAUST's compiler.

Uploaded by

Matt Wall
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

Soft Computing manuscript No.

(will be inserted by the editor)

Syntactical and Semantical Aspects of Faust


Yann Orlarey, Dominique Fober, Stephane Letz
Grame, Centre National de Creation Musciale, Lyon, France

Received: date / Revised version: date

Abstract This paper presents some syntactical and seman- will also describe the complete set of primitives of the lan-
tical aspects of FAUST (Functional AUdio STreams), a pro- guage.
gramming language for real-time sound processing and syn- In section 4 we will give a concrete example of usage
thesis. The programming model of FAUST combines two ap- of Faust with the Karplus-Strong algorithm. In section 5 we
proaches : functional programming and block-diagrams com- will give an overview of how the Faust compiler works. We
position. It is based on a block-diagram algebra. It has a well will end the paper with some concluding remarks and future
defined formal semantic and can be compiled into efficient directions of work.
C/C++ code.

Key words functional programming – real-time – signal 2 The block-diagram algebra


processing – dataflow – compiler
Block-diagram formalisms are widely used in visual langua-
ges particularly in musical languages. The user creates pro-
grams (i.e. block-diagrams), by connecting graphical blocks
which represent the functionalities of the system. In almost
1 Introduction every implementation, a block-diagram is represented inter-
nally as a graph, and interpreted as a dataflow computation
FAUST (Functional AUdio STreams), is a programming lan- (see [2] and[1] for historical papers on dataflow, and [7] or
guage for real-time signal processing and synthesis. It targets [4] for surveys).
high-performance signal processing applications and audio This very common approach has several drawbacks:
plugins. It has been designed with three main goals in mind :
expressiveness, clean mathematical semantics and efficiency. 1. Due to their generality, the semantics of dataflow mod-
els can be quite complex. It depends on many technical
Expressiveness is achieved by combining two approaches:
choices like for example, synchronous or asynchronous
functional programming and algebraic block-diagrams (ex-
computations, deterministic or non-deterministic behav-
tended function composition). This computation model has
ior, bounded or unbounded communication FIFOs, firing
also the advantage of a simple and well defined formal se-
rules, etc. Because of this complexity, the vast majority of
mantics.
dataflow inspired music languages have no explicit formal
Having clean semantics is not just of academic interest. It semantic. The semantics is hidden in the dataflow engine.
allows the Faust compiler to be semantically driven. Instead The real behavior of a block-diagram can be difficult to
of compiling the block-diagram itself, it compiles ”what the understand without a good knowledge of the implemen-
block-diagram compute”. It also allows to discover simplifi- tation.
cations and factorizations to produce efficient code. 2. Dataflow models are difficult to implement efficiently and
Faust is build on top of a block-diagram algebra that we most of the time no compiler exists, only an interpreter is
will describe in the next section. This algebra is totally inde- provided. In order to minimize interpretation overheads,
pendant of Faust and could be reused in a completely differ- computations typically operate on block of samples in-
ent domain. This is why in section 2 we will first present it stead of individual samples. This comes with a cost : re-
without any reference to Faust and its signal processing se- cursive computations are nearly impossible to implement
mantic. and therefore many common signal processing operations
It is only in section 3 that we will relate the block-diagram can’t be implemented and must be provided as primitives
algebra to Faust and to the signal processing semantic. We or external plug-ins.
Yann Orlarey et al.
2

3. Graphs are complex to manipulate. For example it might


be desirable to algorithmically generate block-diagrams A : iA → oA B : iB → oB oA > iB
using templates and macros. But this is very difficult if
(A : B) : iA → oB + oA − iB
the block diagram is represented by a graph. Moreover,
it can be very useful to provide, in addition to the visual
syntax, a textual syntax that can be edited with a simple A : iA → oA B : iB → oB oA < iB
text editor and processed with standard tools. (A : B) : iA + iB − oA → oB

As we will see in the following paragraphs, these prob-


lems can be solved giving up the graph representation and
adopting an equivalent tree representation based on a small
algebra of composition operators [5].
This block-diagram algebra (BDA) is the heart of Faust.
But it is independent from it. Therefore in this section we will
focus on the topological semantic of the BDA: how things
are connected, but not what they do. We will assume the exis-
tence of a set of primitive building blocks, but without further Fig. 1 The sequential composition operator : possible cases accord-
details or any reference to the signal processing semantic of ing to the number of outputs of A and inputs of B
Faust.

2.1 Definitions
2.3 Parallel composition (A, B)
Let B be a set of primitive blocks (this set is leaved undefined
at this stage) and let D be the set of block-diagrams build on The parallel composition operator associates two block-dia-
top of B. A block-diagram D ∈ D is either an identity block grams one on top of the other, without connections. The in-
( ), a cut block (!), a primitive block B ∈ B, or composition puts (resp. the outputs) of (A, B) are the inputs (resp. the
of two block-diagrams based on one of the five composition outputs) of A and B as defined in the following rule :
operator of the algebra. More formally a block-diagram D ∈
D is defined recursively by the following syntactic rule : A : iA → oA B : iB → oB
(A, B) : iA + iB → oA + oB
D = |! | B | (D1 : D2 ) | (D1 , D2 )
| (D1 <: D2 ) | (D1 :> D2 ) | (D1 ∼ D2 )

We will adopt a type-like notation : D : iD → oD to


indicate that block-diagram D ∈ D has iD inputs and oD
outputs thus notated :
inputs(D) = {Din [0], Din [1], . . . , Din [iD − 1]}
outputs(D) = {Dout [0], Dout [1], . . . , Dout [oD − 1]} Fig. 2 The parallel composition operator

2.2 Sequential composition (A : B)

The sequential composition operator is used to connect the 2.4 Split composition (A <: B)
outputs of A to the corresponding inputs of B (such that
Aout [i] is connected to Bin [i]). The inputs of (A : B) are This split composition operator is used to distribute the out-
the inputs of A and the outputs of (A : B) are the outputs of puts of A to several inputs of B. It requires that the number
B. of inputs of B is an exact multiple of the number of outputs
If the number of inputs and outputs are not the same, of A. For example if A has 3 outputs and B has 6 inputs,
the exceeding outputs of A (resp. the exceeding inputs of B) then each output of A will be connected to 2 inputs of B. The
form additional outputs (resp. inputs) of the resulting block- general rule is that, if A : iA → m and B : oA ∗ k → q
diagram (see figure 1). The number of inputs and outputs of then Aout [i] is connected to Bin [i + j ∗ oA ] where j < k.
the resulting block-diagram is given by the following three The inputs (resp. the outputs) of (A <: B) are the inputs of
rules : A (resp. the outputs of B) as defined in the following rule :

A : iA → oA B : iB → oB oA = iB A : iA → oA B : oA ∗ k → oB
(A : B) : iA → oB (A <: B) : iA → oB
3 2.6 Recursive composition (A ∼ B)

Because we suppose that the number of inputs of B is an 2.6 Recursive composition (A ∼ B)


exact multiple of the number of outputs of A, the split compo-
sition is a partial function over D. It would have been easy to
The recursive composition is used to create cycles in the block-
extend the semantic of <: to cover all possible cases, but ex-
diagram in order to express recursive computations. Each in-
periments with Faust have proved that these restrictions are
put of B is connected to the corresponding output of A. Each
useful to discover potential programming errors. The same
output of B is connected to the corresponding input of A.
remark applies to the restrictions introduced in the :> and
∼ operators presented in the next paragraphs. Violations of The inputs of (A ∼ B) are the remaining inputs of A.
these restrictions are typically flagged by the compiler and The outputs of (A ∼ B) are the outputs of A. Two examples
reported as typing errors. of recursive composition are given in figure 5.

A : iA → oA B : iB → oB oB ≤ iA iB ≤ oA
(A ∼ B) : iA − oB → oA

Fig. 5 Two examples of recursive composition


Fig. 3 The split composition operator

Note that if k = 1, then A <: B is equivalent to A : B.


2.7 Identity block and Cut block !

2.5 Merge composition (A :> B)


For the algebra to be complete we need to introduce two addi-
As suggested by the notation, the merge composition opera- tional elements : the identity block (a simple connection wire)
tor does the inverse of the split operator. It is used to connect represented by the underscore symbol and the cut block (a
several outputs of A to the same inputs of B. It requires that connection ending) represented by the symbol !. These two
the number of outputs of A be an exact multiple of the num- elements are represented figure 6. We have :
ber of inputs of B. The general rule is that, if A : iA → iB ∗ k
and B : iB → oB then Aout [i + j ∗ iB ] is connected to Bin [i] :1→1
where j < k.
The inputs (resp. the outputs) of (A :> B) are the inputs and
of A (resp. the outputs of B). The number of outputs of A
should be an exact multiple of the number of inputs of B: !:1→0

A : iA → iB ∗ k B : iB → oB The identity block and cut block are typically used to cre-
(A :> B) : iA → oB ate complex routings. For example to cross two connections,
that is to connect Aout [0] to Bin [1] and Aout [1] to Bin [0] one
can write :

A : ( , <:!, , , !) : B

Fig. 4 The merge composition operator

Fig. 6 The identity and cut primitive boxes


Note that if k = 1, then (A :> B) is equivalent to (A : B)
.
Yann Orlarey et al.
4

2.8 Precedence and associativity 3 The Faust language

In order to simplify the expressions and to avoid too many


The Faust language is built on top of the BDA, extended with
parenthesis, we define a precedence and an associativity for
a suitable set of primitives and some additional syntactic con-
each operator as given in the following table :
structions allowing to define a Faust program as a list of def-
initions.
Priority Symbol Name Associativity This section will start with some few definitions related to
3 ˜ recursive Left the semantic of signal processor. Then the signal processing
2 , parallel Right semantic of the BDA will be presented. The section will end
1 :, <:, :> sequential, split, merge Right with a description of Faust primitives.
Based on these rules we can write :
a : b, c ∼ d, e : f 3.1 Notations and definitions
instead of
(a : (((b, (c ∼ d)), e) : f )) A Faust block-diagram denotes a signal processor transform-
Moreover, the following properties hold : ing input signals into output signals. In this paragraph we de-
fine the notions of signal, of signal processor and some nota-
((A : B) : C) = (A : (B : C)) tions.
((A, B), C) = (A, (B, C))
((A <: B) <: C) = (A <: (B <: C)) 3.1.1 Signals A signal s is a discrete function of time s :
N → R. The value of signal s at time t is written s(t). By con-
((A :> B) :> C) = (A :> (B :> C)) vention, the full range of the AD/DA converters corresponds
to samples values between −1.0 and +1.0. We denote by S
to be the set of all possible signals : S = N → R.
2.9 Stefanescu Algebra of Flownomials

Our block-diagram algebra is related to Gh. Stefanescu [6] 3.1.2 Constant Signals A signal is a constant signal if it
Algebra of Flownomials (AoF) proposed to represent directed always delivers the same value : ∃v ∈ R, ∀t, s(t) = v. We
flowgraphs (blocks diagrams in general including flowcharts) notate Sk ⊂ S the subset of constant signals.
and their behaviors.
The AoF is presented as an extension of Kleene’s calculus
3.1.3 Integer Signals A signal is an integer signal if it al-
of regular expressions. It is based on three operations and var-
ways delivers integer values : ∀t, s(t) ∈ Z. We notate Si ⊂ S
ious constants used to describe the branching structure of the
the subset of integer signals. Moreover we have Si = N → Z.
flowgraphs. They all have a direct translation into our BDA
as shown table 1.
Although the AoF and the BDA are equivalent in that 3.1.4 Constant Integer Signals A signal is an constant in-
they can both represent any block-diagram, the AoF lacks the teger signal if it always delivers the same integer value :
high-level composition operations offered by the BDA and it ∃k ∈ Z, ∀t, s(t) = k. We notate Sik ⊂ S the subset of con-
is less suited for a practical programming language. stant integer signals. We have Sik = Si ∩ Sk .

AoF BDA 3.1.5 Tuples of Signals Signal processors operate on tuples


par. comp. A +B A, B of signals. We will write (x1 , . . . , xn ) : a n-tuple of signals el-
seq. comp A.B A:B ement of Sn . The empty tuple, single element of S0 is notated
feedback A↑ A ∼ :! ().
identity I
transposition X , <:!, , , !
ramification ∧nk ( , . . .)n <: ( , . . .)n∗k 3.1.6 Signal processors Faust primitives and block-diagrams
∧0 ! represent signal processors, functions transforming input sig-
identification ∨kn ( , . . .)n∗k :> ( , . . .)n nals to produce output signals. A signal processors p is a
function from n-tuples of signals to m-tuples of signals p :
Table 1 Correspondences between the algebra of Flownomials and
Sn → Sm . We notate P the set of all signal processors :
the block diagram algebra. Note : ( , . . .)n means the composition
of n identity in parallel. [
P= Sn → Sm
n,m
5 3.3 Faust Primitives

3.1.7 Semantic function In order to explicitly refer to the 3.2.3 Split composition In the split composition, the output
mathematical meaning of a block-diagram D and to distin- signals of D1 : n → m are duplicated k times and distributed
guish it from its syntactic representation we will used seman- to the inputs of D2 : m.k → p :
tic brackets : [[ ]]. The notation [[ D ]] means : the signal pro-
cessor represented by block-diagram D. Therefore [[ . ]] as a [[D1 ]](x1 , . . . xn ) = (s1 , . . . sm )
1 k
semantic function translating block-diagrams into signal pro- z }| { z }| {
cessors : [[ . ]] : D → P. [[D2 ]](s1 , . . . sm , . . . , s1 , . . . sm ) = (y1 , . . . yp )
[[D1 <: D2 ]](x1 , . . . xn ) = (y1 , . . . yp )

3.2 Semantic of the Block-Diagram Algebra 3.2.4 Merge composition In the merge composition, the out-
put signals of D1 : n → m.k are added together by groups of
k signals and sent to the corresponding input of D2 : m → p
In the previous section we have informally presented the topo- :
logical semantic of the Block-Diagram Algebra, how things
are connected, but without any references to the actual mean- [[D1 ]](x1 , . . . xn ) = (s1 , . . . sm.k )
Pk−1 Pk−1
ing of these connections. In this paragraph we will define the [[D2 ]]( j=0 (s1+j.m ), . . . j=0 (sm+j.m )) = (y1 , . . . , yp )
signal processing semantic of the various composition oper-
[[D1 :> D2 ]](x1 , . . . xn ) = (y1 , . . . yp )
ators.

3.2.5 Recursive composition In the recursive composition


3.2.1 Sequential composition The result of the sequential of D1 : v + n → u + m and D2 : u → v the first u output
composition of D1 : n → m and D2 : p → q is defined by signals of D1 are sent with a 1-sample delay to the corre-
different rules according to m and p : sponding inputs of D2 . The outputs of D2 are sent to the first
v inputs of D1 :
[[ D1 ]](x1 , . . . , xn ) = (s1 , . . . , sm )
[[ D2 ]](s1 , . . . , sp ) = (y1 , . . . , yq ) [[D1 ]](r1 , . . . , rv , x1 , . . . , xn ) = (y1 , . . . , ym )
(m = p) [[D2 ]](y1−1 , . . . , yu≤m
−1
) = (r1 , . . . , rv )
[[ D1 : D2 ]](x1 , . . . , xn ) = (y1 , . . . , yq ) [[(D1 ∼ D2 )]](x1 , . . . , xn ) = (y1 , . . . , ym )

For a signal x, the notation x−1 , represents the signal x de-


layed by one sample such that : ∀x ∈ S x−1 (0) = 0 and
[[ D1 ]](x1 , . . . , xn ) = (s1 , . . . , sm ) x−1 (t+1) = x(t). The resulting tuple of signals (y1 , . . . , ym )
[[ D2 ]](s1 , . . . , sm , z1 , . . . , zp−m ) = (y1 , . . . , yq ) is the least fixed point that satisfies the equation. This fixed
(m < p) point always exists as we limit ourselves to recursive compu-
[[ D1 : D2 ]](x1 , . . . , xn , z1 , . . . , zp−m ) = (y1 , . . . , yq ) tation depending only of past values.

3.2.6 Identity box and Cut box ! As shown in figure 6, the


[[ D1 ]](x1 , . . . , xn ) = (s1 , . . . , sp , sp+1 , . . . , sm ) identity primitive ( ) is essentially a simple wire representing
[[ D2 ]](s1 , . . . , sp ) = (y1 , . . . , yq ) the identity function for signals :
(m > p)
[[ ]] : S → S
[[ D1 : D2 ]](x1 , . . . , xn ) = (y1 , . . . , yq , sp+1 , . . . , sm )
[[ ]](s) = (s)
It is easy to deduce from the above rules that sequential com-
The cut box with one input and no output is used to end a
position is an associative operation : (D1 : D2 ) : D3 = D1 :
connection :
(D2 : D3 )
[[ ! ]] : S → S0
[[ ! ]](s) = ()
3.2.2 Parallel composition The result of the parallel com-
position of D1 : n → m and D2 : o → p is defined by
: 3.3 Faust Primitives

[[D1 ]](x1 , . . . , xn ) = (y1 , . . . , ym ) The set B of Faust primitives follows as much as possible
[[D2 ]](s1 , . . . , so ) = (t1 , . . . , tp ) the set of C/C++ operators. In order to guarantee the role of
[[D1 , D2 ]](x1 , . . . , xn , s1 , . . . , so ) = (y1 , . . . , ym , t1 , . . . , tp ) signal processing specification language of Faust, typical sig-
nal processing operations are not part of the primitives. They
The associativity holds also for the parallel composition : are typically implemented in Faust and provided as external
(D1 , D2 ), D3 = D1 , (D2 , D3 ) libraries.
Yann Orlarey et al.
6

3.3.1 Arithmetic primitives Faust arithmetic primitives cor-


respond to the five C/C++ operators + − × / % represented
Fig. 9 The int cast and float cast primitive boxes
figure 7. The semantics scheme of each of these primitives is
the same. For an operation ? ∈ {+, −, ×, /, %} we have :

[[ ? ]] : S2 → S 3.3.6 Foreign definitions Foreign definitions are used to in-


[[ ? ]](s1 , s2 ) = (y) corporate externally defined C functions and constants. For-
y(t) = s1 (t) ? s2 (t) eign functions are declared using the reserved keyword ffunc-
tion, specifying the C prototype, the include file, and the
library to link against.

ffunction( prototype , include , library )


For example the sin function is declared :
ffunction( float sin(float), <math.h>, "-lm")
Fig. 7 The arithmetic primitives
Foreign constants are declared using the reserved word fcons-
tant :
fconstant(int fSamplingFreq, <math.h>)

3.3.2 Comparison primitives The six comparison primitives


are also available : <, >, <=, >=, ! =, ==. They compare 3.3.7 Fixed delays Fixed delays are provided with two prim-
two signals and produce a boolean signal. For a comparison itives @ and mem. More sophisticated delays are implemented
./∈ {<, >, <=, >=, ! =, ==} we have : using the read-write tables. While @ represent a fixed delay :

[[ @ ]] : S × Sik → S
[[ ./ ]] : S2 → S
[[ @ ]](x, d) = (y)
[[ ./ ]](s1 , s2 ) = (y)
y(t + d(t)) = x(t)
1 if s1 (t) ./ s2 (t)
y(t) =
0 else The mem represent a 1-sample delay :

[[ mem ]] : S → S
3.3.3 Bitwise primitives Bitwise primitives corresponding [[ mem ]](x) = (y)
to the five C/C++ operators <<, >>, &, |, ∧ are also pro- y(t + 1) = x(t)
vided. Again the semantics scheme of each of these primi-
tives is the same. For an operation ? ∈ {<<, >>, &, |, ∧} We have ∀s, [[ mem ]](x) = [[ @ ]](x, 1).
we have :
[[ ? ]] : S2 → S
[[ ? ]](s1 , s2 ) = (y)
y(t) = s1 (t) ? s2 (t)
Fig. 10 The mem box represents a one sample delay

3.3.4 Constants Constants are represented by boxes with


no input and a constant output signal (see figure 8). For a
number k ∈ R we have
3.3.8 Read-only table The read-only table rdtable is a
0
[[ k ]] : S → S primitive box with 3 inputs : a constant size signal, an initial-
[[ k ]]() = (y) ization signal and an index signal. It produce an output signal
y(t) = k by reading the content of the table.

[[ rdtable ]] : Sik × S × Si → S
[[ rdtable ]](n, v, i) = (y)
y(t) = v(i(t))

The size of the table is determined by the constant signal n.


Fig. 8 The constant 10 The index signal i is such that ∀t, 0 ≤ i(t)<n.

3.3.9 Read-write table The read-write table rwtable is


almost the same as the rdtable box, except that the data
3.3.5 Casting Two primitives : float and int are pro- stored at initialization time can be modified. It has 2 more
vided to cast signals to floats or integers. inputs streams : the write index and the write signal.
7 3.3 Faust Primitives

The signal delivered by the button reflects the user actions:


[[ button(”label”) ]] : S0 → S
[[ button(”label”) ]]() = (y)
1 when the button is pressed
y(t) =
0 otherwise

Fig. 11 The read-only table primitive This box is a monostable trigger. It has no input, and one
output that is set to 1 if the user click the button, and else to
0.

Fig. 14 The button primitive box


Fig. 12 The read-write table primitive

The checkbox is a bistable trigger. A mouse click sets


the output to 1. A second mouse click sets the output back to
3.3.10 Selectors The primitives select2 and select3
0.
allow to dynamically select between 2 or 3 signals accord-
ing to a selector signal. The select2 box receives 3 input
streams, the selection signal, and the two signals.

[[ select2 ]] : Si × S2 → S
[[ select2 ]](i, s[0], s[1]) = (y)
y(t) = s[i(t)](t) Fig. 15 The checkbox primitive box

The index signal i is such that ∀t, i(t) ∈ {O, 1}. The select3
box is exactly the same except that it selects between 3 sig- Here’s the syntax :
nals :
checkbox("label")
[[ select3 ]] : Si × S3 → S
The slider boxes hslider (horizontal) and vslider
[[ select3 ]](i, s[0], s[1], s[2]) = (y)
(vertical) provide some powerful controls for the parameters.
y(t) = s[i(t)](t)
Here’s the syntax :

hslider("label", start, min, max, step)

This produces a slider, horizontal or vertical, that let the


user pick a value between min and max−step. The initial
value is start. When the user moves the slider, the value changes
by steps of the value of step. All the parameters can be floats
or ints.
Fig. 13 The select2 primitive box
The associated box has no input, and one output which is
the value that the slider displays.

3.3.11 Graphic user interface A Faust block-diagram can


contain user interface elements (buttons, sliders, etc.) grouped
together according to different layout strategies. Like every
thing in Faust, user interface elements deliver signals. It is
therefore possible to mix user interface elements with other Fig. 16 The slider primitive box
signal processing operations.
The button primitive has the following syntax:
This primitive displays a numeric entry field on the GUI.
button("label") The output of this box is the value displayed in the field.
Yann Orlarey et al.
8

nentry("label", start, min, max, step)

Fig. 17 The nentry primitive box

Fig. 19 The noise generator


The layout of the user interface is controlled using group
expressions. For example
hgroup("label", D) 4.2 The trigger
defines an horizontal layout for all the user interface elements
The trigger is used to deliver a one shot control signal every
that appears in D. Similarly vgroup("label", D) defines
time the user press on the play button. The control signal must
a vertical layout and tgroup("label", D) a tabular orga-
have a precise width that is independent of how long the user
nization.
press on the button.
impulse(x) = (x - mem(x)) > 0;
4 Example : the Karplus-Strong Algorithm release(n) = +∼( <: -(>(0)/n)) : >(0);
trigger(n) = impulse : release(n);
Karplus-Strong is a well known algorithm first presented by
Karplus and Strong in 1983 [3]. It can generate interesting Impulse (figure 20) transforms the play button signal into
metallic plucked-string and drum sounds. While non com- a one sample impulse. The a release is added to transform
pletely trivial, the principle of the algorithm is simple enough this impulse into a n-samples signal.
to be described in few lines of Faust.
An overview of the implementation is given figure 18.
It uses an impulse of noise that goes into a resonator based
on a delay line with feedback. The user interface contains a
button to trig the sound and allows to control the size of both
the resonator and the noise impulse, as well as the amount of
feedback.
Fig. 20 impulse in charge of transforming a button signal into a
one sample impulse

4.3 The resonator

Fig. 18 The Faust implementation The resonator uses a delay line implemented using a rwtable
(see figure 21). It averages two consecutive samples with de-
lay d and d − 1 and feeds back the result with an attenuation
a into the table.
index(n) = &(n-1)∼+(1);
4.1 The noise generator delay(n,d) = rwtable(n, 0.0, index(n), ,
(index(n)-int(d)) & (n-1)) ;
The noise generator is based on a very simple random num- resonator(d,a) = (+ <: (delay(4096, d-1)
+ delay(4096, d))/2.0)∼*(1.0-a) ;
ber generator which values are scaled down between −1 and
+1. The following Faust definitions correspond to the block-
diagram of figure 19.
4.4 Putting all together
random = (*(1103515245)+12345) ∼ ;
noise = random *(1.0/2147483647.0); The description in now almost complete. We can put all the
pieces together with the user interface description and define
9 5.1 The compilation process

into the block diagram in order to discover how each out-


put signal can be expressed as a function of the input sig-
nals. These resulting signal expressions are then simplified
and common subexpressions are factorized. Finally they are
translated into C/C++ code resulting in a five methods class
that implements the specification.

5.1 The compilation process

The compilation process involves several phases that we de-


scribe briefly in the following paragraphs.

5.1.1 Parsing the source files As mentioned in the previ-


ous example, a faust program is an unordered list of defini-
tions that includes a definition of the keyword process, the
Faust equivalent to C main(). The first step is to parse all
the input files in order to produce a internal dictionary of defi-
nitions. Each definition is represented as an abstract syntactic
tree (AST). To simplify the discovery of common subtree, the
Fig. 21 The delay line AST are implemented using hash-consing such that syntacti-
cally equal trees are always shared in memory.

process that will produce the standalone application of fig-


5.1.2 Evaluation of process The next step is to evaluate
ure 22. the definition of process stored in the dictionary. This step
is basically a λ-calculus interpreter with a strict evaluation
dur = hslider("duration", 128, 2, 512, 1);
att = hslider("attenuation", 0.1, 0, 1, 0.01);
strategy. Names are replaced with their definition found in
the dictionary and applications of abstractions are β-reduced.
process = noise : *(button("play") The result is ”flat” block-block-diagram where everything have
: trigger(dur)) : resonator(dur,att); been expanded.

5.1.3 Type annotation of block-diagrams Using the rules


described in section 2, every subtree D of the process tree
is annotated with its number of inputs and outputs : n → m.

5.1.4 Symbolic propagation In order to discover what the


block-diagram computes, symbolic signals are propagated through
it using the semantic rules described in section 3. This prop-
Fig. 22 The Karplus-Strong user interface automatically generated agation results in a list of signal expressions expressing how
from the Faust specification each output signals is computed from the input signals.

5.1.5 Type annotation of signals The resulting signal ex-


pressions are type annotated according to several aspects :

5 The Faust Compiler 1. the nature of the signal : integer of float.


2. the computation time of the signal: the signal can be com-
The role of the Faust compiler is to translate a signal pro- puted at compilation time, at initialization time or at exe-
cessor specification written in Faust into C/C++ code. Be- cution time.
cause we target high-performance real-time signal process- 3. the speed of the signal : constant signals computed only
ing applications, the main challenge is to generate efficient once, low speed user interface signals computed once for
code that can compete with hand-written one. The key idea every block of samples, high speed audio signals. com-
is not to compile the block diagram itself, but what it com- puted every samples.
putes. Driven by the semantic rules described in the previous 4. parallelism of the signal : true is the samples of the signal
sections, the compiler starts by propagating symbolic signals can be computed in parallel, false otherwise.
References 10

5.1.6 Simplification and normalization The signal expres- Because of it simple and well defined semantic, the lan-
sions are rearranged and simplified by executing all the com- guage can be easily compiled into efficient C++ code. Pre-
putations that can be done at compilation time producing sig- liminary performance tests on the generated code are encour-
nal expressions in normal form. aging. In some cases the Faust code significantly outperforms
hand-written code.
5.1.7 Sharing of recursive signals The previous steps may The compiler can be improved in several ways, in par-
have produced different, but α-equivalent, representations of ticular by extending its capacities of symbolic simplification
recursive signals. Because they are syntactically different they and normalization and by improving the generation of SIMD
are not automatically shared by the hash-consing technique. code.
This step replaces all α-equivalent subtrees with a common The semantic of the language can also be enlarged. Right
shared subtree. now Faust only deals with scalar signals. The next step is
to add arbitrary data types in particular vectors and matrices
5.1.8 Reuse annotation The subtrees are annotated with a which are needed for image and video processing as well as
reuse flag indicating if the computation should be stored in spectral based transformations.
a temporary variable to be later reused. The notion of reuse
can be quite subtle. It concerns expressions that have several
occurrences in the global expression (space reuse), but also References
expressions with only one occurrence but in a higher speed
context (time reuse). 1. J. B. Dennis and D. P. Misunas. A computer architecture for
highly parallel signal processing. In Proceedings of the ACM
1974 National Conference, pages 402–409. ACM, November
5.1.9 Code generation The compiler generates a C++ class 1974.
that implements the Faust specification. This class may be 2. G. Kahn. The semantics of a simple language for parallel pro-
wrapped into an architecture code that implements a specific gramming. In Proceedings of the IFIP Congress 74. North-
type of application or plugin format. The compiler can op- Holland, 1974.
tionally generate SIMD code using either ALTIVEC or SSE2 3. K. Karplus and A. Strong. Digital synthesis of plucked-string
intrinsics. and drum timbres. Computer Music Journal, 7(2):43–55, 1983.
4. E. A. Lee and T. M. Parks. Dataflow process networks. In Pro-
ceedings of the IEEE, volume 83, pages 773–801, May 1995.
5. Y. Orlarey, D. Fober, and S. Letz. An algebra for block diagram
5.2 Implementations and performances
languages. In ICMA, editor, Proceedings of International Com-
puter Music Conference, pages 542–547, 2002.
An implementation of the Faust compiler, written in C++, is 6. Gheorghe Stefanescu. The algebra of flownomials part 1: Binary
available at sourceforge (http://faudiostream.sourceforge.net). flownomials; basic theory. Report, Technical University Munich,
The code is quite portable and only depends on Lex and Yacc November 1994.
for the parser code. 7. Robert Stephens. A survey of stream processing. Acta Informat-
The performance of the code generate by the compiler is ica, 34(7):491–541, 1997.
quite good. In order to compare it with hand written code we
have reimplemented in Faust two audio effects:Freeverb, a
well known reverb written in C++, and Tapiir a multitap delay
also written in C++. Both applications are freely available on
Internet with the source code.
In both case, the Faust specification is far more compact
than the C++ code. The speed of both versions of the Free-
verb are equivalents (but the speed of the Faust version using
SIMD code generation is about twice faster). The Faust ver-
sion of Tapiir is twice faster than the original version even in
scalar mode.

6 Conclusion

We have presented both some syntactical and semantical as-


pects of Faust. The syntax of Faust is quite surprising at first
but it turns out to be very convenient. Once you get used to it,
it is both expressive and readable. The combination of func-
tional programming and block-diagram composition is also
really pleasant and natural to use.

You might also like