What happens between SineOsc new and a sound leaving your speakers — the FFI bridge, the dynamic engine, and the design decisions that shape how Phausto behaves.

1. The pipeline

Six stages separate the object you write from the sound you hear. Most of Phausto's behaviour — including its surprises — follows from where the boundaries fall.

1 · Unit Generators — Pharo objects describing a signal. Pure Smalltalk, no native code involved.
2 · BoxesasBox converts each UGen into a FAUST box. The box lives in the FAUST compiler's memory, not in the Pharo heap.
3 · Box APIBoxAPI makes FFI calls into libfaust to assemble and combine boxes.
4 · Compilationdsp init asks the FAUST compiler to turn the box graph into native machine code, right there in the running image.
5 · Renderer — the compiled DSP is handed to a platform audio renderer with a buffer size.
6 · Audio threaddsp start begins rendering on a separate real-time thread, independent of Pharo's.
Compilation happens at runtime This is the unusual part. init invokes a real optimising compiler and produces machine code while your image is running — which is why init is not instant, why it must happen before start, and why a value passed as a literal number becomes a compile-time constant that can never be changed afterwards.

2. The FFI layer

2.1 The three libraries

Phausto binds to native code through Pharo's uFFI. Three FFILibrary subclasses define what it talks to:

ClassBinds to
LibFaustThe FAUST compiler itself
BoxAPIThe box API compiled into libfaust
PhaustoDynamicEngineThe dynamic engine: compile, initialise, render

Each declares its library name per platform — macLibraryName, win32LibraryName, unix64LibraryName — which is how one package supports macOS, Windows and Linux. The binaries themselves are the librariesBundle you placed next to your image; see Installation §3.

2.2 Opaque objects

DSP and PhBox are FFIOpaqueObject subclasses. They are handles on memory the FAUST engine owns — Pharo can pass them back and forth but cannot look inside them.

Consequences worth internalising Opaque objects are not garbage collected in the usual way: destroy is what frees them. They do not survive an image save and restart, because the pointers they hold are meaningless in a new process. And they cannot be inspected — the Inspector will show you a handle, not a signal graph. Use generatedCode instead.

3. PhaustoDynamicEngine

The engine is the bridge to the FAUST dynamic runtime — the object that actually compiles and runs your patch.

3.1 Creating a DSP

MessageCreates a DSP from
createDsp: aFaustCodeFAUST source
createDsp: aFaustCode withName: aNameFAUST source, named
createDsp: aFaustCode arguments: argvFAUST source with compiler arguments
createDsp: aFaustCode arguments: argv name: anAppNameThe same, named
createDspFromBoxes: aFaustBoxA box graph
createDspFromBoxes: aFaustBox withName: aStringA box graph, named
createDspMIDIFromBoxes: aFaustBox withName: aStringA box graph, MIDI-enabled

asDsp on a UGen ends up in createDspFromBoxes:; DSP create: ends up in createDsp:. The two routes converge immediately after.

3.2 Initialising and rendering

MessageEffect
initializeDSP: aDspInitialise with platform defaults
initializeDSP: aDsp withRenderer: aTypeInitialise with a chosen renderer
initializeDSP: aDsp withRenderer: aType bufferSize: aSizeRenderer and buffer size
destroyDSP: aDspRelease the DSP
getBuffer / getBuffer: aDspAccess the audio buffer
getNumInputsDsp: aDspInput channel count
PhaustoDynamicEngine bufferSizeThe configured buffer size
PhaustoDynamicEngine dspPlayingThe DSP currently playing

3.3 Errors

method getLastError
PhaustoDynamicEngine new getLastError.
The most useful debugging message in Phausto When a patch fails to build, the Pharo-side exception often says only that something was nil. getLastError returns the FAUST compiler's own diagnostic, which usually names the exact problem. Reach for it before anything else.

4. Renderers and buffer size

RendererType is an FFIEnumeration naming the available audio backends. Which one is used depends on the platform — CoreAudio on macOS, and the appropriate native backend elsewhere. DSP rendererType reports the current choice.

