There are at least three different resolutions in the path between your finger and a filter cutoff, and they are rarely the same number. The trigger's analog-to-digital converter has one. The MIDI message format has another. The receiving plugin has a third. Where those disagree, the smallest one wins — and if you do not know which is smallest, you cannot know whether your stepped-sounding sweep is the controller's fault, the protocol's fault, or the plugin's.
This post traces each limit back to its specification, explains how MIDI 1.0's 14-bit CC convention works and where it came from, and gives a clear rule for when sending twice the data is worth it.
- MIDI 1.0 CC carries 7 bits — 128 steps — per the MIDI Association's Control Change reference.
- 14-bit CC is a pairing convention, not a different message type: CC 0-31 carry the MSB, CC 32-63 the matching LSB, giving 16,384 steps.
- Your trigger's resolution is in the HID descriptor, in the Logical Maximum field — go and read it rather than assuming.
- The destination decides whether it is worth it. A plugin that only reads 7 bits ignores your LSB entirely.
- Curve before quantising. Applying a curve to an already-quantised value creates plateaus you can hear.
Why does MIDI Control Change only have 128 steps?
Because MIDI 1.0 reserves the high bit of every byte to distinguish status bytes from data bytes. A status byte has its most significant bit set; a data byte has it clear. That single design decision — which is what makes the stream self-synchronising, so a receiver can recover from a dropped byte — leaves seven bits for actual data, and seven bits is 128 values. The message structure is set out in the summary of MIDI 1.0 messages and in the MIDI 1.0 core specification.
A Control Change message is therefore three bytes: a status byte carrying the message type and channel, a data byte carrying the controller number, and a data byte carrying the value. All three are constrained by the same seven-bit rule, which is also why there are 128 controller numbers and 128 note numbers. The symmetry is not a coincidence; it is the same constraint applied everywhere.
128 steps is plenty for a lot of things and obviously insufficient for others. Switching a drum sample is one of 128. Sweeping a filter across eight bars is not — with 128 steps spread over a long gesture, each individual step transition becomes slow enough to hear as a discrete event. That audible stepping is sometimes called zipper noise, and it is the single most common complaint about using analog controls over MIDI 1.0.
How does 14-bit CC work?
The MIDI 1.0 specification anticipated this and built in a pairing convention. Controller numbers 0 through 31 are defined to carry the most significant byte of a high-resolution value, and controller numbers 32 through 63 carry the corresponding least significant byte — each LSB controller is exactly 32 above its MSB partner. The full assignment is in the Control Change message reference.
| MSB controller | LSB controller | Common name |
|---|---|---|
| CC 1 | CC 33 | Modulation wheel |
| CC 2 | CC 34 | Breath controller |
| CC 4 | CC 36 | Foot controller |
| CC 7 | CC 39 | Channel volume |
| CC 11 | CC 43 | Expression |
| CC 16-19 | CC 48-51 | General purpose 1-4 |
Two seven-bit values concatenated give fourteen bits, which is 16,384 steps — a 128-fold increase. The assembly is straightforward:
// Normalised 0.0 .. 1.0 -> 14-bit MIDI pair
uint16_t v14 = (uint16_t)lround(normalised * 16383.0);
uint8_t msb = (v14 >> 7) & 0x7F; // top 7 bits
uint8_t lsb = v14 & 0x7F; // bottom 7 bits
// Send MSB first, then LSB. Order matters to receivers.
midi_send_cc(channel, cc_msb, msb);
midi_send_cc(channel, cc_msb + 32, lsb);
// Receiver side
uint16_t assembled = ((uint16_t)msb << 7) | lsb; // 0 .. 16383 Order matters because the conventional receiver behaviour is to treat an incoming MSB as a new value with the LSB reset or held, then refine it when the LSB arrives. Sending the LSB first, or sending a lone LSB with no preceding MSB, produces results that vary by implementation and are reliably confusing. Send them as a pair, MSB first, always.
What resolution does my trigger actually have?
Read the HID report descriptor and look at the Logical Maximum for the trigger axis. That field, defined in the USB HID 1.11 class definition, is the device's own statement of its range, and it is the only honest answer. The procedure for dumping and decoding it is in HID report descriptors explained.
If you are going through a platform abstraction instead of raw HID, the API documents its own range. XInput's XINPUT_GAMEPAD structure defines triggers as a byte and thumbsticks as a signed short, which is 8 bits for triggers and 16 for sticks — note that those are the API's representation, not necessarily the hardware's. Apple's GCControllerButtonInput hands you a normalised float, which deliberately abstracts away the underlying integer entirely. Web contexts get normalised doubles via the W3C Gamepad API.
This is why the question "is my trigger 8-bit?" has to be answered per device and per API, not in general. An abstraction handing you a float may be presenting an 8-bit underlying value with many decimal places of false precision, and no amount of 14-bit MIDI will recover information that was never there.
When is 14-bit actually worth sending?
When all three of these are true. If any one fails, you are sending twice the bytes to express exactly the same information.
- The source has more than 128 distinct values. An 8-bit trigger has 256, so there is one bit of genuine extra information. A 16-bit stick has far more. A device reporting only 128 values has nothing to add.
- The destination accepts 14-bit pairs. Many plugins and most DAW mapping layers read the MSB and ignore the LSB. Check before assuming.
- The gesture is slow enough for stepping to be audible. A fast trigger stab through the whole range passes every step in milliseconds and nobody hears quantisation. An eight-bar filter rise does.
| Use case | 7-bit enough? | Why |
|---|---|---|
| Trigger as note velocity | Yes | MIDI velocity is 7-bit by definition; there is nowhere to put more |
| Fast filter stab | Yes | Steps pass too quickly to resolve individually |
| Drum choke or mute switch | Yes | Binary in practice |
| Eight-bar filter rise | No | Slow traversal makes each of the 128 transitions audible |
| Slow crossfade between layers | No | Amplitude stepping is especially audible |
| Fine pitch or detune | No | 128 steps across a pitch range is coarse enough to be musically wrong |
| Wavetable position sweep | Depends | Depends entirely on how many frames the table has |
The practical workflow this suggests is per-mapping rather than global. Put your slow, continuous, exposed controls on 14-bit pairs; leave everything else on single 7-bit CCs. A bridge that forces one choice for the whole device is making you pay for resolution on controls that cannot use it. Configure the per-control choice in trigger to CC mapping and stick to CC mapping.
What does doubling the message count cost?
On a USB or virtual-port chain, bandwidth is rarely the issue — the USB Device Class Definition for MIDI Devices packetises MIDI into fixed-size USB-MIDI event packets carried at USB speeds, and a modern machine is not troubled by it.
On a 5-pin DIN cable it absolutely is. MIDI 1.0 specifies a serial rate of 31,250 bits per second, and with the standard framing of one start bit, eight data bits and one stop bit, each byte occupies ten bit periods. A three-byte Control Change message therefore occupies thirty bit periods — roughly a millisecond at that rate. Two such messages per value update, on two axes, at a high update rate, saturates the link quickly, and a saturated link means queued messages, and queued messages are late. That arithmetic and its consequences are worked through in MIDI's 31,250 baud explained.
There is a cheap and effective optimisation here. During fine movement, the MSB frequently does not change while the LSB does — that is the whole point of having an LSB. Tracking them separately and only sending what changed removes a large fraction of the traffic at no cost in fidelity:
static uint8_t last_msb[128], last_lsb[128];
void send_14bit(int ch, int cc, double normalised) {
uint16_t v = (uint16_t)lround(normalised * 16383.0);
uint8_t msb = (v >> 7) & 0x7F;
uint8_t lsb = v & 0x7F;
if (msb == last_msb[cc] && lsb == last_lsb[cc]) return; // nothing new
if (msb != last_msb[cc]) {
midi_send_cc(ch, cc, msb); // MSB only when it genuinely moved
last_msb[cc] = msb;
}
midi_send_cc(ch, cc + 32, lsb);
last_lsb[cc] = lsb;
} Be aware of one receiver behaviour before adopting this wholesale: some implementations reset their internal LSB to zero whenever an MSB arrives. Against such a receiver, suppressing the MSB is correct and helpful, but you must send the MSB-then-LSB pair whenever the MSB does change, or you will produce a momentary jump. The code above does exactly that.
Why does curving after quantisation sound worse?
Because quantisation discards information and a curve then redistributes what is left unevenly. Apply a curve to a 7-bit value and the output still has at most 128 distinct levels, but they are no longer evenly spaced — regions where the curve is shallow map multiple input steps onto the same output step, creating plateaus, and regions where it is steep skip output values entirely, creating gaps. You hear plateaus as the control "sticking" and gaps as jumps.
Doing it in the right order costs nothing and fixes it completely:
// WRONG - curve applied to an already-lossy value
uint8_t cc = (uint8_t)(raw * 127.0 / 255.0);
uint8_t out = curve_table_7bit[cc]; // 128 in, 128 out, unevenly
// RIGHT - full precision through the curve, quantise once at the end
double t = (double)raw / 255.0; // keep every bit you were given
double shaped = pow(t, 1.6); // exponential-ish trigger feel
uint16_t v14 = (uint16_t)lround(shaped * 16383.0);
uint8_t msb = (v14 >> 7) & 0x7F;
uint8_t lsb = v14 & 0x7F; The rule generalises beyond curves: do deadzone, curve, smoothing, scaling and any combination of them in floating point, and convert to MIDI integers exactly once, at the output. The deadzone and curve shapes themselves are covered in deadzone and response curve mathematics, and the filtering stage in stick smoothing filter algorithms.
What are the alternatives to 14-bit CC?
| Approach | Resolution | Support | Good for |
|---|---|---|---|
| 7-bit CC | 128 steps | Universal | Almost everything; the sensible default |
| 14-bit CC pair | 16,384 steps | Common but not universal | Slow, exposed continuous controls |
| Pitch Bend | 14-bit, natively one message | Universal | Anything you can route from bend; one per channel |
| NRPN | 14-bit, addressed by a 14-bit parameter number | Common on hardware synths | Deep parameter access on hardware |
| MPE | Per-note expression over multiple channels | Growing | Polyphonic per-note control |
| MIDI 2.0 | High-resolution CC in Universal MIDI Packets | Emerging | The long-term answer |
Pitch Bend deserves special mention because it is 14-bit natively, in a single three-byte message rather than a pair, and support for it is genuinely universal. If the parameter you want to control can be driven from pitch bend in your host, that is the cheapest high-resolution route available — one message instead of two, and no receiver ambiguity.
Longer term, MIDI 2.0 removes the seven-bit constraint by abandoning the self-synchronising byte structure entirely in favour of the Universal MIDI Packet, which carries far wider value fields. It requires both ends to support it, so it is not a switch you can flip unilaterally, but it is where this problem stops existing. We cover the gamepad angle in MIDI 2.0 and gamepads and the expression comparison in MPE versus MIDI 2.0.
How do I fall back gracefully?
Send the MSB alone. This is the quietly elegant part of the MIDI 1.0 design: a well-formed 14-bit MSB is also a perfectly valid 7-bit CC value for the same controller number. A receiver that understands pairs assembles the full value; a receiver that does not sees a sensible 7-bit control and behaves correctly. There is no error case and no configuration flag needed.
The one thing to avoid is sending an LSB to a receiver that does not understand pairing, because it will interpret CC 33 as an unrelated control and may map it to something you did not intend. If you are unsure of the destination, start with MSB-only, confirm the control works, then enable the LSB and confirm it improves rather than breaks. That two-step check takes thirty seconds and saves a confusing afternoon.
FAQ
How many steps does a standard MIDI CC have?
A standard MIDI 1.0 Control Change message carries a 7-bit value, which gives 128 discrete steps from 0 to 127.
What is 14-bit MIDI CC?
It is a MIDI 1.0 convention where two Control Change messages are paired, one carrying the most significant seven bits and one the least significant seven bits, giving 16,384 steps for a single control.
Which CC numbers pair together for 14-bit?
Controller numbers 0 through 31 carry the most significant byte and controller numbers 32 through 63 carry the corresponding least significant byte, so CC 1 pairs with CC 33.
Do I need 14-bit CC for a gamepad trigger?
Only if the trigger reports more than 128 distinct values and the destination can actually use them, because otherwise you are sending twice the data to express the same information.
Why does my slow filter sweep sound stepped?
Because a 7-bit CC has 128 steps spread across the full parameter range, and a slow sweep moves through those steps slowly enough for each transition to be individually audible.
Does MIDI 2.0 remove the 7-bit limit?
Yes, MIDI 2.0 carries Control Change values in Universal MIDI Packets with far greater resolution, but it requires both ends of the chain to support it rather than being something you can enable unilaterally.
Put it into practice
Pick the one control in your current project that sounds stepped on a slow move — usually a filter or a crossfade — and put that single mapping on a 14-bit pair. Leave everything else on 7-bit. If the stepping disappears, you have found the one place the extra bytes were worth it. Download Universal Controller MIDI and set the resolution per control in the mapping editor rather than globally.