Pure Data gives you an infinitely flexible signal graph and almost no physical interface. That is the whole problem with patching environments: you build a beautiful synthesis engine and then drive it with a mouse on a number box. A gamepad fixes it for the price of something you already own — five simultaneous continuous axes and fourteen buttons, all arriving as plain MIDI that Pd Vanilla handles with built-in objects. This guide builds a Pure Data gamepad MIDI input stage with Universal Controller MIDI, including the scaling and smoothing that stops 7-bit control data from zippering in your audio.
- What you do: publish the pad as a virtual MIDI port, pick it in Media → MIDI Settings, build
ctlinreceivers, scale withexpr, smooth withline~. - What you need: Pd Vanilla 0.54 or later (or Purr Data), macOS, Windows or Linux, any supported controller, the bridge.
- Time: 15 minutes to a working abstraction you never have to build again.
- Objects used:
ctlin,notein,expr,line~,route— all Vanilla, no externals.
Why not just use a HID external?
Because the externals are inconsistent across platforms and they hand you raw, unscaled, undeadzoned axis integers that you then have to clean up in the patch yourself.
There have been HID objects in the Pd world for twenty years. They work, sometimes, on one operating system, with one controller model. What they do not do is give you a drifting stick corrected, a trigger with a usable response curve, or a stable control number that means the same thing on a DualSense and an Xbox pad. Routing through MIDI pushes all of that into the bridge, where it belongs, and leaves your patch with clean 0–127 values from built-in objects that will still work in five years. It also means the same patch runs unchanged on macOS, Windows and Linux.
What you need
- Pd Vanilla 0.54+ or any Pd-compatible environment
- Universal Controller MIDI — download for macOS, Windows or Linux
- A supported gamepad — DualSense, DualShock 4, Xbox Series, Switch Pro, 8BitDo
- A virtual MIDI port — IAC on macOS, the bridge's own port on Windows, ALSA sequencer on Linux
How do you get the gamepad into Pd?
Launch the bridge first, then open Media → MIDI Settings in Pd and set Input Device 1 to the bridge port.
Pd's MIDI Settings dialog is a simple list of input and output device slots. Set Input Device 1 to UCM Bridge. Leave the output devices empty unless you are also sending MIDI out — an unused output slot costs nothing but it is one more thing to misconfigure.
Click Apply, then OK. Pd applies MIDI device changes without a restart on most platforms, which is a nice change from most DAWs. If the port is missing from the list, the bridge was not running when Pd started scanning — reopen the dialog, or restart Pd.
Verify with a bare ctlin
Before building anything, prove data is arriving. Create an object box containing just ctlin with no arguments, connect all three outlets to number boxes, and move a trigger. You should see the value, the controller number and the channel update live. This is the fastest possible sanity check and it tells you the real CC numbers rather than the ones you assumed.
[ctlin]
| | |
| | [number] <- MIDI channel
| [number] <- controller number
[number] <- value 0..127 Building the receiver block
Once you know the numbers, switch to argument form. [ctlin 1 1] means "controller 1, channel 1" and outputs just the value from its left outlet. That is what you want in production, because it does the filtering for you and leaves one clean stream per control.
[ctlin 1 1] <- L2 trigger
|
[expr $f1 / 127 * 4000 + 200] <- scale to 200..4200 Hz
|
[pack f 20]
|
[line~]
|
[lop~ ] <- or whatever consumes it Three stages, and each one matters:
ctlin— receives the raw 0–127 value.expr— scales it to the range your parameter actually needs. Write the maths explicitly rather than using a generic scale abstraction; six months later you want to read the numbers.pack f 20intoline~— converts each discrete value into a 20 ms ramp. This is the step people skip, and it is the one that stops the clicking.
Why does CC sound like zippering without line~?
Because standard MIDI CC is 7-bit — 128 discrete steps — and jumping a filter frequency or a gain instantly between steps produces an audible discontinuity.
A trigger sweep that takes half a second generates maybe 60 distinct values. Connected straight to a signal object, each one is a step change in the audio, and you hear the staircase. Ramping between them with line~ over 10–50 ms makes the parameter continuous, and the stepping disappears entirely.
Pick the ramp time by what the parameter does:
- 10 ms — fast gestures, percussive filter stabs. Short enough to feel immediate.
- 20 ms — the sensible default for most sweeps.
- 50 ms+ — slow pads and drones, where you want the control to feel heavy. Also useful for hiding a noisy stick.
How do you receive buttons?
Use notein, and split the note numbers with route or select so each button gets its own outlet.
[notein]
| |
| [number] <- velocity
[route 60 62 64 65 67 69]
| | | | | |
| | | | | [bang] <- R1
| | | | [bang] <- L1
| | | [bang] <- Triangle
| | [bang] <- Square
| [bang] <- Circle
[bang] <- Cross
A gotcha worth knowing: notein emits both note-on and note-off, and a note-off is a note-on with velocity zero. If you bang something off the left outlet without checking velocity, every button press fires twice. Filter it with [select 0] on the velocity outlet, or use [moses 1] to split zero-velocity messages away before they reach your triggers.
Default mapping table for a Pd patch
The bridge's default output on channel 1, with the object you would use to receive each one.
| Gamepad input | MIDI | Pd object | Good use |
|---|---|---|---|
| L2 trigger | CC 1 | [ctlin 1 1] | Filter cutoff |
| R2 trigger | CC 2 | [ctlin 2 1] | Output gain / wet mix |
| Left stick X | CC 3 | [ctlin 3 1] | Detune / FM index |
| Left stick Y | CC 4 | [ctlin 4 1] | Grain density |
| Right stick X | CC 5 | [ctlin 5 1] | Stereo spread |
| Right stick Y | CC 6 | [ctlin 6 1] | Resonance |
| Touchpad X | CC 16 | [ctlin 16 1] | Table read position |
| Touchpad Y | CC 17 | [ctlin 17 1] | Table read speed |
| Cross / A | Note 60 | [notein] → [route 60] | Envelope bang |
| Circle / B | Note 62 | [route 62] | Sample retrigger |
| Square / X | Note 64 | [route 64] | Buffer freeze toggle |
| Triangle / Y | Note 65 | [route 65] | Preset recall |
| D-pad | Notes 78–81 | [route 78 79 80 81] | Scene navigation |
Make it an abstraction so you never build it twice
Save the whole receiver block as a file called gamepad-in.pd in your Pd search path, with outlet objects for each scaled stream. From then on, any patch gets the whole rig by creating a single object box containing gamepad-in.
Keep the scaling outside the abstraction. The abstraction should emit clean, labelled 0–1 floats; each patch then scales those into whatever ranges it needs. If you bake "200 to 4200 Hz" into the abstraction, you will copy it and edit it, and then you have two abstractions that disagree — which is exactly the trap this pattern exists to avoid.
Troubleshooting
- Nothing arrives at ctlin. Wrong input device in Media → MIDI Settings, or the bridge was not running when Pd scanned. Reopen the dialog.
- Every button fires twice. You are not filtering note-off. Split velocity zero away with
[moses 1]or[select 0]. - Audible stepping on sweeps. No
line~in the chain, or the ramp time is zero. - A control jitters at rest. Stick drift. Widen the deadzone in the bridge rather than patching around it in Pd — see stick drift compensation.
- Values arrive on the wrong CC. Check the real numbers with a bare
ctlinbefore assuming. - You want 32-bit floats instead of 0–127. That is OSC, not MIDI. The bridge's OSC mode is a Pro feature; the trade-offs are in OSC vs MIDI from a gamepad.
FAQ
Can Pure Data read a gamepad directly?
Not in Pd Vanilla. There are HID externals in Pd-extended-era libraries, but they are inconsistent across platforms and give you raw unscaled axis numbers. Routing through a MIDI bridge gives Pd clean, named, deadzoned CC values using only built-in objects.
Which object receives continuous controls?
ctlin. Written with arguments as ctlin <cc> <channel>, it outputs that single controller value from its left outlet. Written with no arguments, it outputs value, controller number and channel from three outlets, which is useful for debugging.
How do I stop CC changes from clicking in the audio?
Never connect a control value straight to a signal object. Send it into line~ with a ramp time of 10 to 50 milliseconds, or use a one-pole lowpass on the control stream. Jumping a gain or frequency instantly is what produces the click.
Does this work with Purr Data or Pd-extended?
Yes. ctlin, notein, line~ and expr are all Pd Vanilla objects, so any Pd-compatible environment handles this patch unchanged.
Do I need Pro for a Pure Data rig?
The free tier sends the default CCs and notes, which is plenty for a Pd patch. Pro is what you need for custom CC assignments, per-binding curves, more than three saved presets, or OSC output instead of MIDI.
Where to go next
If you want the same approach in a commercial patching environment, the Max/MSP guide covers the equivalent objects and the one place Max does it better. For code-first environments, SuperCollider and Csound both have their own write-ups using the same bridge output.
Download Universal Controller MIDI, drop a bare ctlin into a patch, and watch the numbers move. Everything after that is just scaling.