Usage

Installation

Faust-ctypes can be installed from PIP

$ pip install faust-ctypes

or one can get the sources from GitLab

$ git clone https://gitlab.com/adud2/faust-ctypes

Compiling Faust DSP Code Into a DLL

Let’s start with a trivial Faust program tests/dsp/minimal.dsp

process = _;

To compile it to a DLL, we must use a special architecture file (see Faust Documentation on architecture files for more details) called dllarch.c. It is provided with the package. One must also ensure C and Faust compiler are installed. One can then use Faust compiler to generate C code :

$ faust -lang c -A <package dir> -a dllarch.c <dsp file>

This C code can then be compiled into a DLL by any C compiler.

For instance, on the git repository

$ faust -lang c -A faust_ctypes -a dllarch.c tests/dsp/minimal.dsp > tests/dsp/minimal.c
$ gcc -fPIC -shared tests/dsp/minimal.c -o tests/dsp/minimal.so

Or using the provided Makefile

$ make tests/dsp/minimal.so

Loading a DLL from Python

We can now load this file as a DLL using CTypes

>>> import ctypes
>>> dll = ctypes.CDLL("../tests/dsp/minimal.so")
>>> dll
<CDLL '../tests/dsp/minimal.so', ...>

Yet, a raw DLL is not very convenient to use. For instance, one has to handle function signatures by hand. One must also handle memory and initiate C objects. The wrapper is made to automate this repetitive work.

>>> from faust_ctypes.wrapper import Faust
>>> dll = ctypes.CDLL("../tests/dsp/minimal.so")
>>> dsp = Faust(dll)
>>> dsp
<faust_ctypes.wrapper.Faust object at ...>

or even simpler

>>> from faust_ctypes.wrapper import Faust
>>> dsp = Faust("../tests/dsp/minimal.so")
>>> dsp
<faust_ctypes.wrapper.Faust object at ...>

The initialization of the Faust object creates and bind three components together :

  • The Processor object, used to handle computations, accessible from the attribute proc

  • The UserInterface object, used to access the user interface elements declared in the Faust program accessible from the attribute ui

  • The Metadata objet, a storage for metadatas gathered from source and from computations, accessible from the attribute meta

Processing Data

The data processor of a DSP is stored in the proc attribute of the wrapper.

The highest-level way of processing signals through a DSP is the following

>>> inp = dsp.proc.gen_io(10)
>>> dsp.proc.compute(inp)
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], dtype=...)

We first create a signal of 10 samples of dimension 1 (minimal has one input), and then we pass it through the minimal processor using the compute method, which returns a signal of 10 samples of dimension 1 (minimal has one output).

One can also create an array from numpy-compatible objects using the from_obj method.

>>> inp = dsp.proc.from_obj(range(10))
>>> dsp.proc.compute(inp)
array([[0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]], dtype=...)

In both case, compute checks that inp is a DSP-compatible input, creates the output array, do the computations and then returns the output array.

One can also provide an output array if it has been created otherwise. compute will not create the array, but instead check that it is a DSP-compatible output.

To reprocess with different data, one can modify the input array and call directly the process method.

>>> inp = dsp.proc.from_obj(range(10))
>>> out = dsp.proc.compute(inp)
>>> out
array([[0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]], dtype=...)
>>> inp[0][1] = 0
>>> dsp.proc.process(10)
>>> out
array([[0., 0., 2., 3., 4., 5., 6., 7., 8., 9.]], dtype=...)

There is a special kind of DSP with 0 input, the synthesizers. For instance, the trivial Faust program

process = 0;

is a synthesizer with 0 input and 1 output.

>>> from faust_ctypes.wrapper import Faust
>>> dsp = Faust("../tests/dsp/minisynth.so")
>>> dsp.proc.compute(10)
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], dtype=...)

Interacting with the User Interface

Note

the building of the interface has been mostly taken from Marc Joliet’s FaustPy code

In order to control the DSP, Faut provides a set of primitives to build a user interface with sliders, buttons, checkboxes, etc… organized into horizontal, vertical or tab groups (see Faust Documentation for more detail). These interface elements are supposed to variate slowly (compared to the sampling frequency). Thus, during one call of data processing the UI elements are considered as constants. Yet one can change them between two subsequent calls. The interface object, stored in the ui attribute of the wrapper, handles this.

Note

only active widgets (checkboxes, sliders, numerical entries) has been implemented. Displays and Bargraphs are ignored for the moment

Consider the following less-tivial Faust code :

gain = hslider("gain", 0, 0, 1, 0.25);
gate = button("gate");

process = *(gain * gate);

It implements a simple sound controller: the input signal is amplified by the gain and the resulting signal is outputted iff the gate is opened. As default, the gain is set to 0 (not 0 dB, proper 0) and the gate is closed. So whatever the input is, the output will be 0.

>>> from faust_ctypes.wrapper import Faust
>>> dsp = Faust("../tests/dsp/simpleui.so")
>>> inp = dsp.proc.from_obj(range(5))
>>> dsp.proc.compute(inp)
array([[0., 0., 0., 0., 0.]], dtype=...)

In order to change this, we must turn on the gain and set the gate up. In faust_ctypes (as in FaustPy), all groups are stored as “boxes” with the user interface elements that they contain as attributes, down to basic UI elements, such as buttons and sliders, called “parameters”. The attribute name of the boxes are their label prefixed with b_, whereas the attribute name of the parameters are their label prefixed with p_.

The Parameter interacting with the gate is then stored in b_simpleui.p_gate (recall that Faust creates by default a vertical group labelled with the DSP name and containing all the UI elements of the upper level).

>>> dsp.ui.b_simpleui.p_gate
<faust_ctypes.interface.Param object at ...>

Each object has a zone attribute, which is a pointer to the values of the UI elements of the DSP. So, to set the gain to .5 and open the gate, one must write

>>> dsp.ui.b_simpleui.p_gate.zone = 1
>>> dsp.ui.b_simpleui.p_gain.zone = .5
>>> dsp.proc.compute(inp)
array([[0. , 0.5, 1. , 1.5, 2. ]], dtype=...)

And the signal now passes through the processor.

Accessing Metadata

The last element of the wrapper is a dictionnary containing all the metadata gathered by the compiler on the source code. With the previous example :

>>> dsp.meta.data
{...:...}