Being a high-level specification language, the Faust code says nothing about the audio drivers, the GUI toolkit, or any other non-GUI interface to be used to control the dsp. It is the role of the architecture file to describe how to relate the dsp code to the external world. This approach allows a single Faust program to be easily deployed to a large variety of audio standards (Max/MSP externals, PD externals, VST plugins, CoreAudio applications, JACK applications, etc.).

The dsp class

The Faust compiler typically generates a self contained C++ sub class of the followig dsp class :

class dsp {

 protected:
	int fSamplingFreq;
    
 public:
	dsp() {}
	virtual ~dsp() {}

	virtual int getNumInputs() = 0;
	virtual int getNumOutputs() = 0;
	virtual void buildUserInterface(UI* ui_interface) = 0;
	virtual void init(int samplingRate) = 0;
 	virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) = 0;
};

The dsp object is central to the Faust architecture design:

  • getNumInputs, getNumOutputs provides information about the signal processor,
  • buildUserInterface creates the user interface using a given UI class object (see later),
  • init is called to initialize the dsp object with a given sampling rate, typically obtained from the audio architecture,
  • compute is called by the audio architecture to execute the actual audio processing. It takes as a count number of samples to process, inputs and outputs arrays of non-interleaved float/double samples, to be allocated and handled by the audio driver with the required dsp input and ouputs channels (as given by getNumInputs, getNumOutputs).

