A patch is UGens joined by operators. Phausto gives you a handful, each corresponding to one of FAUST's composition primitives — and once you know them, most patches read like a sentence.
1. Series: the chuck operator
=> connects two UGens in series: the output of the left becomes the input of
the right. The name and the symbol are borrowed, with affection, from
ChucK.
"An electric guitar model into a reverb"
synth := ElecGuitar new => GreyHole new.
dsp := synth asDsp.
dsp init.
dsp start.
dsp displayUI. "press ElecGuitarTrigger to pluck"
dsp stop.
The long form is chuckInto:, and the two are interchangeable. Use the operator
for chains, the keyword message when the expression is already dense with operators.
1.1 The receiver decides
=> does not always mean the same thing. What happens depends on the UGen on
the right — each class defines how it takes an input. This is what makes the operator
concise rather than ambiguous: an envelope has only one sensible way to combine with a
signal, and so does a filter.
| Expression | What it means |
|---|---|
| osc => env | The oscillator is multiplied by the envelope — amplitude shaping |
| osc => filter | The oscillator becomes the filter's input |
| synth => reverb | The synth signal feeds the reverb |
| pulse => PhImpulsify | A gate becomes a one-sample impulse |
| signal => saturator | The signal is waveshaped |
1.2 Precedence
Pharo binary operators all have equal precedence and associate left to right, so a chain needs no parentheses and reads in signal order:
synth := oscillator => envelope => filter.
"is exactly ((oscillator => envelope) => filter)"
a + b * c is (a + b) * c, not what
arithmetic convention suggests. When mixing + and * in a signal
expression, parenthesise deliberately.
2. Parallel: the comma operator
, places UGens side by side as separate channels rather than mixing them. A
four-oscillator parallel block has four outputs, not one:
oscillators := sine , tri , sqr , saw.
"four channels, ready for a selector or a multichannel destination"
a , b gives two channels; a + b gives one channel containing their
sum. Feeding a parallel block somewhere that expects a single input is the most common
connection error in Phausto.
Parallel composition is what selectors, multichannel effects and routing primitives consume. See Synthesis §1 for the waveform selector built this way.
3. Arithmetic
The arithmetic operators work sample by sample on signals, which makes them the workhorses of modulation.
| Operator | Effect | Typical use |
|---|---|---|
| + | Sum | Mixing signals; offsetting a modulator |
| - | Difference | Phase cancellation; bipolar offsets |
| * | Product | Amplitude modulation; scaling a control signal |
| / | Quotient | Normalising; ratio control |
| % | Modulo | Wrapping phase or index values |
"Mixing: two instruments into one signal"
mix := djembe + marimba.
"Amplitude modulation: a 5 Hz tremolo"
trem := SineOsc new * (LFOTri new freq: 5).
"Scaling a control signal into a useful range"
sweep := ADSREnv new * 2000 asBox.
Comparison operators are available too — <, <=,
>, >=, equalTo:, notEqualTo: —
each producing a signal that is 1 where the comparison holds and 0 elsewhere. They are how
you build gates and conditional logic inside the graph.
3.1 Mixing in plain numbers
FAUST arithmetic needs a signal on both sides. A number has to be promoted first, with
asBox:
"correct — the number becomes a constant signal"
freq := 100 asBox + pitchEnv.
"wrong — asks a SmallInteger to add a UGen to itself"
freq := 100 + pitchEnv.
SineOsc new freq: 440 needs no
asBox. You only need it when a bare number is one operand of an arithmetic
expression whose other operand is a signal.
Phausto adds a few other conversions to Number:
| Message | Result |
|---|---|
| asBox | A constant signal |
| asConst | A compile-time constant |
| midiNNToFreq | MIDI note number converted to hertz |
4. Recursion and feedback
~ is FAUST's recursive composition: it feeds a UGen's output back into its own
input, delayed by one sample. This is the primitive underneath every delay line, comb filter
and feedback network.
"The recursive operator — output fed back as input"
loop := someUGen ~ someOtherUGen.
5. Explicit routing
When the operators are not enough — usually because channel counts do not line up — there are explicit routing messages.
| Message | Effect |
|---|---|
| connectTo: | Sequential connection, matching outputs to inputs |
| chuckInto: | Keyword form of => |
| splitTo: | Fan one signal out to several inputs |
| mergeWith: | Fold several channels down into fewer |
| patchedWith: | Supply an input to a UGen that expects one |
| par:with: | Explicit parallel composition |
"The FAUST-style routing used by the four-way waveform selector"
selector := index , oscillators connectTo: PhSelect4 new.
Corresponding UGens exist for the same jobs — PhSplitOperation,
PhMergeOperation, PhCrossnn, PhBus — when you would
rather have an object in the graph than an operator between two.
6. Connecting collections
Phausto extends SequenceableCollection, so an ordinary Pharo array of UGens can
be collapsed into a single signal in one message. This is usually clearer than accumulating
in a loop:
| Message | Effect |
|---|---|
| asSumOfUGen | Mix every element into one signal |
| asChainOfUGen | Connect every element in series, left to right |
| asPhList | Build a PhList from the values |
| asPhZeroList | Build a zero-filled PhZeroList |
"Ten detuned oscillators, summed"
partials := (1 to: 10) collect: [ :i |
SineOsc new freq: 200 + (i * 14); uLevel: 0.05
].
voice := partials asSumOfUGen.
"A stack of effects, applied one after another"
chain := { CubicNl new. MoogVcf new. GreyHole new } asChainOfUGen.
7. Changing the channel count
Two messages handle almost every channel mismatch you will meet:
| Message | Effect |
|---|---|
| stereo | Duplicate one channel into two |
| mono | Collapse two channels into one |
| numberOfInputs | Ask how many channels a UGen consumes |
| numberOfOutputs | Ask how many it produces |
Order matters: stereo applied before an effect gives the effect two channels to
work with; applied after, it duplicates whatever the effect produced. For genuinely stereo
reverbs you want the former.
"true stereo processing"
dsp := (synth stereo => ZitaRevStereo new) asDsp.
"mono processing, widened afterwards"
dsp := (synth => FreeverbMono new) stereo asDsp.
8. Unary maths on a signal
Any signal understands the common mathematical messages, applied sample by sample. They are useful for shaping control signals — rectifying an LFO, converting units, folding a value into a range.
| Group | Messages |
|---|---|
| Sign and rounding | abs, absolute, floor, ceil, round, rInt |
| Powers and logs | sqrt, exp, log, log10 |
| Trigonometry | sin, cos, aSin, aCos, aTan |
| Ranges | min:, max:, fMod: |
| Audio units | db2linear, linear2db, midikey2hz |
| Smoothing | smoo — a one-pole smoother for jumpy control values |
"smoo takes the zipper noise out of a stepped control"
cutoff := (PhHSlider new label: 'Cutoff' values: #(800 40 12000 1)) smoo.
Each of these has an equivalent UGen class as well, catalogued in Math & Conversion Tools.
10. Troubleshooting
My expression raises an error about inputs and outputs not matching
A parallel block met something expecting one channel, or a stereo signal met a mono UGen.
Check numberOfOutputs on the left and numberOfInputs on the right —
§7.
Adding two UGens made them quieter, not louder
Two correlated signals of opposite phase partially cancel. If they are the same source,
a + a doubles amplitude and may clip; if they are detuned copies, expect
beating rather than a clean sum.
100 + someUGen raises doesNotUnderstand
Promote the number: 100 asBox + someUGen — §3.1.
a + b * c does not do what I expected
Pharo binary operators are all equal precedence, left to right. Write
a + (b * c) — §1.2.
The comma operator gave me four voices instead of a mix
That is what it does. Use + or asSumOfUGen to mix — §2.
A feedback patch got loud very fast
Loop gain is at or above 1. See the warning in §4.