Max has had a human-interface object for decades, so the obvious question is why you would route a gamepad through MIDI at all. The answer is that hi gives you raw numbers from a specific device, and raw numbers from a specific device are exactly what you do not want in a patch you intend to still be using next year. This guide builds a Max/MSP gamepad MIDI controller input stage using ctlin, scale and slide, with Universal Controller MIDI handling deadzones, curves and cross-controller normalisation before anything reaches your patch.
- What you do: publish the pad as a virtual MIDI port, confirm it in Options → MIDI Setup, build
ctlinreceivers, scale withscale, smooth withslideorline~. - What you need: Max 8 or Max 9, macOS or Windows, any supported controller, the bridge.
- Time: 15 minutes to a reusable abstraction.
- Why not
hi: raw axis numbers, no deadzone handling, and a different layout per controller model.
Why use MIDI when Max has the hi object?
Because hi reports whatever the device reports, and every device reports something different — so a patch built on hi is a patch built for one controller.
Plug a DualSense into hi and you get a numbered list of elements. Plug in an Xbox pad and the numbering changes. Plug in a Switch Pro and it changes again. Nothing is deadzoned, so a worn stick floods the patch with jitter. Nothing is curved, so a trigger reads linearly when your hand does not. And nothing is named, so the patch is a wall of magic index numbers that mean nothing when you reopen it.
Going through MIDI pushes all of that into the bridge. Deadzones and curves are applied before the data leaves. CC 1 means "left trigger" whether you plugged in a PlayStation pad or an Xbox one. Your patch stops caring what hardware is attached, which is the actual goal. The trade is one extra application running — which, on a machine already running Max, is noise.
What you need
- Max 8 or Max 9 — macOS or Windows
- Universal Controller MIDI — download for your platform
- A supported gamepad — DualSense, DualShock 4, Xbox Series, Switch Pro, 8BitDo
- macOS: IAC Driver online in Audio MIDI Setup. Windows: nothing extra
How do you get the gamepad into Max?
Launch the bridge first, then check the port appears in Options → MIDI Setup and give it an abbreviation.
Max's MIDI Setup window lists every input and output port the system exposes, with an Abbreviation column. That column is more useful than it looks: set the bridge's abbreviation to something short like ucm, and you can then write [ctlin ucm 1 1] to bind an object to that specific port rather than listening to everything. In a studio with three MIDI devices attached, that is the difference between a patch that works and a patch that responds to your keyboard too.
Verify before you build
Create a bare ctlin with no arguments, connect all three outlets to number boxes, and move a trigger.
[ctlin]
| | |
| | [number] <- channel
| [number] <- controller number
[number] <- value 0..127 This tells you the real CC numbers rather than the ones you assumed, and it proves MIDI is arriving before you spend twenty minutes debugging a scale object that was never the problem.
Building the receiver chain
Three objects per control, in this order: receive, scale, smooth.
[ctlin ucm 1 1] <- L2 trigger, CC 1, channel 1
|
[scale 0 127 200. 4200.] <- map to a filter range in Hz
|
[slide 1 50] <- asymmetric smoothing: fast up, slow down
|
[number~] / [lores~] / whatever consumes it scale is the object people under-use. It takes input low, input high, output low, output high, and optionally an exponential factor as a fifth argument. That fifth argument is how you get a filter sweep that feels right — frequency is perceived logarithmically, so a linear mapping always feels like all the action is at the bottom. Try [scale 0 127 200. 4200. 3.] and the sweep suddenly feels even across the trigger's travel.
slide is the control-rate smoother, and it takes two arguments: slide-up and slide-down. Asymmetric values are enormously useful. [slide 1 50] responds instantly when a value rises and decays slowly when it falls — perfect for an envelope follower feel on a trigger. For symmetric smoothing, use the same number twice.
For anything feeding a signal object directly, use line~ with a ramp time instead, exactly as you would in Pure Data. slide smooths numbers; line~ smooths signals.
Why does raw CC click in the audio?
Because standard MIDI CC is 7-bit, so a full sweep is 128 discrete jumps — and each jump into a signal object is a discontinuity you can hear.
A one-second trigger sweep produces perhaps 60 distinct values. Connected directly to a gain or a cutoff, every one of them is a step. Ramping between them over 10–50 ms removes it completely. This is not a gamepad problem — it is true of every 7-bit MIDI controller ever made, including the expensive ones. The difference is that a hardware fader's physical inertia hides it slightly, and a trigger's spring does not.
How do you handle buttons?
Use notein, strip the note-offs, then route or select each note number to its own outlet.
[notein ucm]
| |
| [sel 0] <- velocity zero = note-off
[route 60 62 64 65]
| | | |
[t b][t b][t b][t b]
The note-off trap catches everyone once: notein emits a note-off as a note-on with velocity zero, so if you bang off the pitch outlet without checking velocity, every button press fires its target twice. Gate on velocity before you route.
For toggles rather than momentary triggers, drop a toggle after the bang. For a latch that holds while pressed, use the velocity value directly as a gate instead of converting it to a bang at all.
Default mapping table for a Max patch
What the bridge sends on channel 1 by default, with the object and a sensible synthesis destination.
| Gamepad input | MIDI | Max object | Good destination |
|---|---|---|---|
| L2 trigger | CC 1 | [ctlin ucm 1 1] | Filter cutoff, exponential scale |
| R2 trigger | CC 2 | [ctlin ucm 2 1] | Output gain / wet mix |
| Left stick X | CC 3 | [ctlin ucm 3 1] | FM index |
| Left stick Y | CC 4 | [ctlin ucm 4 1] | Grain size |
| Right stick X | CC 5 | [ctlin ucm 5 1] | Stereo position |
| Right stick Y | CC 6 | [ctlin ucm 6 1] | Feedback amount |
| Touchpad X | CC 16 | [ctlin ucm 16 1] | Buffer read position |
| Touchpad Y | CC 17 | [ctlin ucm 17 1] | Playback rate |
| Cross / A | Note 60 | [notein ucm] → [route 60] | Envelope trigger |
| Circle / B | Note 62 | [route 62] | Buffer record toggle |
| Square / X | Note 64 | [route 64] | Freeze |
| Triangle / Y | Note 65 | [route 65] | Preset recall via pattrstorage |
| D-pad | Notes 78–81 | [route 78 79 80 81] | Preset step up/down |
Hook it into pattrstorage for instant presets
The nicest Max-specific win here is pairing the d-pad with pattrstorage. Bind d-pad up and down to next and previous messages into a pattrstorage object and you can step through every stored state of the patch without touching the mouse. Combine that with the triggers riding two live parameters and you have a performance instrument rather than a patch — which is usually the thing standing between a finished Max project and actually playing it somewhere.
Should you move to OSC instead?
Only if you genuinely hit the 7-bit ceiling — and with proper scaling and smoothing, most patches never do.
OSC gives you 32-bit floats and named addresses like /gamepad/trigger/left, which is objectively better data. The bridge's OSC output is a Pro feature and Max receives it happily through udpreceive. But OSC is also another transport to configure and another failure mode at a gig. Start with MIDI. If you find yourself fighting resolution on a slow sweep that smoothing cannot hide, then move. The full trade-off is in OSC vs MIDI from a gamepad.
Troubleshooting
- Port missing from MIDI Setup. Max enumerated before the bridge started. Quit Max, start the bridge, relaunch Max.
- Patch responds to your MIDI keyboard too. Add the port abbreviation to the object arguments so it only listens to the bridge.
- Buttons fire twice. You are not filtering note-off. Gate on velocity before routing.
- Sweeps zipper. Missing
slideorline~, or the smoothing value is zero. - Filter sweep feels bottom-heavy. Linear scaling on a logarithmic parameter. Add an exponent as the fifth argument to
scale. - Control jitters at rest. Stick drift — widen the deadzone in the bridge, not in the patch. See drift compensation.
FAQ
Should I use the hi object instead of MIDI?
The hi object reads the gamepad HID stream directly, which saves installing a bridge but hands you raw, undeadzoned, device-specific axis numbers that differ between a DualSense and an Xbox pad. Routing through MIDI gives you cleaned, calibrated, consistent values and a patch that works on both controllers unchanged.
Which Max object receives continuous controller data?
ctlin. With arguments as ctlin <cc> <channel> it outputs only that controller value from its left outlet; with no arguments it outputs value, controller number and channel from three outlets, which is the fastest way to discover what a control is actually sending.
How do I stop CC data clicking in the audio?
Never send a raw control value into a signal object. Use slide for control-rate smoothing or pack it into line~ with a 10 to 50 millisecond ramp. The discontinuity between 7-bit steps is what you are hearing.
Does this work in Max for Live too?
Yes, and it is the same objects. The difference is that inside Live the device receives MIDI from the track rather than from a MIDI Setup port, so you route the bridge port to the track instead of naming it in the object.
Is OSC better than MIDI for a Max patch?
For resolution, yes — OSC carries 32-bit floats and named addresses rather than 7-bit integers. The bridge can output OSC as a Pro feature. For most patches MIDI plus smoothing is indistinguishable, so start with MIDI and move to OSC if you genuinely hit the resolution ceiling.
Where to go next
If you work inside Ableton rather than standalone Max, the Max for Live device guide builds the same input stage into a device with mappable dials, which is a meaningfully different workflow. For the free alternative, Pure Data does all of this with Vanilla objects. And for the visual side of the same idea, TouchDesigner's MIDI CHOP is the closest equivalent.
Download Universal Controller MIDI, set the port abbreviation, and build the abstraction once. Every patch after that gets hands for free.