Contributing
In general, libraries are organised in a stacked manner: the base ones define functions or constants without any dependancies, and additional ones are gradually built on top of simpler ones, layer by layer. Dependency loops must be avoided as much as possible. The resources folder contains tools to build and visualise the libraries dependencies graphs.
If you wish to add a function to any of these libraries or if you plan to add a new library, make sure that you observe the following conventions:
New Functions
- All functions must be preceded by a markdown documentation header respecting the following format (open the source code of any of the libraries for an example):
//-----------------`(pr).functionName`--------------------
// Description
//
// #### Usage
//
// ```
// Usage example
// ```
//
// Where:
//
// * `param1`: parameter 1 description
// * `param2`: parameter 2 description
//
// #### Example
//
// ```
// Additional example
// ```
//
// #### Test
// ```
// functionName_test = some_dsp_code;
// ```
//
// #### References
//
// * <https://some_url1>
// * <https://some_url2>
//-------------------------------------------------
- The
functionNamemust be prefixed by thelibraryNamename prefix, like(pr)in the example. - The environment system (e.g.
os.osc) should be used when calling a function declared in another library (see the section on Library Import). - Try to reuse existing functions as much as possible.
- The
Usageline must show the input/output shape (the number of inputs and outputs) of the function, likegen: _for a mono generator,_ : filter : _for a mono effect, etc. TheWhere:section then allows each parameter to be described individually, using the appropriate surrounding quotes. - The
Exampleline can be used to provide additional examples. - The
Testline should used to add a DSP program to test the function. The test name must befunctionName_test. The actual code can be extracted and independantly tested using the-pncompiler option (to specify the name of the dsp entry-point instead of process). The test code must import all the needed libraries, likean = library("analyzers.lib");if a function from analyzers.lib is used in the test code. ThefunctionName_testtest should be added in the relevant file in the tests folder. - The
Referencesline can be used to add links to references. - Some functions use parameters that are constant numerical expressions. The convention is to label them in capital letters and document them preferably to be constant numerical expressions (or known at compile time in existing libraries).
- Functions with several parameters should better be written by putting the more constant parameters (like control, setup...) at the beginning of the parameter list, and audio signals to be processed at the end. This allows to do partial-application. So prefer the following
clip(low, high, x) = min(max(x, low), high);form whereclip(-1, 1)partially applied version can be used later on in different contexts, better thanclip(x, low, high) = min(max(x, low), high);version. - Every time a new function is added, the documentation should be updated simply by running
make doclib.
Layering UI-ready variants
Many functions benefit from two public faces so the same DSP can serve both low-level reuse and ready-to-tweak usage:
- Core function: exposes all parameters, no UI or side effects; best for reuse, composition, and testing.
- UI wrapper: fixes sensible defaults and exposes only runtime-tuned parameters as UI controls; leaves signals that must be provided externally as arguments.
Use the UI-free core for correctness and performance work; build the UI variant when you need something directly tweakable in examples or end-user contexts.
A generic core/UI pair could be:
// Core: parameters explicit, no UI
coreEffect(paramA, paramB, mix) =
fooProcessing(mix, wet)
with {
wet = *(paramA) : barProcessing(paramB); // barProcessing is your DSP
};
// UI wrapper: binds smoothed controls to the core
coreEffect_ui =
coreEffect(paramA_ui, paramB_ui, mix_ui)
with {
paramA_ui = hslider("Param A", 1.0, 0.0, 2.0, 0.01) : si.smoo;
paramB_ui = hslider("Param B", 0.5, 0.0, 1.0, 0.01) : si.smoo;
mix_ui = hslider("Mix", 1.0, 0.0, 1.0, 0.01) : si.smoo;
};
process = coreEffect_ui;
This keeps the core reusable (no UI dependencies) and the wrapper ready for immediate tweaking; process points to the UI layer for quick testing.
Instrument-specific three-layer pattern
Instrument models often add a third, ready-to-play layer. The clarinet model is a reference:
pm.clarinetModel(tubeLength, pressure, reedStiffness, bellOpening): core DSP with every parameter explicit and no UI.pm.clarinetModel_ui(pressure): wraps the core and adds UI sliders for tube length, reed stiffness, bell opening, and output gain; keepspressureas an argument.pm.clarinet_ui_MIDI: builds a playable instrument by pairing the core with a blower/envelope plus MIDI-mapped UI (pitch bend, sustain, vibrato, gain, etc.).
When adding similar models, start with the UI-free core, add a minimal UI wrapper, then optionally provide a controller-specific wrapper (MIDI or otherwise). Keep the core independent so it remains reusable.
Variables and identifiers scoping
To avoid name clashes between libraries, keep identifiers as local as possible. Prefer defining intermediate constants and helpers inside with { ... } blocks or environment { ... } sections, and only expose the intended public entry points. This minimizes collisions when several libraries are imported together and keeps global namespace usage limited to documented, public-facing functions.
When a helper cannot live inside a with/environment block (for instance
because several public functions share it), prefix its name with an
underscore: _helperName. The underscore marks it as internal: it is not
part of the library's public API, needs no documentation block, is excluded
from the documentation coverage accounting (scripts/audit2.py,
make checkdoc), and may change or disappear without notice. Do not use
underscore-prefixed symbols from another library.
New Libraries
- Any new "standard" library should be declared in
stdfaust.libwith its own environment (2 letters - seestdfaust.lib) and inall.lib. - Some of the new library functions should be demonstrated in demos.lib.
- Any new "standard" library must be added to
generateDoc. - Functions must be organized by sections.
- Any new library should at least
declareanameand aversion. - Any new library has to use a prefix declared in the header section with the following kind of syntax:
Its official prefix is 'qu'(look at an existing library to follow the exact syntax). - Be sure to add the appropriate kind of
ma = library("maths.lib");import library line, for each external library function used in the new library (for instancema.foothat would be used somewhre in the code). - The comment based markdown documentation of each library must respect the following format (open the source code of any of the libraries for an example):
//############### libraryName ##################
// Description
//
// * Section Name 1
// * Section Name 2
// * ...
//
// It should be used using the `[...]` environment:
//
// ```
// [...] = library("libraryName");
// process = [...].functionCall;
// ```
//
// Another option is to import `stdfaust.lib` which already contains the `[...]`
// environment:
//
// ```
// import("stdfaust.lib");
// process = [...].functionCall;
// ```
//##############################################
//================= Section Name ===============
// Description
//==============================================
Coding Conventions
In order to have a uniformized library system, we established the following conventions (that hopefully will be followed by others when making modifications to them).
Function Naming
The libraries historically mix snake_case and camelCase in roughly equal
proportion, sometimes inside a single file. Renaming existing public symbols
is not an option (it breaks user code), so the rule applies to new code:
- a new function added to an existing library follows the dominant style of that library (and of the section it lands in, when the library is mixed);
- a new library picks one style in its header section and uses it consistently;
- internal helpers follow the underscore convention described in Variables and identifiers scoping.
For terminology, use the terms of the digital signal processing field (JOS proposal):
impulse: ...,0,1,0,...pulse: ...,0,1,1,0,... or longerimpulse_trainpulse_traingate= pulse controlled externally (e.g., by NoteOn,NoteOff)trigger= impulse controlled externally (gate - gate' > 0) == gate rising edge
Variable Argument List
Strictly speaking, there are no lists in Faust. But list operations can be simulated (in part) using the parallel binary composition operation , and pattern matching.
Thus functions expecting a variable number of arguments can use this mechanism, like a foo function that would be used this way: foo((a,b,c,d)). See fi.iir and fi.fir examples.
Documentation
- All the functions that we want to be "public" are documented.
- We used the
faust2md"standards" for each library://###for main title (library name - equivalent to#in markdown),//===for section declarations (equivalent to##in markdown) and//---for function declarations (equivalent to####in markdown - seebasics.libfor an example). - Sections in function documentation should be declared as
####markdown title. - Each function documentation provides a "Usage" section (see
basics.lib). - The full documentation can be generated using the doc/Makefile script. Use
make helpto see all possible commands. If you plan to create a pull-request, do not commit the full generated code but only the modified .lib files. - Each function can have
declare author "name";anddeclare copyright "XXX";declarations. - Every new function must carry a
declare functionName license "ID";line whoseIDis a canonical SPDX identifier -- e.g.MIT,GPL-3.0-only,LGPL-2.1-or-later,BSD-3-Clause,ISC, or the repository'sLicenseRef-STK-4.3andLicenseRef-LGPL-2.1-or-later-with-Faust-exceptionfor the two licenses without an SPDX-listed id. The accepted spellings are theCANONICAL/ALLOWEDtables ofscripts/normalize_licenses.py; runscripts/normalize_licenses.py --check(included inmake checkdoc) to verify, and plainscripts/normalize_licenses.pyto fix legacy spellings. - Each library has a
declare version "xx.yy.zz";semantic version number to be raised each time a modification is done, and the globalversiontriplet inversion.libfollows — see Versioning for the exact rules.
Versioning
Version numbers live at two levels, and both follow semantic versioning:
- each library carries its own
declare version "MAJOR.MINOR.PATCH";, raised in the same commit as the change it describes; version.libholds the globalvl.versiontriplet for the library set as a whole, raised once per batch of changes according to the highest-ranking change since it was last raised.
Which component to raise:
- MAJOR — any backwards-incompatible change to the public API: removing or renaming a documented function, changing its arity, its parameter order or units, or its audible semantics; and in particular removing deprecated aliases at the end of their grace period.
- MINOR — backwards-compatible functionality: a new function or library, new optional behavior, documenting a previously undocumented symbol, or marking a function deprecated (the alias still works, so deprecation itself is not a breaking change).
- PATCH — backwards-compatible bug fixes and internal improvements: wrong coefficients or constants, performance work, documentation fixes, and any change confined to underscore-prefixed internal symbols (they are not API — see Variables and identifiers scoping).
The deprecation lifecycle ties into this: deprecating a name is MINOR,
and its removal must wait until at least one published release has
shipped with the declare ... deprecated warning, at which point the
removal is the MAJOR change of the next version. When in doubt about
whether a change is breaking, treat the documented behavior — the
function's doc block and its regression test — as the contract: if an
existing test reference has to be regenerated because the output changed,
the change is at least a bug fix worth calling out, and if callers must
edit their code, it is MAJOR.
Library Import
To prevent cross-references between libraries, we generalized the use of the library("") system for function calls in all the libraries. This means that everytime a function declared in another library is called, the environment corresponding to this library needs to be called too. To make things easier, a stdfaust.lib library was created and is imported by all the libraries:
aa = library("aanl.lib");
sf = library("all.lib");
an = library("analyzers.lib");
ba = library("basics.lib");
co = library("compressors.lib");
db = library("debug.lib");
de = library("delays.lib");
dm = library("demos.lib");
dx = library("dx7.lib");
en = library("envelopes.lib");
fd = library("fds.lib");
fi = library("filters.lib");
ho = library("hoa.lib");
hy = library("hysteresis.lib");
it = library("interpolators.lib");
la = library("linearalgebra.lib");
ma = library("maths.lib");
mi = library("mi.lib");
ef = library("misceffects.lib");
mo = library("motion.lib");
no = library("noises.lib");
os = library("oscillators.lib");
pf = library("phaflangers.lib");
pl = library("platform.lib");
pm = library("physmodels.lib");
qu = library("quantizers.lib");
rm = library("reducemaps.lib");
re = library("reverbs.lib");
ro = library("routes.lib");
sp = library("spats.lib");
si = library("signals.lib");
so = library("soundfiles.lib");
sy = library("synths.lib");
ve = library("vaeffects.lib");
vl = library("version.lib");
wa = library("webaudio.lib");
wd = library("wdmodels.lib");
For example, if we wanted to use the smooth function which is now declared in signals.lib, we would do the following:
import("stdfaust.lib");
process = si.smooth(0.999);
This standard is only used within the libraries: nothing prevents coders to still import signals.lib directly and call smooth without ro., etc. It means symbols and function names defined within a library have to be unique to not collide with symbols of any other libraries.
"Demo" Functions
"Demo" functions are placed in demos.lib and have a built-in user interface (UI). Their name ends with the _demo suffix. Each of these function have a .dsp file associated to them in the Faust project examples folder.
Any function containing UI elements should be placed in this library and respect these standards.
"Standard" Functions
"Standard" functions are here to simplify the life of new (or not so new) Faust coders. They are declared in /libraries/doc/standardFunctions.md and allow to point programmers to preferred functions to carry out a specific task. For example, there are many different types of lowpass filters declared in filters.lib and only one of them is considered to be standard, etc.
Testing the library
Before preparing a pull-request, the new library must be carefully tested:
- all functions defined in the library must be tested by preparing a DSP test program, to be added using the
#### Testsyntax - the compatibility library
all.libimports all libraries in a same namespace, so check functions names collisions using the following test program:import("all.lib"); process = _; - reference files for all tests can be generated using the
make referencecommand and then verified with themake checkcommand, which compares the generated samples against the reference files within a specified tolerance. A good practice for developers is therefore to generate the reference files and re-run the checks whenever the code is modified.make checkfails on the first divergence; usemake -k checkto run the whole suite and collect every failure. - every new function therefore ships with both its
#### Testsection and the correspondingfunctionName_testentry in the tests folder, and its reference is generated withmake referencein the same change. - finally,
make checkdocmust pass: it rejects any new undocumented symbol, any documentation block without a#### Usagesection, any block reduced to a#### Testsection, a staledoc/standardFunctions.md, and any non-canonical license string, while the historical debt recorded intests/doc-baseline.jsonstays accepted.
Formal certification (experimental, work in progress)
Status: experimental. This tool is under active development: the set of properties it can certify is deliberately small, its verdicts and file formats may change, and it is not required for a pull-request to be accepted. Regular testing (
make reference/make check/make checkdoc) remains the contract.
Alongside the numerical test harness, the repository carries a formal
certification pipeline based on Lean 4: the compiled
signal graph of a DSP example is imported into Lean, analysed, and each verdict
is pinned as a machine-checked theorem. The hand-written specifications live in
formalisation/, the certified examples in tests/lean/, and the generator in
scripts/sig2lean.py.
Two properties are currently certified, on concrete instantiations:
- feedback stability: linear recursions of order ≤ 2 with constant
coefficients are checked against the Jury criterion in exact rational
arithmetic (e.g.
fi.tf2instances); - index bounds: every table read and delay tap is checked to stay in range as written — as opposed to being made safe by a compiler-inserted clamp.
Everything the analysers do not recognise exactly is refused, not guessed:
a refusal (not certified, not proven) is a statement about the analyser's
current coverage, not a defect report about the function.
Contributor workflow
No Lean knowledge is needed. Prerequisites: lean (4.31, bundled Std only) and
faust-rs on the PATH — or override with make certify FAUST_RS=... LEAN=....
- Add a small DSP program to
tests/lean/instantiating the new function with concrete parameters, e.g.tests/lean/machin.dsp:
xx = library("malib.lib");
process = xx.machin(3, 1000);
The file's base name becomes the name of the generated definitions and
theorems. A nominal case plus a boundary case is a good default; a
deliberate counter-example whose refusal is itself pinned (like
tf2_unstable.dsp or table_bad_clamp.dsp) is also valuable.
-
Run
make certify. It regenerates the theorems intotests/build/, kernel-checks them, and diffs against the committedtests/lean/certified.lean— so a new.dspmakes it fail, displaying exactly the block that would be added. Read the verdicts in that diff: -
STABLE/IN RANGE: the property is certified by a kernel-checked theorem; NOT STABLE/CLAMP REQUIRED: the graph really is unstable, or the index only stays in the table thanks to the backend's clamp — for a library function, a hint to clamp or document on the Faust side;not a recognised linear recursion/not proven: outside the certified fragment (coefficients depending onma.SR, nonlinear recursion, order > 2…). The refusal is pinned as a= falsetheorem, so if a later extension of the analysers unlocks the case,make certifywill show the verdict flip.
The run also cross-checks every table verdict against the compiler's own
clamp insertion (the -ct pass, read from
faust-rs --dump-sig-dag-prepared): a CLAMP REQUIRED table the
compiler left unclamped fails certification outright, and a clamp on a
table Lean proves in range is recorded as a missed optimisation in the
"Compiler clamp oracle" section of certified.lean.
- Once the verdicts look right, run
make certify-referenceto regeneratetests/lean/certified.leanin place, and commit both the.dspand the regeneratedcertified.lean.
From then on, every make certify re-proves the theorems and turns any
verdict drift — from a compiler or specification change — into a loud failure,
exactly as make check does for numerical outputs.
Two standing limits are worth knowing: certification applies to the exported
signal graph (not to the generated C++/Rust code), and to the exact rationals
denoted by the coefficients (not to floating-point execution). Both are
recorded as named obligations in formalisation/signal-import-formal-spec.lean.
A third, optional layer exists for maintainers: make certify-deep builds a
small mathlib-based project
(formalisation/mathlib/) that discharges the central recorded obligation —
the executable Jury test is proved equivalent, at order 2, to "every pole lies
strictly inside the unit disc" — plus the positivity hypothesis of the tf2s
theorem. It pins its own toolchain and downloads the mathlib build cache
(several GB) on first run; it is never needed for make certify, for
contributions, or for pull-requests.
LLMs
The site exposes an llms.txt file generated from doc/docs/llms.txt and published at https://faustlibraries.grame.fr/llms.txt.
Faust Library JSON Exports
This repository can also generate machine-readable JSON exports of the Faust
library documentation directly from the .lib sources.
The generator is:
scripts/build_faust_doc_index.py
It parses documentation blocks from the library sources, starting from
stdfaust.lib, follows library("...") and import("...") directives, and
extracts for each documented symbol:
summaryusageparamsnotesiowithinSignals/outSignalswhen derivabletestCodereferenceslicensewhen a per-symboldeclare ... license|licence "..."is presentsource
Two JSON layouts are supported:
- monolithic: one full JSON file containing all libraries and symbols
- split: one compact global index plus one detailed JSON file per module
Default Make targets:
make doc-index
make doc-index-split
make doc-index-commercial
Default output locations:
make doc-indexwritestests/faust-doc-index.jsonmake doc-index-splitwrites:tests/faust-doc-index.jsontests/faust-doc/index.jsontests/faust-doc/modules/*.jsonmake doc-index-commercialwrites the same paths asmake doc-index-split, but filters the exported symbols using thecommercial-compatiblelicense policy
You can override the output paths:
make doc-index DOC_INDEX_OUTPUT=/tmp/faust-doc-index.json
make doc-index-split DOC_INDEX_OUTPUT=/tmp/faust-doc-index.json DOC_INDEX_SPLIT_DIR=/tmp/faust-doc
You can also run the generator directly:
python3 scripts/build_faust_doc_index.py --repo-root . --output tests/faust-doc-index.json --pretty
python3 scripts/build_faust_doc_index.py --repo-root . --output tests/faust-doc-index.json --split-output-dir tests/faust-doc --pretty
python3 scripts/build_faust_doc_index.py --repo-root . --output tests/faust-doc-index.json --split-output-dir tests/faust-doc --license-policy commercial-compatible --pretty
License-policy filtering is optional. The supported values are:
all: export every documented symbolcommercial-compatible: keep only symbols that pass a conservative per-symbol license heuristic
The current commercial-compatible heuristic:
- accepts missing per-symbol licenses and treats them as falling back to the library default
- accepts common permissive or weak-copyleft markers such as
MIT,BSD,Apache,LGPL,LGPL with exception,MPL,ISC,zlib,Boost,Unlicense,public domain, andSTK-4.3 - rejects markers such as
GPL,AGPL, and explicitly non-commercial terms
This is a practical export filter for tooling, not a legal opinion.
The policy can also be customized with external allowlist/denylist files:
python3 scripts/build_faust_doc_index.py \
--repo-root . \
--output tests/faust-doc-index.json \
--split-output-dir tests/faust-doc \
--license-policy commercial-compatible \
--license-allowlist-file /path/to/license-allowlist.txt \
--license-denylist-file /path/to/license-denylist.txt \
--pretty
These files use a simple newline-based format:
- one token or pattern per line
- matching is case-insensitive and based on substring inclusion
- empty lines are ignored
- lines starting with
#are treated as comments
Example:
# Allow permissive licenses and a specific local marker
mit
bsd
apache
my-company-approved-license
The Make target also supports these overrides:
make doc-index-commercial \
DOC_INDEX_LICENSE_ALLOWLIST_FILE=/path/to/license-allowlist.txt \
DOC_INDEX_LICENSE_DENYLIST_FILE=/path/to/license-denylist.txt
The split layout is recommended for LLM or retrieval-based use because it avoids loading the whole documentation into context for every request.
Local Documentation Query API
The repository also ships with a local query tool that reproduces the Faust
library documentation operations used in faustforge:
scripts/faust_doc_api.py
Supported operations:
search_faust_libget_faust_symbollist_faust_moduleget_faust_examplesexplain_faust_symbol_for_goal
The tool can read either:
- the monolithic export
tests/faust-doc-index.json - the split export
tests/faust-doc/index.json
If --index is omitted, it tries these defaults in that order:
tests/faust-doc/index.jsontests/faust-doc-index.json
Examples:
python3 scripts/faust_doc_api.py --pretty search_faust_lib reverb --limit 5
python3 scripts/faust_doc_api.py --pretty search_faust_lib filter --module filters --limit 10
python3 scripts/faust_doc_api.py --pretty get_faust_symbol de.delay
python3 scripts/faust_doc_api.py --pretty list_faust_module delays --limit 20
python3 scripts/faust_doc_api.py --pretty get_faust_examples delay
python3 scripts/faust_doc_api.py --pretty explain_faust_symbol_for_goal re.springreverb "build a metallic spring reverb"
To force a specific index location:
python3 scripts/faust_doc_api.py --index tests/faust-doc-index.json --pretty get_faust_symbol aa.Rsqrt
python3 scripts/faust_doc_api.py --index tests/faust-doc --pretty list_faust_module filters --limit 10
The make clean target removes the generated JSON artifacts by default:
tests/faust-doc-index.jsontests/faust-doc/
Licensing issue
Some DSP functions carry per-symbol licenses that are more restrictive than the default Faust libraries distribution terms. This matters in LLM-assisted code generation and code reuse workflows.
In particular, functions released under licenses that are non-commercial, copyleft-incompatible, or otherwise unsuitable for the intended downstream use must not be suggested blindly in generated code.
When exposing Faust library data to LLM tools or retrieval systems:
- preserve per-function license metadata when it exists
- make that metadata queryable alongside the usual documentation fields
- check license compatibility before recommending or assembling generated DSP code from library functions
The local JSON export and query tooling can expose this information, so license checks can be integrated into higher-level assistants and generation pipelines.
Library test and deployment
For GRAME maintainers:
- global tests can be done using the
make referenceandmake checkat rool level - regenerate the PDF documentation using
make pdftarget at rool level - update the library submodule in faust, recompile and deploy WebAssembly libfaust in fausteditor, faustplayground and faustide
- update the library submodule in faustlive
- update the library list in this fausteditor page as well as the snippets (using the
faust2atomsnippets -s *.libcommand at root level). - update the library list in this faustide page and LIBRARIES folder.
- update the library list in the faustgen~ code
- update the Faust Syntax Highlighting Files
- make an update PR for vscode-faust project