Buffer size is the latency-versus-stability trade, and is set on the engine rather than per patch. Typical values are 64, 256 and 512 samples.

BufferLatency at 48 kHzSuits
64~1.3 msLive playing, where responsiveness matters most
256~5.3 msGeneral use — a reasonable default
512~10.7 msHeavy patches, or an older machine
If audio crackles, raise it Clicks and dropouts almost always mean the audio thread is not finishing a buffer in time. A larger buffer gives it more room, at the cost of latency you will only notice when playing an instrument by hand.

5. Two threads

Audio runs on its own real-time thread; your code runs on Pharo's. Almost every timing question in Phausto comes back to this split.

Runs on the audio threadRuns on the Pharo thread
Everything in the signal graphsetValue:parameter: and friends
Pulse, counters, envelopesForked pattern loops
Sample-accurate, never driftsScheduled, and subject to whatever else the image is doing
They do not lock together A rhythm generated by Pulse inside the DSP and one generated by a forked loop are driven by different clocks and will drift apart. Pick one source of time per rhythmic layer — see Sequencing §1.

The upside is robustness: a long computation, a debugger, even a garbage collection pause in Pharo does not interrupt the audio. A patch keeps playing while you work on it, which is what makes live coding viable at all.

6. Instance tracking

Phausto tracks UGen instances as a DSP is assembled, so that repeated instances of the same class get distinct parameter names rather than colliding.

Class-side messageEffect
UnitGenerator activeInstancesThe instances currently tracked
UnitGenerator instancesInDSPContextInstances within the DSP being built
UnitGenerator resetInstancesInDSPContextReset the counter
UnitGenerator resetSelfAndAllSubClassesReset across the whole hierarchy
UnitGenerator removePlaygroundBindingsClear Playground variable bindings
UnitGenerator startUp:Behaviour at image start-up
Why your parameter names sometimes gain a suffix Two SineOsc instances in one DSP cannot both own SineOscFreq, so the counter distinguishes them. If the names you see in traceAllParams are not the ones you expected, this is usually why — and the fix is to give each UGen an explicit label:. See Unit Generators §2.2.

7. Memory and the image

A DSP holds memory outside the Pharo heap. The garbage collector cannot reclaim it, so the rules are manual and simple:

Every DSP you create should eventually be destroyed. stop silences; only destroy frees.
Rebuilding a patch creates a new DSP. The old one is still alive and still consuming CPU until destroyed.
Nothing survives an image restart. DSPs, boxes and the library context all point into a process that no longer exists — rebuild after reopening.
The registry is your safety net. DSP initializedDSPs finds everything still live, including DSPs you have lost the variable for.
"Silence everything, whatever it is"
DSP initializedDSPs do: [ :each | each stop ].

8. Where to go next

DocumentWhat it covers
Raw FAUST & the Box APIWorking directly at the box and source layers.
DSP & Parameter APIThe protocol these internals implement.
InstallationPlacing the native libraries this page depends on.
ExportingTaking the compiled result out of Pharo.

9. Troubleshooting

Audio crackles or drops out

The audio thread is missing its deadline. Raise the buffer size — §4.

A build failure with an unhelpful Pharo exception

PhaustoDynamicEngine new getLastError gives the compiler's own message — §3.3.

Everything broke after saving and reopening the image

DSPs and boxes hold pointers into a dead process. Rebuild your patches; nothing is recoverable — §2.2, §7.

The image gets slower the longer I work

Undestroyed DSPs are accumulating — §7.

Parameter names have unexpected numeric suffixes

Instance tracking is disambiguating duplicate UGens. Set explicit labels — §6.

The Inspector shows nothing useful for a DSP or a box

They are opaque FFI handles with no inspectable contents. Use generatedCode or asFaustCode — §2.2.

init is noticeably slow

It is invoking an optimising compiler. That is expected, it happens once per DSP, and it is the reason the resulting audio is native-speed — §1.