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
{...:...}