FaustScript ยท a superset of Faust ยท MIT

Name one instance. Act on it while it plays.

In Faust, a name is a macro: it is replaced by its body at evaluation, so writing lpf twice builds two filters. Measured on Faust 2.70.3 โ€” one use gives 18 memory fields, two uses give 36.

Separate state is therefore free. What is missing is the address: no Faust expression can point at an instance that already exists. That is what stops you writing, an hour into a set, that filter, the one currently ringing โ€” open it up.

FaustScript gives that address, and nothing else. It computes nothing: a FaustScript program is translated into Faust, and Faust compiles it.

A : B
in series
decorated
A , B
stacked
unchanged
A <: B
split
now implicit
A :> B
merged
now implicit
A ~ B
fed back
decorated

Faust composes circuits with five signs. Of the five, two get decorated, one stays as it is, and two vanish from everyday writing โ€” because naming an instance makes splitting and merging automatic.

The principle

Decorate Faust's own signs; do not invent new ones. The : stays the connection, and what surrounds it qualifies it. Everything below follows from three rules, and there is nothing else to remember.

! in frontcancels what it precedes โ€” !: !~ !let ! lpf1
a digit aftersays how many โ€” :8 ~8 lpfs:8
the dotreaches into an instance โ€” lpf1.cutoff saw1.3

= gives a value, : connects. fc=800 gives a parameter its value, lpf1.fc = 400 sets a port, a : b connects, and on a wiring line :8 or : 8 connects as eight copies. A Faust definition runs up to its ;, as in Faust.

let lpf1 fi.lowpass(fc=800)   // place one instance, named
saw1 : lpf1 : process        // wire it to what sounds
    lpf1 fi.lowpass(fc=400)  // replace its body โ€” its memory stays
    lpf1.fc = 400            // or just turn the knob
   !let lpf1                 // give the name back, the tail runs out

: in series

What Faust requires

The outputs on the left must be exactly as many as the inputs on the right. A mismatch is not recovered from: it stops the compilation.

// verified by compiling
_ : _        โ†’ passes
_ : (_,_)    โ†’ ERROR : sequential composition A:B

What live performance demands

Connecting and disconnecting while the sound plays, and working at eight voices without rewriting both sides. Faust can do both โ€” but only by rewriting the whole circuit.

FAUST saw1 lpf1 saw1 : lpf1 FAUST โ€” EIGHT VOICES par(i,8,โ€ฆ) par(i,8,โ€ฆ) 56 signs to type FAUSTSCRIPT saw1 8 lpf1 saw1 :8 lpf1 12 signs
The same chain at eight voices: Faust duplicates both sides by hand, FaustScript writes the count on the sign that connects. The : stays the connection; the digit qualifies it.

How it is decorated

saw1 :8 lpf1     // connect as eight copies
saw1 !: lpf1     // cut the wire

And the bare : stops refusing โ€” it adapts, like a polyphonic cable in VCV Rack.

what arriveswhat FaustScript does
one channel into severalit spreads over all of them
several into an instance's inputthey sum โ€” nothing goes silent
several into a named portthe first is taken

That distinction is VCV Rack's, to the letter โ€” its voltage standards prescribe summing on an audio input and taking the first channel on a modulation input. And our notation already carries it: saw8 : lpf1 targets the instance's input, lfo8 : lpf1.cutoff targets a port.

It costs Faust nothing: since we translate, FaustScript emits the <:, the :> or the bus that fits the widths it sees. The compiler receives widths that add up and never knows an adaptation happened.

, stacked

What Faust requires

Nothing: A , B stacks two circuits that ignore each other. One's inputs follow the other's, and likewise for the outputs.

โš ๏ธ The comma is not the studio's "parallel", and Faust's documentation โ€” which calls it parallel composition โ€” invites the confusion. Measured: A , B has 2 inputs and 2 outputs, where the musician's parallel, A <: (X,Y) :> B, has one of each.

What live performance demands

Adding a voice while it plays, without touching what already sounds.

WHAT IS PLAYING voice1 rev1 WHAT YOU TYPE WHILE IT PLAYS voice2 voice2 : rev1 one line, nothing else to rewrite in Faust you would rewrite: process = (voice1 : rev1) , (voice2 : rev1);
A parallel branch needs no sign: live, a line sent on its own adds itself to the graph. The comma keeps its role inside an expression written in one go.

How it is decorated

