Blog Creative 9 min read

Faust Web Audio Patching with Gamepad CC

Write Faust DSP patches in the browser and bind every hslider to a gamepad CC over WebMIDI. Real functional DSP, real gamepad control, zero plugins.

By Aidxn Design

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.

TL;DR
  • 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 inputCCFaust slider
Left stick XCC 0freq (global pitch macro)
Left stick YCC 1cutoff
Right stick XCC 2resonance
Right stick YCC 3attack time
L2 triggerCC 6LFO depth
R2 triggerCC 7gain
Touchpad X / YCC 16 / 17FM index / detune
Face buttonsNote 36–39Voice 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.

Keep reading

More setup walkthroughs