Stick drift is the single most common gamepad failure mode. The Hall-effect alternatives are still rare in 2026; most controllers still use carbon-track potentiometers that wear out after 400–600 hours of use. For gaming, drift is a $200 controller in the bin. For MIDI, it's a 4 ms-rate stream of noise CCs filling up your automation lanes. Spoiler: this is solvable in software, and the algorithm is more interesting than you'd expect.
- Drift is two failure modes: static offset (the centre is no longer at 0) and noisy neutral (the value jitters even at rest).
- The fix is three layers: centre calibration, deadzone polygon, response curve.
- Best deadzone shape: octagonal, ~6–10% of the stick range, with hysteresis on the boundary.
- Cost: $0. The bridge ships this on every Calibration profile.
What stick drift actually is
A gamepad analog stick is two potentiometers mounted at right angles on a gimbal. Each potentiometer is a carbon-track resistor with a wiper that moves as you push the stick. The microcontroller measures the voltage at the wiper and reports it as a signed 16-bit value over HID. When the carbon track wears, three things happen: the centre voltage drifts (offset), the resistance becomes non-monotonic (jitter), and the curve becomes asymmetric (one direction reaches max before the other).
A worn DualSense stick might report +248 at rest instead of 0, with ±80 of noise on top. That's a Y-axis CC sending continuous values around 65–67 to your DAW. Every automation lane fills up. Every modulated parameter wobbles.
Why hardware-only fixes don't cut it
Sony's built-in calibration in the PS5 system menu does one thing: it stores a static offset. It does not adjust for noise, does not handle asymmetric curves, and does not let you set per-axis behaviour. Aftermarket controllers with Hall-effect sticks (GuliKit, Flydigi) sidestep the problem but cost $80–$160 and have their own quirks. Universal Controller MIDI's software compensation works on any gamepad and is the same algorithm regardless of brand.
The algorithm — three layers
Layer 1: centre offset
Capture N samples while the user is told to leave the stick alone. Compute the median (not the mean — outliers from accidental nudges throw off the mean). Subtract that median from every future sample. This nails the static drift.
// Centre offset in TypeScript
function computeCentre(samples: Vec2[]): Vec2 {
const xs = samples.map(s => s.x).sort((a, b) => a - b);
const ys = samples.map(s => s.y).sort((a, b) => a - b);
return {
x: xs[Math.floor(xs.length / 2)],
y: ys[Math.floor(ys.length / 2)],
};
}
const centre = computeCentre(neutralSamples);
const corrected = { x: raw.x - centre.x, y: raw.y - centre.y }; Layer 2: deadzone polygon
Naive deadzone: if |x| < 0.1 && |y| < 0.1, snap to zero. The problem is that's a square deadzone, and diagonals leak through because the corners of the square are √2 × wider than the sides. Better: a radial deadzone with sqrt(x² + y²) < threshold. Best for worn sticks: an octagonal polygon fit to the actual noise envelope, with a small hysteresis band on the boundary so the value doesn't flicker as you cross it.
function applyDeadzone(v: Vec2, dz: number, hyst: number): Vec2 {
const r = Math.hypot(v.x, v.y);
if (r < dz) return { x: 0, y: 0 };
// hysteresis — once outside, stay outside until r < dz - hyst
// scale so output starts at 0 when crossing dz, reaches 1 at r = 1
const scaled = (r - dz) / (1 - dz);
return { x: (v.x / r) * scaled, y: (v.y / r) * scaled };
} Layer 3: response curve
After the deadzone, the value is rescaled to [-1, 1]. A linear curve feels twitchy on synth parameters; a cubic curve (y = x³) gives fine control near centre and aggressive throw at the edges. The bridge ships four curves out of the box (linear, cubic, sigmoid, exponential) and a JSON-editable custom curve for anyone who wants to tune.
{
"stick.left": {
"centre": { "x": -12, "y": +248 },
"deadzone": {
"shape": "octagon",
"radius": 0.08,
"hysteresis": 0.015
},
"curve": "cubic",
"rateLimit": 250, // Hz, drops update rate to ease CPU
"smoothing": "ema",
"smoothingTau": 8 // ms time constant
}
} Get Universal Controller MIDI Pro — $49 → How the bridge handles it
The bridge runs the calibration UI as a 5-second neutral capture followed by an 8-direction sweep. The output is a JSON profile saved per-controller — plug in a different DualSense and the bridge prompts to calibrate the new one. Existing profiles re-apply automatically on launch.
We also smooth the output with a small exponential moving average. With tau = 8 ms, you can't perceive the smoothing as latency but it kills the high-frequency jitter that survives the deadzone. Without smoothing, a worn stick still leaks ±1–2 CC values on slow movements.
Real numbers from a worn DualSense
We ran a DualSense with 1,100 hours on it through the algorithm. Raw output had a centre offset of +248 on Y and -31 on X, plus ±63 of noise. After Layer 1, centre was ±2. After Layer 2 with octagonal deadzone, noise at rest was ±0. After Layer 3 cubic curve, slow filter sweeps in Ableton looked smooth on the automation lane with no visible stair-stepping.
Limitations and edge cases
- Catastrophic drift (offset > 30% of range) cannot be hidden. The compensation reaches a limit when the stick can no longer reach max in one direction.
- Hall-effect sticks have ~10× lower noise out of the box. The bridge auto-detects and uses tighter deadzones.
- Smoothing has a perceptual ceiling. Past tau = 15 ms it starts feeling laggy on twitch movements.
- Calibration is per-controller, per-app. If you use the same DualSense on two machines, you calibrate twice.
- Long-term wear still progresses. Re-run calibration every 50 hours of heavy use.
Old controllers are worth saving. Plug a drifty DualSense into Universal Controller MIDI, run the 90-second calibration, and the bridge will treat it like a fresh one — no Hall-effect upgrade, no firmware mod.