Between the raw integer a gamepad stick reports and the MIDI value a synth receives sits a small amount of maths that almost entirely determines how the control feels. Get it wrong in one specific way and the stick snaps to cardinal directions. Get it wrong in another and it never reaches full scale. Get it wrong in a third and slow sweeps develop plateaus. All three are common, all three are avoidable, and all three come down to doing four operations in the right order with the right shapes.
This is the maths post behind every gamepad deadzone curve decision. Deadzone geometry, rescaling, curve shapes, and the ordering rule that ties them together.
- Order: normalise → radial deadzone → rescale → curve → clamp → quantise once.
- Radial, not axial. Apply the threshold to vector magnitude, not per axis, or you get diagonal snapping.
- Rescaling is mandatory. Subtracting a deadzone without dividing by the remaining range costs you your ceiling.
- Curve exponent above one gives fine control near centre — usually what you want for sweeps.
- Measure before you set. Your deadzone should come from your measured noise floor, not a default.
Why does a stick need a deadzone at all?
Because "at rest" is not a single value. Two independent things make the resting reading non-zero. First, electrical and sensor noise means the reported value wanders within a small band even when nothing moves. Second, the mechanical return-to-centre of a gimbal is approximate, so the physical resting position is near centre rather than exactly at it, and that offset grows as springs age.
Without a deadzone, both of those become MIDI messages. In a game, a slow unintended camera drift is annoying. In a MIDI rig it is worse: a filter that wobbles when nobody is touching the controller, an automation lane filling with spurious points during a take, and a continuous stream of CC traffic competing with the messages you actually meant to send. The bandwidth cost of that is real on a constrained link, as covered in MIDI's 31,250 baud explained.
So the deadzone is not a tuning preference, it is a requirement. The question is only how large it must be, and the honest answer is: as large as your measured noise floor and no larger. Deadzone is range you are throwing away, and the argument for why that matters so much is in Hall-effect versus potentiometer sticks.
Why is axial deadzone the wrong shape?
An axial deadzone applies the threshold to each axis independently: if |x| < d then x = 0, and likewise for y. It is one line of code and it is what most first implementations do. The dead region it produces is a square, and the square is the problem.
Consider pushing the stick diagonally with a deadzone of 0.2 and reaching x = 0.6, y = 0.15. The y axis is below threshold, so it is zeroed, and the output is pure horizontal. You pushed diagonally and got a cardinal. Push a little further and y crosses the threshold and suddenly appears at full strength. The control snaps between axis-aligned and diagonal rather than moving continuously, and on a two-axis MIDI mapping — an XY filter pad, say — that is immediately audible as one parameter jumping while the other holds.
A radial deadzone applies the threshold to the vector magnitude instead. The dead region is a circle, the behaviour is identical in every direction, and diagonal input is preserved proportionally:
// Axial - square dead region, snaps to cardinals. Do not use for sticks.
if (fabs(x) < dz) x = 0.0;
if (fabs(y) < dz) y = 0.0;
// Radial - circular dead region, direction-independent. Correct for sticks.
double mag = sqrt(x*x + y*y);
if (mag < dz) {
x = y = 0.0;
} else {
double rescaled = (mag - dz) / (1.0 - dz); // restore full-scale ceiling
if (rescaled > 1.0) rescaled = 1.0; // square gate corners
double k = rescaled / mag; // scale factor preserving angle
x *= k;
y *= k;
}
It is worth knowing that platform APIs ship their own deadzone constants and expect you to use them. XInput's XINPUT_GAMEPAD documentation names XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE and XINPUT_GAMEPAD_TRIGGER_THRESHOLD, whose values live in XInput.h — and note that those are documented as per-axis filters, which is to say axial. If you use them as given on a two-axis stick, you inherit the square dead region described above. The W3C Gamepad API takes the opposite approach and specifies no deadzone at all, leaving it entirely to the page, which is why browser-based rigs so often feel twitchy out of the box — see WebMIDI browser-only gamepad rigs.
The one case where axial is correct is a genuinely single-axis control — a trigger, or a stick axis you have deliberately decided to treat independently because you have mapped the two axes to unrelated parameters. For anything where the two axes form a direction, radial is the only shape that behaves.
Why is rescaling not optional?
Because without it, you have just lowered your maximum output. If your deadzone is 0.2 and you subtract it without dividing, full physical deflection produces 0.8 rather than 1.0 — your filter never fully opens, your crossfade never fully crosses, and your velocity never reaches 127. People then compensate by raising the output gain somewhere downstream, which reintroduces the problem in a less obvious form.
The correct form is the one in the code above: subtract the deadzone, then divide by 1 - deadzone. That maps the surviving range [dz, 1] onto [0, 1], so the deadzone edge produces zero and full deflection produces one. The cost of the deadzone is now entirely in resolution rather than in range, which is the right place for it to be.
The clamp on the next line matters for a reason that is easy to miss. Many sticks have a square or octagonal physical gate, so the corners allow both axes to reach near-maximum simultaneously. The vector magnitude at a corner can therefore exceed 1.0 — around 1.41 for a perfect square gate — and without a clamp your corner values rescale to above full scale and get truncated at the MIDI stage, producing a region where the control stops responding. Clamp first, and corners behave like a slightly flattened version of the cardinals rather than a dead zone of their own.
What about an outer deadzone?
Often worth having, and rarely implemented. Just as a stick does not rest at exactly zero, it does not necessarily reach exactly full scale at its physical limit — manufacturing tolerance and wear mean the reported maximum can fall short, which means you can push the stick as hard as you like and never get 127.
An outer deadzone treats everything above a threshold as full scale, guaranteeing you can reach the ceiling:
// Inner and outer deadzone in one normalisation step
double apply_deadzones(double mag, double inner, double outer) {
if (mag <= inner) return 0.0;
if (mag >= outer) return 1.0;
return (mag - inner) / (outer - inner);
}
// Typical: inner from your measured noise floor, outer just inside
// the lowest full-deflection reading you observed during calibration.
Linux gives you a shortcut here, because the kernel input layer carries per-axis range information alongside the events themselves. The EV_ABS event type described in the kernel input event codes documentation is accompanied by an absinfo structure reporting minimum, maximum, fuzz and flat for each axis — where flat is the driver's own declared deadzone and fuzz is its noise estimate. Those two fields are a sensible starting point when you have no measurement of your own, and the input subsystem overview explains how drivers populate them.
Set the outer threshold from measurement, not a guess: deflect the stick fully in several directions during calibration, record the lowest magnitude you achieved, and set the outer deadzone just below that. The calibration docs cover the capture procedure, and the deadzone docs cover the settings.
Which response curve should I use?
The curve reshapes how physical deflection maps to output. All of the common shapes operate on a normalised magnitude already in [0, 1] after deadzone and rescaling, and all of them preserve the endpoints — zero stays zero, one stays one. What changes is everything in between.
| Curve | Formula | Behaviour | Good for |
|---|---|---|---|
| Linear | y = x | Uniform; output matches deflection exactly | Position-like mappings, XY pads, crossfades |
| Exponential | y = xg, g > 1 | Fine near centre, fast at the extremes | Filter sweeps, sends, anything where you dial small amounts often |
| Logarithmic-ish | y = xg, g < 1 | Fast initial response, compressed at the top | Triggers you want to feel immediate, drum velocity |
| S-curve | y = x2(3 - 2x) | Gentle at both ends, steep in the middle | Crossfades and morphs where both extremes want stability |
| Piecewise | Breakpoint-defined | Arbitrary; exactly the feel you specify | Matching a specific hardware control's response |
For continuous MIDI control the exponential family is the workhorse, because it matches how people actually use the control. Most of the time you are making small adjustments near the current position and occasionally you want a large sweep. An exponent above one gives you more physical travel per unit of output in the small-adjustment region and less in the large-sweep region, which is exactly the right allocation.
// Exponential curve preserving sign for bipolar axes
double curve(double v, double gamma) {
double s = (v < 0.0) ? -1.0 : 1.0;
return s * pow(fabs(v), gamma);
}
// gamma 1.0 linear
// gamma 1.5 moderately fine near centre - good default for sweeps
// gamma 2.0 strongly fine near centre - good for precise rides
// gamma 0.7 faster initial response - good for triggers and velocity Note the sign preservation. A bipolar axis runs from -1 to +1, and raising a negative number to a fractional power is undefined. Take the absolute value, curve it, reapply the sign. Forgetting this produces NaNs for half the stick's travel, which manifests as the control working in one direction and dying in the other.
Why does the order matter so much?
Because each operation assumes a specific input domain, and feeding it the wrong domain distorts the result in ways that are hard to diagnose afterwards.
- Normalise. Converts a device-specific integer into a float on a known range, using the Logical Minimum and Logical Maximum from the HID report descriptor. Everything downstream can now be device-independent. The decoding procedure is in HID report descriptors explained.
- Deadzone and rescale. Removes the unusable region and restores full scale. The output is a clean
[0, 1]magnitude representing genuine intent. - Curve. Reshapes that clean range. Applying a curve before the deadzone means curving a range that still contains a gap, which distorts the shape near the bottom in a way that no amount of adjusting the exponent fixes.
- Clamp. Catches corner overshoot and any floating-point excursion before it can wrap at the integer stage.
- Quantise. Once, at the end. Quantising earlier and then curving creates the plateau-and-gap artefact described in analog trigger resolution.
Where does smoothing go? Between normalisation and deadzone, so the filter sees the real signal including the noise it is there to suppress — and so that a well-chosen filter can let you reduce the deadzone, because a quieter signal needs a smaller dead region. The filter choices and their lag characteristics are in stick smoothing filter algorithms.
Do triggers need different treatment from sticks?
Yes, in three ways. Triggers are unipolar, so they run [0, 1] rather than [-1, +1] and there is no centre to protect — a trigger at rest is at one end of its range, not the middle. Axial deadzone is correct for a trigger because there is no second axis to form a direction with.
Second, the outer deadzone matters more. A trigger that cannot report its maximum means you physically cannot reach the top of your parameter, which on a velocity mapping means you can never play loudest. Set it from measurement.
Third, the useful curve shape is often the opposite of a stick's. The DualSense adaptive triggers add force zones that change how pull depth maps to perceived effort, which interacts with your software curve in ways worth experimenting with — we cover that interaction in adaptive trigger velocity curves. Apple exposes the adaptive trigger modes through GCDualSenseAdaptiveTrigger if you want to drive the haptic side programmatically. Note also that several controllers report triggers as digital rather than analog — Switch Pro ZL and ZR are the common example, covered in the Switch Pro Ableton guide — in which case no curve helps, because there are only two values.
What do the common failure modes look like?
| Symptom | Cause | Fix |
|---|---|---|
| Diagonals snap to horizontal or vertical | Axial deadzone on a two-axis stick | Switch to radial on vector magnitude |
| Output never reaches 127 | Deadzone subtracted without rescaling, or no outer deadzone | Divide by (1 - dz); set outer from calibration |
| Corners behave differently from cardinals | Magnitude exceeding 1.0 and being truncated | Clamp the rescaled magnitude to 1.0 |
| Control works one way, dies the other | Fractional power applied to a negative value | Curve the absolute value and reapply the sign |
| Plateaus and jumps on slow sweeps | Curve applied after quantisation | Curve in float, quantise once at the end |
| Still drifting at rest with a big deadzone | Calibration offset, not noise — centre is wrong | Recalibrate centre; the deadzone is treating a symptom |
| Feels sluggish after tuning | Smoothing filter adding lag, not the deadzone | Reduce filter strength or use an adaptive filter |
The sixth row is the one that wastes the most time. A persistent offset and a wandering value look similar in a meter and need completely different fixes. If your resting value is stable but not zero, no deadzone size is the right answer — recalibrate. Stick drift compensation in software covers the distinction in depth.
FAQ
What is a deadzone?
A deadzone is a region near a control's resting position where input is forced to zero, so that sensor noise and a stick that does not return exactly to centre do not produce unwanted output.
What is the difference between axial and radial deadzone?
An axial deadzone applies a threshold to each axis independently and produces a square dead region, while a radial deadzone applies the threshold to the combined vector magnitude and produces a circular one that behaves consistently in every direction.
Why does my stick snap to the cardinal directions?
That is the classic symptom of an axial deadzone, because small movements on one axis are zeroed while the other axis passes through, forcing diagonal input to collapse onto an axis.
Why does my stick never reach the maximum value?
Because the deadzone was subtracted without rescaling the remaining range, so the output ceiling is reduced by exactly the deadzone amount.
What response curve should I use for a filter sweep?
An exponent above one is usually best, because it gives finer resolution near centre where you make small adjustments and faster travel at the extremes where precision matters less.
Should the deadzone be applied before or after the curve?
Before, because the curve should operate on the rescaled usable range rather than on a range that still contains a dead region, otherwise the curve shape is distorted by the gap.
Put it into practice
Set your deadzone to zero, measure your resting spread for a minute, and set the inner deadzone just above it. Then switch the shape to radial and raise the curve exponent to around 1.5 on anything you sweep slowly. Those three changes fix the majority of "this stick feels wrong as a MIDI control" complaints. Download Universal Controller MIDI and make them in the calibration panel.