Skip to content
roomi fields

Audio and hardware

FaustScript

A language that adds to Faust the name of an instance and the gestures to change it while it plays; it translates to Faust.

experimental licence MIT

Source code Website

In Faust, a name is a macro: writing fi.lowpass twice builds two filters, and no expression can point at a filter that already exists. FaustScript provides that address, so that in the middle of a set you can say “that filter, open it up”. Its main limit: it makes no sound, it writes Faust; recompiling while the audio runs is the host’s job, and real .dsp files do not yet parse whole.

The project was called FaustX until 7 October 2026; its files then ended in .fx, they now end in .fsc.

No playable demo: there is no host yet that makes FaustScript sound. The project site shows Faust’s five signs and what each one becomes.

Decorate Faust’s signs, invent none

The : stays the connection; what surrounds it qualifies it. Three rules are enough: a ! in front cancels what follows, a digit after says how many, the dot reaches into an instance. The = gives a value, the : only connects.

WrittenEffect
let lpf1 fi.lowpassplace one instance
lpf1 fi.lowpass(fc=400)replace its body, memory kept
saw1 :8 lpf1connect as eight instances
saw1 !: lpf1cut the wire
_ lpf1 · !_ lpf1bypass · un-bypass
! lpf1remove the instance and its wires
lpf1.fc = 400set one control
lfo1 : lpf1.fcdrive a control with a signal

A module is written under its Faust name, prefix included: fi.lowpass, not lowpass. A Faust definition runs up to its ;, as in Faust; any other line is a gesture.

Getting started

You need Node 22 or later.

git clone https://github.com/roomi-fields/faustscript
cd faustscript
npm install
npx faustscript examples/1-drone-that-plays-alone.fsc

The command prints the resulting Faust; -o file.dsp writes it to a file. It ran as written on 28 October 2026, on the repository as of 10 October 2026. A refused gesture goes to standard error, with its reason and its line, and the rest of the file is still translated. For example:

let saw1 os.sawtooth(freq=110)
let lpf1 fi.lowpass(fc=800)

saw1 : lpf1 : process

becomes:

import("stdfaust.lib");
saw1 = vgroup("saw1", os.sawtooth(hslider("freq[unit:Hz][scale:log]", 110, 0.08, 220, 0.001)));
lpf1 = vgroup("lpf1", fi.lowpass(4, hslider("fc[unit:Hz][scale:log]", 800, 2, 8000, 0.001)));
process = saw1 : lpf1;

The filter order stays a constant, fc becomes a slider on a logarithmic scale with its real bounds, and the instance name gives the control path /lpf1/fc. That information comes from the catalogue, not from what you wrote.

What it does

  • Translate to Faust, which the Faust compiler compiles unmodified, with the functions its libraries declare.
  • Know 1,172 modules of those libraries: parameters, starting values, bounds, measured input and output counts, and measured output range for 848 of them. The catalogue is generated from the documentation of the libraries that faustwasm 0.19.0 carries (Faust 2.90.0, libraries 2.74.2); its header records those versions and that count.
  • Cut nothing off abruptly. A bypassed or removed module keeps running on silence, and what was ringing finishes ringing. Measured on a reverb, according to the README: Faust’s ba.bypass1 drops its 97 memory fields to 0; FaustScript keeps 81.
  • Refuse a wrong line without stopping the sound: the graph is only touched if the program compiles.

Common issues

An existing .dsp file does not pass. The grammar reads Faust’s definitions, imports, operators, iterators and feedback, but not yet with{}, letrec, pattern matching or explicit substitution.

A module is refused, lowpass is not a module; fi.lowpass is. Since October 2026, a module is written with its library prefix, as in Faust.

A file written for FaustX no longer translates as is. Its modules without a prefix are refused, and a parameter written fc:800 now reads as a connection, with no error: write fc=800.

Where it stands

Version 0.1.0 of 7 August 2026, according to the changelog, with no release published on GitHub; the rename and the new notation do not have a numbered version yet. The language is settled: all eight elements of Faust have been reviewed, and every claim in the specification checked by compiling. The translator works: the three pieces in examples/ pass without a refused gesture, and the translator’s 310 tests pass on 28 October 2026. Recompiling one module takes about 35 ms, against about 614 ms for a fifty-module program (medians measured on 28 October 2026 with libfaust in WebAssembly, faustwasm 0.19.0); node tools/measure-compilation.mjs reruns the measurement.

What comes next: the host that makes FaustScript sound in the browser, through faustwasm. The project is listed on the “Powered By Faust” page of the Faust website and in livecoding.fr’s list of tools; I proposed it to both, and both additions were accepted in August 2026. As of 28 October 2026, both pages still present it under the name FaustX and link to the repository’s former address, which GitHub redirects.

Repository · Site · Faust · Powered By Faust · livecoding.fr

FaustScript is an independent project, not affiliated with GRAME-CNCM, who develop Faust. Thanks to Stéphane Letz (GRAME), who advised me to redo the measurements through faustwasm, and to Raphaël Forment for the addition to livecoding.fr.

How it was made. Claude Code (Claude Opus 5.5), from the repository, its README and its release notes. Method