(note that FAUSTFLOAT label is typically defined to be the actual type of sample : either float or double using #define FAUSTFLOAT float in the code for instance).

We can look at what the Faust compiler generates for a given DSP source file. For the following Faust DSP example:

import("music.lib");

smooth(c)	= *(1-c) : +~*(c);
gain		= vslider("[1]", 0, -70, +4, 0.1) : db2linear : smooth(0.999);
process		= *(gain);

Here is the C++ code the Faust compiler will produce:

//----------------------------------------------------------
// name: "volume"
// version: "1.0"
// author: "Grame"
// license: "BSD"
// copyright: "(c)GRAME 2006"
//
// Code generated with Faust 0.9.73 (https://faust.grame.fr)
//----------------------------------------------------------

/* link with  */
#ifndef FAUSTFLOAT
#define FAUSTFLOAT float
#endif  

#ifndef FAUSTCLASS 
#define FAUSTCLASS mydsp
#endif

class mydsp : public dsp {
  private:
	FAUSTFLOAT fslider0;
	float fRec0[2];
  public:
	static void metadata(Meta* m) 	{ 
		m->declare("name", "volume");
		m->declare("version", "1.0");
		m->declare("author", "Grame");
		m->declare("license", "BSD");
		m->declare("copyright", "(c)GRAME 2006");
		m->declare("music.lib/name", "Music Library");
		m->declare("music.lib/author", "GRAME");
		m->declare("music.lib/copyright", "GRAME");
		m->declare("music.lib/version", "1.0");
		m->declare("music.lib/license", "LGPL with exception");
		m->declare("math.lib/name", "Math Library");
		m->declare("math.lib/author", "GRAME");
		m->declare("math.lib/copyright", "GRAME");
		m->declare("math.lib/version", "1.0");
		m->declare("math.lib/license", "LGPL with exception");
	}

	virtual int getNumInputs() 	{ return 1; }
	virtual int getNumOutputs() 	{ return 1; }
	static void classInit(int samplingFreq) {
	}
	virtual void instanceInit(int samplingFreq) {
		fSamplingFreq = samplingFreq;
		fslider0 = 0.0f;
		for (int i=0; i<2; i++) fRec0[i] = 0;
	}
	virtual void init(int samplingFreq) {
		classInit(samplingFreq);
		instanceInit(samplingFreq);
	}
	virtual void buildUserInterface(UI* interface) {
		interface->openVerticalBox("0x00");
		interface->declare(&fslider0, "1", "");
		interface->addVerticalSlider("0x00", &fslider0, 0.0f, -7e+01f, 4.0f, 0.1f);
		interface->closeBox();
	}
	virtual void compute (int count, FAUSTFLOAT** input, FAUSTFLOAT** output) {
		float fSlow0 = (0.001f * powf(10,(0.05f * float(fslider0))));
		FAUSTFLOAT* input0 = input[0];
		FAUSTFLOAT* output0 = output[0];
		for (int i=0; i<count; i++) {
			fRec0[0] = ((0.999f * fRec0[1]) + fSlow0);
			output0[i] = (FAUSTFLOAT)((float)input0[i] * fRec0[0]);
			// post processing
			fRec0[1] = fRec0[0];
		}
	}
};

Note that by default mydsp is used as the name of the created class. You may need to use the -cn name Faust compiler parameter to possibly generate another class name, especially if you need to compile several Faust generated C++ classes in a same context and avoid name clashes.

The audio class

Faust audio architecture is a glue between the host audio system and a Faust module. It is responsible to allocate and release audio channels and to call the Faust dsp::compute method to handle incoming audio buffers and/or to produce audio output buffers. It is also responsible to present the audio as non-interleaved float/double data, normalized between -1 and 1.

A Faust audio architecture derives the following audio class:

typedef void (* shutdown_callback)(const char* message, void* arg);

class audio {

 public:
    audio() {}
    virtual ~audio() {}

    virtual bool init(const char* name, dsp*)               = 0;
    virtual bool start()                                    = 0;
    virtual void stop()                                     = 0;
    virtual void shutdown(shutdown_callback cb, void* arg)  {}

    virtual int get_buffer_size() = 0;
    virtual int get_sample_rate() = 0;

    virtual int get_num_inputs() { return -1; }
    virtual int get_num_outputs() { return -1; }
    virtual float get_cpu_load() { return 0.f; }
};

The API is simple enough to give a great flexibility to audio architectures implementations. The init method should initialize the audio underlying driver. The start method begins the actual audio processing, until stop method is called. A concrete implementation of audio class will typically get buffer size and sample rate driver parameters in its contructor. So get_buffer_size and get_sample_rate methods can be used to retrieve those values, or the one actually choosen by the driver.

The UI class

A Faust UI architecture is a glue between a host control layer and a Faust module. It is responsible to associate a Faust control parameter to a user interface element and to update the parameter value according to the user actions. This association is triggered by the dsp::buildUserInterface call, where the dsp asks a UI object to build the module controllers.

Since the interface is basically graphic oriented, the main concepts are widget based: an UI architecture is semantically oriented to handle active widgets, passive widgets and widgets layout.

A Faust UI architecture derives the following UI class:

class UI
{

 public:
    UI() {}
    virtual ~UI() {}

    // -- widget's layouts
    virtual void openTabBox(const char* label) = 0;
    virtual void openHorizontalBox(const char* label) = 0;
    virtual void openVerticalBox(const char* label) = 0;
    virtual void closeBox() = 0;

    // -- active widgets
    virtual void addButton(const char* label, FAUSTFLOAT* zone) = 0;
    virtual void addCheckButton(const char* label, FAUSTFLOAT* zone) = 0;
    virtual void addVerticalSlider(const char* label, FAUSTFLOAT* zone, 
        FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step) = 0;
    virtual void addHorizontalSlider(const char* label, FAUSTFLOAT* zone, 
        FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step) = 0;
    virtual void addNumEntry(const char* label, FAUSTFLOAT* zone, 
        FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step) = 0;

    // -- passive widgets
    virtual void addHorizontalBargraph(const char* label, FAUSTFLOAT* zone, 
        FAUSTFLOAT min, FAUSTFLOAT max) = 0;
    virtual void addVerticalBargraph(const char* label, FAUSTFLOAT* zone, 
        FAUSTFLOAT min, FAUSTFLOAT max) = 0;

    // -- metadata declarations
    virtual void declare(FAUSTFLOAT*, const char*, const char*) {}
};

Controlling the dsp

The FAUSTFLOAT* zone element is the primary connection point between the control interface and the dsp code. The compiled dsp Faust code will give access to all internal control value addresses used by the dsp code by calling the approriate addButton, addVerticalSlider, addNumEntry etc. methods (depending of what is described in the original Faust DSP source code).

The control/UI code keeps those addresses, and will typically change their pointed values each time a control value in the dsp code has to be changed. On the dsp side, all control values are “sampled” once at the beginning of the dsp::compute method, so that to keep the same value during the entire audio buffer. Since writing/reading the FAUSTFLOAT* zone element is atomic, there is no need of complex synchronization mechanism between the writer (controller) and the reader (the Faust dsp object). Look as how the fslider0 field of the previously mydsp displayed C++ class is used in the mydsp::compute method, and how its value is read and kept in a local variable, before entering the actual sample computation loop.

Active widgets

Active widgets are graphical elements that control a parameter value. They are initialized with the widget name and a pointer to the linked value. The widget currently considered are Button, CheckButton, VerticalSlider, HorizontalSlider and NumEntry. A GUI architecture must implement a method addxxx(const char* name, FAUSTFLOAT* zone, …) for each active widget. Additional parameters are available to Slider and NumEntry: the init value, the min and max values and the step.

Passive widgets

Passive widgets are graphical elements that reflect values. Similarly to active widgets, they are initialized with the widget name and a pointer to the linked value. The widget currently considered are HorizontalBarGraph and VerticalBarGraph. A UI architecture must implement a method addxxx(const char* name, FAUSTFLOAT* zone, …) for each passive widget. Additional parameters are available, depending on the passive widget type.

Widgets layout

Generally, a GUI is hierarchically organized into boxes and/or tab boxes. A UI architecture must support the following methods to setup this hierarchy:

  • openTabBox(const char* label)
  • openHorizontalBox(const char* label)
  • openVerticalBox(const char* label)
  • closeBox(const char* label)

Note that all the widgets are added to the current box.

Metadata

The FAUST language allows widget labels to contain metadata enclosed in square brackets. These metadata are handled at GUI level by a declare method taking as argument, a pointer to the widget associated value, the metadata key and value: declare(FAUSTFLOAT, const char, const char*). They are used to add specific informations to be decoded by specialized UI interfaces (like OSCUI, MidiUI…).

Non grapical interface controllers

Those UI elements have firstly been defined to have a “graphical meaning”, but you can perfectly decide to control the dsp with a pure non-graphical controller. OSC (see faust/gui/OSCUI.h file) and HTTP (see faust/gui/httpdUI.h file) controllers are good examples of that approach.

Developing your own architecture file

Developing your own architecture file typically means thinking on what part of your system is going to handle the dsp control state (by changing the value of each UI element), and what part is going to activate the actual dsp computation (by calling the dsp::compute method). Handling the dsp state can be done by a regular User Interface or a network based controler for instance. Dsp computation is usually trigerred by the real-time audio chain in a standalone application or plugin, but could be also be done in a pure deferred time context.

Implementing a subclass of audio base class is usually done when producing standalone applications. In the context of a plugin, subclassing a given base “audio node” class part of the host audio chain is usually sufficient. Then developing a subclass of UI base class could also be needed.

For audio you can look at the faust/audio/portaudio-dsp.h file that implements the portaudio class using the PortAudio API as as simple example. Other files in /faust/audio/ allows to use JACK, NetJack, CoreAudio, RTAudio, Alsa, OpenSL ES, etc API.

On the UI side, note that a lot of helper classes (like GUI, MapUI, PathUI, etc.) have already been developed, and may be helpful in your project:

  • PathUI class builds complete hierarchical path for UI items,
  • MapUI class creates a map of complete hierarchical path and zones for each UI items,
  • JSONUI class creates a JSON description of the dsp (including name, number of inputs/outputs and complete UI description),
  • GUI class provides additional mechanisms to synchronize widgets and zones. Widgets should both reflect the value of a zone and allow to change this value.
  • FUI class used to save and recall the state of the user interface. This class provides essentially two new methods saveState() and recallState() used to save on file and recall from file the state of the user interface. The file is human readable and editable.

Using your own architecture file

As soon as you architecture file is ready, you can test it using the following Faust command for instance:

  • faust -a your_architecture_file.cpp foo.dsp -o foo.cpp to create the new foo.cpp file to be compiled with a regular C++ compiler, and linked with the required libraries to produce the final executable or plugin.

Note that you can possibly use the -i parameter to textually include all Faust related dependancies in the output C++ file. You may want to automatize all build steps like invoking the Faust compiler, then the C++ compiler, and possibly additional steps. See scripts located in the tools folder as possible examples.