Not at all โ€” and that is a result. What stacking is for, live, is already covered by a rule about placement: in a file, a line describes the circuit; sent on its own while the sound plays, it adds to it.

<: split

What Faust requires

The inputs on the right must be a multiple of the outputs on the left. Otherwise, refusal at compile time.

_ <: (_,_,_,_)      โ†’ passes   (1 into 4)
(_,_) <: (_,_,_)    โ†’ ERROR : split composition A<:B

What live performance demands

Sending one source to two places โ€” and above all adding a third destination an hour later without rebuilding the circuit.

FAUST โ€” THE SPLIT IS DRAWN saw1 lpf1 rev1 saw1 <: (lpf1, rev1) FAUSTSCRIPT โ€” IT COMES FOR FREE saw1 lpf1 rev1 saw1 : lpf1 โ† wired at the start saw1 : rev1 โ† added an hour later
Dashed is what you add an hour later. Because saw1 names an instance rather than a copy, wiring it a second time spreads it โ€” with the split never written.

How it is decorated

Not at all: the sign becomes unnecessary in everyday writing. Naming an instance turns the split into a consequence rather than a construction. <: remains perfectly valid โ€” it is Faust โ€” but you no longer write it to patch.

This is the first place where the definition, settled first, pays somewhere other than at home.

:> merged

What Faust requires

The outputs on the left must be a multiple of the inputs on the right; signals that meet are added.

(_,_,_,_) :> _      โ†’ passes   (4 into 1)
(_,_,_) :> (_,_)    โ†’ ERROR : merge composition A:>B

What live performance demands

The most ordinary gesture in a studio: sending several voices into the same effect, and adding one while the others are still ringing.

voice1 voice2 voice3 rev1 voice1 : rev1 voice2 : rev1 voice3 : rev1 โ† added while playing one reverb, three sends that add up measured: 97 memory fields โ€” once, not three times.
An effect send, written by naming its destination. The junction is written nowhere: it follows from rev1 designating a single circuit.

How it is decorated

Not at all, for the same reason as the split. Several wires arriving at the same input sum โ€” the rule of an effect send, and musically the right one: two voices sent into the same reverb resonate in the same space.

The gain is not only in writing. A reverb weighs 97 memory fields in the generated code; three copies would weigh 291, and their tails would never mix.

~ fed back

What Faust requires

The output returns to the input with a one-sample delay placed automatically โ€” that is what makes the loop computable. It is the construction that 645 of the library's functions depend on: every reverb, every compressor, 109 filters out of 114.

What live performance demands

Opening and closing the loop while it sounds. The most spectacular gesture of a set โ€” a delay starting to feed itself โ€” and the only one that can run away.

THE LOOP CLOSED dly1 zโปยน dly1 ~ fb1 the one-sample delay is placed by Faust THE LOOP OPENED, WHILE IT SOUNDS dly1 dly1 !~ fb1 the loop opens, what circulates drains out
The one-sample delay is what makes the loop computable โ€” Faust places it itself. Opening the loop live is the same ! as everywhere else.

How it is decorated

dly1 !~ fb1     // open the loop
dly1 ~8 fb1     // feed back eight channels

The same rule as everywhere: ! cancels, a digit says how many. Nothing new to learn once the : is understood.

And the same musical rule as !let: opening a loop does not cut the sound โ€” what is circulating drains out.

What the review gives

signwhat it doesin FaustScript
:in series:8 how many ยท !: cut ยท and it adapts instead of refusing
,stackedunchanged โ€” a line sent on its own already adds a branch
<:splitimplicit โ€” an instance wired twice spreads
:>mergedimplicit โ€” two wires into one input sum
~fed back!~ open ยท ~8 how many

The rule that runs through all of it

Faust's sign stays, and what surrounds it qualifies it. A ! in front cancels โ€” the connection, the loop, the binding. A digit after says how many. Two decorations, learned once, valid everywhere: that is what separates a rule from three clever finds.

Where it stands

The three pieces in the repository translate with nothing refused, and the Faust they produce compiles. 35 tests, a dozen of which invoke the real compiler. The catalogue declares the modules Faust's libraries declare, generated from their own documentation.

There is no sound here. FaustScript emits Faust source; compiling it while the audio runs, and swapping an instance without dropping a sample, belongs to the host โ€” that is where the measured 32 ms per instance matters, against 620 ms for a fifty-instance program โ€” both taken through libfaust compiled to WebAssembly, which is what a browser host runs.