A patch built in a Playground does not have to stay there. Phausto can emit a CMajor patch, a complete JUCE plugin project, or C++ for a Bela board — from the same object you have been listening to.
1. Why export at all
Pharo is an excellent place to design a sound and a poor place to ship one. Exporting closes that gap: the instrument you prototyped interactively becomes a plugin your collaborators can load, or a binary on a board that boots without a computer attached.
Everything rests on the same fact — a Phausto patch is a FAUST program. The
exporters take the FAUST that generatedCode would give you and wrap it in
whatever scaffolding the target needs.
2. Plain FAUST files
The simplest export is the FAUST source itself, which is also the most portable — anything that accepts FAUST will take it.
| Message | Writes |
|---|---|
| createFileNamed: aString | The DSP as a FAUST file |
| createFaustSynthFileNamed: aName | A synth-shaped FAUST file |
| createFaustPolySynthFileNamed: aName | A polyphonic synth file |
| createFaustSynthHeaderFileNamed: aName | The accompanying header |
| createFaustHeaderFileNamed: aName for: aPluginType | A header for a given plugin type |
| generatedCode | The source as a string, without writing anything |
| writeAsPosixText: aString to: aFileReference | Write text with POSIX line endings |
synth := SawOsc new => ADSREnv new => MoogVcf new.
dsp := synth stereo asDsp.
dsp init.
dsp generatedCode. "inspect it first"
dsp createFaustSynthFileNamed: 'MySynth'.
3. CMajor patches
CMajor is a language and runtime for audio, with its own patch format that loads in a host or standalone player. Phausto emits complete patches, including the interface.
| Message | Produces |
|---|---|
| createCmajorPatchNamed: aName | An instrument patch |
| createCmajorFXPatchNamed: aName | An effect patch — audio in, audio out |
| createCmajorMIDIPatchNamed: aName | A MIDI-driven instrument patch |
dsp createCmajorMIDIPatchNamed: 'MyInstrument'.
3.1 The CMajor classes
The export is modelled by objects rather than string templating, so it can be extended:
| Class | Role |
|---|---|
| CmajorPatch | A patch |
| CmajorFXPatch | An effect patch |
| CmajorMIDIPatch | A MIDI instrument patch |
| CmajorView | The patch's interface |
| CmajorUIElement | Superclass of the view elements |
| CmajorKnob | A knob |
| CmajorFader | A fader |
| CmajorBackground | The view background |
Your UI primitives become CMajor endpoints and view
elements automatically — a PhHSlider in the patch becomes a control in the
exported interface, with the range you declared.
3.2 Polyphonic wrapping
A Phausto patch is monophonic by construction. To make a CMajor instrument polyphonic it has to be wrapped in a voice allocator, and Phausto generates that wrapper:
| Message | Produces |
|---|---|
| asCmajorPolyWrapperFor: aName | A polyphonic voice wrapper |
| aDictionary asCmajorPolyWrapper | The wrapper from an endpoint description |
| aDictionary asCmajorEventEndpoint | An event endpoint |
| aDictionary asCmajorEventsToVoicesConnection | Event-to-voice routing |
| aDictionary asCmajorListOfUIEndpoints | The UI endpoint list |
4. JUCE plugins
The most substantial exporter writes a complete JUCE project: the audio processor, its parameter tree, an editor, a CMake build and a script that runs the build with one command. The result compiles into a VST3, an Audio Unit (on macOS) and a standalone app.
| Message | Writes |
|---|---|
| export2JuceWithName: aString type: aPluginType | The project, with the generic editor |
| export2JuceWithName: aString type: aPluginType editor: anEditorExport | The project, with the editor you choose (§4.3) |
fx := (PhResonLp new frequency: #Cutoff; q: #Resonance; yourself) stereo asDsp.
fx init.
fx export2JuceWithName: 'ResonFilter' type: PhEffectPluginType new.
The name becomes the plugin's product name and its CMake target, so use letters and digits
only. The project lands in Documents/faust2Juce/ResonFilter/, and
cmake -P build.cmake in that folder builds it (§4.5).
architecture folder from the root of the Phausto repository into your
Documents folder. The exporter compiles your FAUST program against
Documents/architecture/faustMinimalInlined.h. Without it the export still
finishes, but leaves out the FAUST header, and the build stops at the missing file.
4.1 Plugin types
The type: argument is an object, not a string. It answers everything that
differs between an effect and an instrument (the buses, the MIDI handling, the flags the
host reads, which controls become parameters), so the exporter never tests which kind of
plugin it is writing. A misspelt type fails when you accept the code, not halfway through
an export.
| Type | Host sees | MIDI | Patch channels |
|---|---|---|---|
| PhEffectPluginType new | An effect: audio in, audio out | None | Up to 2 in, up to 2 out |
| PhMonoSynthPluginType new | An instrument: MIDI in, audio out | Notes drive freq, gate and gain | No inputs, up to 2 out |
The buses are stereo, and a host may also choose mono. A mono patch is fine either way:
the generated PhaustoChannelAdapter.h repeats the last channel when the bus
has more and averages when it has fewer, so a one-channel program feeds both sides.
An instrument is played through three controls named exactly freq,
gate and gain, the FAUST convention. A note-on sets
freq from the note number and gain from the velocity, then
opens gate; the matching note-off closes it. The last note wins, and MIDI is
read once per audio block. Because MIDI drives those three, they never become host
parameters; the patch's other controls (here the envelope's attack and release) do.
lead := ((SquareOsc new freq: #freq)
=> (AREnv new trigger: #gate; release: #Release; yourself))
* (PhHSlider new label: #gain).
dsp := lead stereo asDsp.
dsp init.
dsp export2JuceWithName: 'SquareLead' type: PhMonoSynthPluginType new.
PhPolySynthPluginType voices: 8, wraps the same single-voice
patch in FAUST's own voice allocator, with voice stealing, and every knob drives every
voice. It is not usable yet, so for now a JUCE instrument plays one note at a time. For a
polyphonic instrument today, export to CMajor with the poly wrapper (§3.2).
4.2 Parameters and the APVTS
JUCE's AudioProcessorValueTreeState is how a plugin exposes automatable
parameters to a host. Phausto builds it from the compiled program: every control in the
DSP's FAUST UI description becomes one row of a generated table,
PhaustoParams.h, holding its id, FAUST path, label, range, default and step.
The processor creates its parameters from that table, and the generic editor creates its
widgets from the same table, so parameters and controls cannot drift apart.
| In the patch | In the plugin |
|---|---|
| PhHSlider, PhVSlider, PhNumEntry | An AudioParameterFloat with the range, step and default you declared |
| PhButton, PhCheckbox | An AudioParameterBool |
| PhHBarGraph, PhVBarGraph | Nothing: a bar graph is an output, not a parameter |
| freq, gate, gain (instruments) | Nothing: MIDI drives them (§4.1) |
| A soundfile, as in samplers | The export stops |
A parameter's id is its control's short name, and the host displays its label. Once per audio block the processor writes each parameter's current value into the DSP through its FAUST path, so automation runs at block rate. The values are saved with the host session and restored when it reopens.
To see what the host will get without exporting anything:
| Message | Returns |
|---|---|
| apvtsControls | Every control of the compiled DSP, one dictionary each |
| apvtsParameterControls | The controls that become parameters in an effect |
| apvtsParameterControlsFrom: aJsonString for: aPluginType | The same for any type, from a UI description |
| asPhaustoParamsHeaderFrom: aJsonString for: aPluginType | The text of PhaustoParams.h |
dsp apvtsParameterControlsFrom: dsp getJSON for: PhMonoSynthPluginType new.
dsp asPhaustoParamsHeaderFrom: dsp getJSON for: PhMonoSynthPluginType new.
getJSON, see
DSP & Parameter API §7), not from Pharo objects. A patch
built from UGens, from raw FAUST or with the Box API exports
the same way, and the plugin exposes exactly what the program declares.
4.3 Editors
The editor: argument decides what the plugin window shows. It writes only
PluginEditor.h and PluginEditor.cpp: the processor and the rest
of the project are the same whichever editor you choose.
| Editor | Gives you |
|---|---|
| PhGenericEditorExport new | The default. One labelled control per parameter: a rotary slider, or a toggle for buttons and checkboxes. Built from PhaustoParams.h at run time, and rewritten on every export. |
| PhSkeletonEditorExport new | An empty editor for a UI you write in C++. Written once: exporting again never overwrites it. |
fx export2JuceWithName: 'ResonFilter'
type: PhEffectPluginType new
editor: PhSkeletonEditorExport new.
In the skeleton, parameters are reached through audioProcessor.apvts, and
their ids are listed in PhaustoParams.h. Bind a JUCE control to one with an
attachment, declared after the control so that it is destroyed first:
// PluginEditor.h, private members
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
juce::Slider cutoff;
std::unique_ptr<SliderAttachment> cutoffAttachment;
// PluginEditor.cpp, in the constructor
addAndMakeVisible (cutoff);
cutoffAttachment = std::make_unique<SliderAttachment> (
audioProcessor.apvts, "Cutoff", cutoff);
// PluginEditor.cpp, in resized()
cutoff.setBounds (getLocalBounds().reduced (20));
Keep the class name and constructor the skeleton gives you
(EffectAudioProcessorEditor or MonoSynthAudioProcessorEditor,
built from the processor): the generated createEditor() relies on both. To
generate an editor of your own from Pharo, subclass PhEditorExport and
answer headerFor: and implementationFor:; override
write:to: to change when the files are overwritten.
4.4 The generated project
Everything is written to Documents/faust2Juce/<Name>/:
| File | Holds (written by) |
|---|---|
| CMakeLists.txt | The JUCE build: formats, plugin codes, JUCE version (createCMakeFileNamed:for:) |
| build.cmake | The one-command build, §4.5 (createBuildScriptNamed:) |
| <Name>.jucer | The same project for the Projucer (createJucerFileNamed:for:) |
| Source/Faust<Type>.h | Your FAUST program, compiled to C++ (createFaustHeaderFileNamed:for:) |
| Source/PhaustoParams.h | The parameter table, §4.2 (createPhaustoParamsHeaderNamed:for:) |
| Source/PhaustoChannelAdapter.h | The mapping between program channels and buses, §4.1 (createChannelAdapterNamed:) |
| Source/PluginProcessor.h, .cpp | The audio processor: parameters, MIDI, saved state (createHeaderAPVTSFilesNamed:for:, createCppAPVTSFilesNamed:for:) |
| Source/PluginEditor.h, .cpp | The plugin window, §4.3 (the editor export) |
<Type> is Effect or MonoSynth, which is also
the prefix of the processor class (EffectAudioProcessor). Every file but the
FAUST header also has a query that answers its text without writing anything, such as
asCMakeListsFor:type: or asPluginProcessorCppAPVTSFor:: inspect
those to read the C++ before you export.
PhSkeletonEditorExport. Keep hand-written C++ in a skeleton editor, never in
the generated files.
The export name also decides how hosts tell the plugin apart:
| From 'ResonFilter' | Used as |
|---|---|
| ResonFilter | Product name and CMake target |
| Reso | Plugin code: the first four letters, one of them a capital (the AU rule) |
| Phausto, Phau | Manufacturer and its code, the same for every Phausto plugin |
| com.phausto.resonfilter | Bundle id |
ResonFilter and ResonDrive, give two plugins the host takes for
one. (PhPluginIdentity productName: 'ResonFilter') pluginCode shows the code
before you export.
4.5 Building
You need CMake 3.22 or newer and a C++ compiler: Xcode on macOS, Visual Studio on Windows, GCC or Clang on Linux together with the packages in JUCE's Linux dependencies. Then, in a terminal on any of the three:
cd ~/Documents/faust2Juce/ResonFilter
cmake -P build.cmake
| Command | Does |
|---|---|
| cmake -P build.cmake | Configures once, builds Release on every core, installs the plugins |
| cmake -DJUCE_PATH=/path/to/JUCE -P build.cmake | The same with your own JUCE instead of a download (a JUCE_PATH environment variable works too) |
JUCE copies the VST3 and the Audio Unit into the system plug-in folders, and the
standalone app stays in build/ResonFilter_artefacts/Release/Standalone/.
Rescan plug-ins in your host to find the new one. After a change in Pharo, export again and
rerun the script: only what changed is rebuilt.
JUCE_PATH, CMake fetches JUCE 8.0.4 with git, which takes a few
minutes once. The script configures only on its first run, so delete the
build folder before switching between the download and your own JUCE.
The script is a convenience, not a requirement. CMakeLists.txt is an
ordinary JUCE CMake project that any CMake-aware IDE opens, and the .jucer
is there if you prefer the Projucer.
5. Bela
Bela is an embedded platform for low-latency audio. Exporting to it produces a C++ file to build on the board:
| Message | Writes |
|---|---|
| createBelaCppFileNamed: aName | A Bela-ready C++ file |
Bela's analogue inputs are reached through widget metadata: declaring
belaPin: on a UI primitive binds that control
to a physical input. A potentiometer wired to the board then drives the parameter directly,
with no code in between.
"This slider becomes a hardware input on the board"
cutoff := PhHSlider new
label: 'Cutoff' values: #(800 40 12000 1);
belaPin: 0.
6. Writing a patch that travels
Some habits cost nothing while prototyping and save real trouble at export. All of them amount to the same principle: put information in the patch rather than in the Playground around it.
PhHSlider becomes a plugin parameter, a CMajor endpoint or a Bela pin.PhSec2Samp or PhSampleRate; see Math §6.midiCtrl: is honoured by the FAUST runtime and by the board. A JUCE plugin leaves controller mapping to the host, where you MIDI-learn the parameter instead.[ ... ] fork and dsp setValue:parameter: makes
sound in Pharo and silence everywhere else. Pharo is the sequencer in that arrangement, and
Pharo does not come with the plugin.
8. Troubleshooting
The exported plugin has no parameters
The program declares no control the host can see: its values are fixed inside the signal
graph, or it is an instrument whose only controls are freq, gate
and gain, which MIDI drives (§4.1). Expose what should be automatable as
widgets (§6). Give a JUCE plugin at least one parameter: one with none does not compile
with Visual Studio.
The plugin loads but is silent
If the sequencing lived in a forked Pharo loop, it did not come with you. Move the rhythm into the signal graph, or drive the plugin from the host (§6).
The instrument ignores the notes I play
MIDI reaches only controls named exactly freq, gate and
gain. List the names with dsp traceAllParams (see
DSP & Parameter API §4.2), relabel what differs, and export
again with PhMonoSynthPluginType (§4.1).
A controller bound with midiCtrl: does nothing in the plugin
A JUCE plugin does not read MIDI metadata. Map the controller to the parameter in your host, with its MIDI learn (§6).
Parameter ranges in the host look wrong
They are whatever you declared on the widget. Check the values: array order:
initial value first. See UI Primitives §2.1.
The exported instrument is monophonic
Phausto patches are single-voice by construction, and JUCE export plays one note at a time until the polyphonic type lands (§4.1). For polyphony today, use the CMajor poly wrapper (§3.2).
The export stops with an error
The message names the check that failed, and no file was written: a soundfile, more
channels than the plugin type takes, two controls with the same name, or a double quote or
backslash in a label (§4). A stereo unit fed from a mono chain can declare more inputs than
it seems to; compare dsp numberOfInputs and dsp numberOfOutputs
with the limits in §4.1.
The build cannot find FaustEffect.h or FaustMonoSynth.h
The FAUST program was never compiled because Documents/architecture is
missing. Copy the folder from the Phausto repository and export again (§4).
The generated project will not build
Export writes source, not binaries: the target's toolchain does the rest (§1). For JUCE,
check CMake 3.22 or newer, a C++ compiler, and either git with a network connection for the
JUCE download or a valid JUCE_PATH (§4.5).
On Windows, the build fails while installing the plugin
The system VST3 folder may not be writable without administrator rights. Run the terminal
as administrator, or take the plugin from
build/<Name>_artefacts/Release/VST3/ yourself (§4.5).
The new plugin does not appear in the host
Rescan plug-ins, or restart the host: Ableton Live may list a new plugin only after a
restart. On macOS, auval -a lists every Audio Unit the system can see.
Two exported plugins show up as one
Their names start with the same four letters, so they share a plugin code. Rename one and export again (§4.4).
My changes to PluginEditor.cpp disappeared
The generic editor rewrites its files on every export. Write your own UI with
PhSkeletonEditorExport, whose files are never overwritten (§4.3).
A new control is missing from my hand-written editor
That is by design: export never touches a skeleton editor. Add the control in C++, with the
id listed in PhaustoParams.h (§4.3).
The exported patch sounds different from the one in Pharo
Most often a sample-rate assumption. Anything specified in samples will shift; express times in seconds (§6).