Faust gamepad patching is the rig for the people who think Tone.js is too high-level and writing your own JUCE plugin is too low-level. Faust is a functional DSP language out of GRAME — you write signal-flow expressions, the compiler emits WebAssembly that runs at C++ speed in any modern browser. Add a gamepad through Universal Controller MIDI and every hslider in your patch becomes a live macro. No DAW required.
- What you do: write a Faust patch, annotate sliders with
[midi:ctrl N], enable MIDI in the IDE. - What you need: Faust IDE (browser), Universal Controller MIDI, a gamepad.
- Time: 20 minutes for a polyphonic synth with gamepad macros.
- Cost: $89 bridge. Faust is GPL/MIT, the IDE is free.
Why Faust is the right DSP layer
Faust is small, sharp, and brutally efficient. The whole language fits in a one-pager: process is the output, operators wire signals together, sliders are user-facing parameters. You can write a working synth in fifteen lines. The compiler then targets WebAssembly, VST3, AU, LV2, JUCE, Max external — the same source becomes a browser toy and a paid plugin. For gamepad work, the killer feature is [midi:ctrl N] metadata: annotate a slider, the runtime binds it to CC N automatically. No glue code.
The smallest playable Faust patch
Paste this into the Faust IDE. It's a saw oscillator with a low-pass filter — frequency, cutoff, and resonance are all CC-bound.
import("stdfaust.lib");
freq = hslider("freq[midi:ctrl 0]", 220, 40, 880, 0.1);
cutoff = hslider("cutoff[midi:ctrl 1]", 1200, 100, 8000, 1);
res = hslider("resonance[midi:ctrl 2]", 1, 0.5, 20, 0.01);
gain = hslider("gain[midi:ctrl 7]", 0.2, 0, 1, 0.01);
process = os.sawtooth(freq) : fi.resonlp(cutoff, res, 1) * gain
<: _, _; // mono → stereo
Hit Run. Enable MIDI in the IDE (gear icon, MIDI input → bridge). Wiggle the left stick — frequency moves. Squeeze R2 — gain rides up. Stick Y is filter cutoff. You wrote a synth in eight lines.
Going polyphonic with note input
Faust handles polyphony through compiler metadata. Add [polyphony:8] to the patch and Faust spins up an 8-voice wrapper that accepts MIDI Note On/Off. The face buttons on the gamepad land as notes — wire them as drum pads, or hold a chord across the dpad.
declare options "[midi:on][nvoices:8]";
import("stdfaust.lib");
// Per-voice freq/gain comes from the polyphonic wrapper.
freq = hslider("freq", 220, 40, 4000, 0.1);
gain = hslider("gain", 0.5, 0, 1, 0.01);
gate = button("gate");
// Global macros driven by the gamepad sticks.
cutoff = hslider("cutoff[midi:ctrl 1]", 1200, 100, 8000, 1);
res = hslider("resonance[midi:ctrl 2]", 2, 0.5, 20, 0.01);
attack = hslider("attack[midi:ctrl 3]", 0.01, 0.001, 1, 0.001);
env = en.adsr(attack, 0.2, 0.6, 0.4, gate);
voice = os.sawtooth(freq) * env * gain : fi.resonlp(cutoff, res, 1);
process = voice <: _, _;
The polyphonic wrapper assigns one voice per Note On. Press Cross — voice 1 fires at 36 Hz... no, wait, it fires at MIDI note 36 → 65.4 Hz. The dpad gives you four buttons; map them to a chord, and the right stick lives on top as the filter sweep.
The default CC map for the patch
| Gamepad input | CC | Faust slider |
|---|---|---|
| Left stick X | CC 0 | freq (global pitch macro) |
| Left stick Y | CC 1 | cutoff |
| Right stick X | CC 2 | resonance |
| Right stick Y | CC 3 | attack time |
| L2 trigger | CC 6 | LFO depth |
| R2 trigger | CC 7 | gain |
| Touchpad X / Y | CC 16 / 17 | FM index / detune |
| Face buttons | Note 36–39 | Voice trigger |
Effects chain — built right into the patch
Faust comes with stdfaust.lib, a library of vetted DSP — reverbs, delays, distortions, filters. Layer them inside process and bind their parameters to more CCs. The whole signal chain stays in one source file.
import("stdfaust.lib");
freq = hslider("freq[midi:ctrl 0]", 220, 40, 880, 0.1);
cutoff = hslider("cutoff[midi:ctrl 1]", 1200, 100, 8000, 1);
res = hslider("res[midi:ctrl 2]", 2, 0.5, 20, 0.01);
drive = hslider("drive[midi:ctrl 6]", 1, 1, 20, 0.1);
verb = hslider("verb[midi:ctrl 7]", 0.3, 0, 1, 0.01);
synth = os.sawtooth(freq) : fi.resonlp(cutoff, res, 1);
dist = synth * drive : ef.cubicnl(0.5, 0);
process = dist : re.mono_freeverb(0.8, 0.7, 0.5, 44100) * verb
+ dist * (1 - verb)
<: _, _;
Saw → resonant low-pass → cubic distortion → freeverb, with dry/wet controlled by R2 and drive by L2. The whole signal flow is in one expression. Try writing that in JUCE in twenty lines.
Export to a real plugin
Once the patch sounds right, hit Export in the IDE. Faust will hand you a VST3 or AU bundle with the same MIDI bindings baked in. Drop it into Ableton/Logic/Bitwig, set the bridge as the MIDI input for that track, the gamepad still drives the same sliders. The browser session becomes a production plugin without rewriting a line. For more on the bridge's plugin-host integration, see modular synth CV from a gamepad.
When Faust beats everything else
Faust wins when you need DSP that actually performs and runs everywhere — browser, plugin, embedded device — from one source file. It's not the right tool for sequencing or note arrangement; Tone.js covers that better. Faust is also a steeper curve than p5.sound — see the p5.js gamepad guide if you want a softer ramp. But once you're past the learning hump, Faust is the cleanest expression of "this is the DSP I want, and these are the gamepad knobs that control it" that exists today.
Eight lines of code, one gamepad, a $89 bridge, a free compiler. The Universal Controller MIDI is the cable that ties the room together.