Blog Max/MSP 9 min read

Max/MSP — Gamepad MIDI Patching That Beats the hi Object

Wire a PS5 DualSense or Xbox pad into Max/MSP: ctlin receivers, scale and slide smoothing, a reusable abstraction, and why MIDI beats the hi object.

By Aidxn Design

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.

TL;DR
  • What you do: publish the pad as a virtual MIDI port, confirm it in Options → MIDI Setup, build ctlin receivers, scale with scale, smooth with slide or line~.
  • 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 inputMIDIMax objectGood destination
L2 triggerCC 1[ctlin ucm 1 1]Filter cutoff, exponential scale
R2 triggerCC 2[ctlin ucm 2 1]Output gain / wet mix
Left stick XCC 3[ctlin ucm 3 1]FM index
Left stick YCC 4[ctlin ucm 4 1]Grain size
Right stick XCC 5[ctlin ucm 5 1]Stereo position
Right stick YCC 6[ctlin ucm 6 1]Feedback amount
Touchpad XCC 16[ctlin ucm 16 1]Buffer read position
Touchpad YCC 17[ctlin ucm 17 1]Playback rate
Cross / ANote 60[notein ucm] → [route 60]Envelope trigger
Circle / BNote 62[route 62]Buffer record toggle
Square / XNote 64[route 64]Freeze
Triangle / YNote 65[route 65]Preset recall via pattrstorage
D-padNotes 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 slide or line~, 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.

Keep reading

More setup walkthroughs