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.
asBox converts each UGen into a FAUST box. The box lives in the FAUST compiler's memory, not in the Pharo heap.BoxAPI makes FFI calls into libfaust to assemble and combine boxes.dsp init asks the FAUST compiler to turn the box graph into native machine code, right there in the running image.dsp start begins rendering on a separate real-time thread, independent of Pharo's.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:
| Class | Binds to |
|---|---|
| LibFaust | The FAUST compiler itself |
| BoxAPI | The box API compiled into libfaust |
| PhaustoDynamicEngine | The 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.
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
| Message | Creates a DSP from |
|---|---|
| createDsp: aFaustCode | FAUST source |
| createDsp: aFaustCode withName: aName | FAUST source, named |
| createDsp: aFaustCode arguments: argv | FAUST source with compiler arguments |
| createDsp: aFaustCode arguments: argv name: anAppName | The same, named |
| createDspFromBoxes: aFaustBox | A box graph |
| createDspFromBoxes: aFaustBox withName: aString | A box graph, named |
| createDspMIDIFromBoxes: aFaustBox withName: aString | A 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
| Message | Effect |
|---|---|
| initializeDSP: aDsp | Initialise with platform defaults |
| initializeDSP: aDsp withRenderer: aType | Initialise with a chosen renderer |
| initializeDSP: aDsp withRenderer: aType bufferSize: aSize | Renderer and buffer size |
| destroyDSP: aDsp | Release the DSP |
| getBuffer / getBuffer: aDsp | Access the audio buffer |
| getNumInputsDsp: aDsp | Input channel count |
| PhaustoDynamicEngine bufferSize | The configured buffer size |
| PhaustoDynamicEngine dspPlaying | The DSP currently playing |
3.3 Errors
PhaustoDynamicEngine new getLastError.
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.
| Buffer | Latency at 48 kHz | Suits |
|---|---|---|
| 64 | ~1.3 ms | Live playing, where responsiveness matters most |
| 256 | ~5.3 ms | General use — a reasonable default |
| 512 | ~10.7 ms | Heavy patches, or an older machine |
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 thread | Runs on the Pharo thread |
|---|---|
| Everything in the signal graph | setValue:parameter: and friends |
Pulse, counters, envelopes | Forked pattern loops |
| Sample-accurate, never drifts | Scheduled, and subject to whatever else the image is doing |
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 message | Effect |
|---|---|
| UnitGenerator activeInstances | The instances currently tracked |
| UnitGenerator instancesInDSPContext | Instances within the DSP being built |
| UnitGenerator resetInstancesInDSPContext | Reset the counter |
| UnitGenerator resetSelfAndAllSubClasses | Reset across the whole hierarchy |
| UnitGenerator removePlaygroundBindings | Clear Playground variable bindings |
| UnitGenerator startUp: | Behaviour at image start-up |
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:
stop silences; only destroy frees.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 ].
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.