note

MIDI Maths

Because MIDI values are typically 7-bit (0–127) or 14-bit (0–16383), it is useful to have some math formulas to scale, normalize, constrain, and manipulate this data. Here are the most common math functions used to transform MIDI messages.

1. Scaling & Normalisation

Raw controller inputs, like a Mod Wheel (CC 1), output values from 0 to 127. This range often needs to be mapped to a different parameter, for example a filter cutoff frequency from 200 Hz to 8000 Hz, or a normalised 0.0–1.0 float. Formula:

function scale(value, inMin, inMax, outMin, outMax) {
   if (inMax === inMin) return outMin;
   return outMin + (value - inMin) * (outMax - outMin) / (inMax - inMin);
}

// Example: map a CC value (0–127) to a frequency range (200–8000 Hz)
const freq = scale(ccValue, 0, 127, 200, 8000);

The guard if (inMax === inMin) return outMin; catches the case where the input range has zero width (i.e. inMax - inMin = 0), which would produce a division by zero. Returning outMin is a sensible fallback. The input is a single point, so the output collapses to the start of the output range.

Use case: Converts a raw Control Change (CC) value to real-world hardware or software parameters such as filter cutoff, LFO rate, or reverb depth.

2. Modulo

The modulo function returns the remainder of a division. In music theory it is essential for pitch class mapping: reducing any MIDI note number to the 12 chromatic pitch classes regardless of octave.

Formula:

const pitchClass = noteNumber % 12;
// Note 60 (Middle C) -> 60 % 12 = 0  (C)
// Note 64 (E4)       -> 64 % 12 = 4  (E)
// Note 71 (B4)       -> 71 % 12 = 11 (B)

Use case: Extracts the pitch class of any MIDI note number, independently of octave. Note 60 (Middle C) gives 0, which correctly maps to the C pitch class.

Modulo is also used to wrap LFO phases, cycle through chord voicings, or compute beat positions within a bar.

3. Bitwise Operators

Standard MIDI bytes are 8-bit sequences with one bit reserved for the status flag, so data bytes carry only 7 bits of information. Bitwise math enables highly efficient processing and data packing.

Bitwise AND (&)

Used for masking values to a fixed number of bits.

// Extract the lower 7 bits (equivalent to mod 128)
const lsb = value & 0x7F;

// Extract the MIDI channel from a status byte
const channel = statusByte & 0x0F;   // lower nibble -> channel 0–15

// Extract the message type from a status byte
const type = statusByte & 0xF0;      // upper nibble -> e.g. 0x90 = Note On

Bitwise Shift (>> and <<)

Used to split high-resolution data (like 14-bit pitch bend) into separate Most Significant Byte (MSB) and Least Significant Byte (LSB).

// Split a 14-bit pitch bend value into two 7-bit bytes
const msb = (pitchBend >> 7) & 0x7F;  // high 7 bits
const lsb =  pitchBend       & 0x7F;  // low  7 bits

// Reassemble from MSB + LSB
const pitchBend = (msb << 7) | lsb;

4. Exponential / Logarithmic Curves

Human hearing is logarithmic: a linear increase in MIDI volume (CC 7) does not sound like a linear increase in loudness. Applying a power or logarithmic curve to velocity or expression data produces more natural, musical dynamics.

Formula (power curve):

where is the curve exponent (γ > 1 → concave, softer response; γ < 1 → convex, more aggressive).

function applyCurve(value, gamma = 2.0) {
return Math.pow(value / 127, gamma);
}

// Gentle logarithmic-style response (gamma > 1)
const expression = applyCurve(ccValue, 2.0);

// Aggressive response (gamma < 1)
const brightness = applyCurve(ccValue, 0.5);

Use case: Applies logarithmic or exponential curves to velocity or expression data to emulate natural instrument dynamics and produce smooth MIDI fades that feel musical.

5. Clamping / Constraints

MIDI data can easily “clip” if it exceeds the bounds of its designated byte range. This can cause hardware bugs, undesirable pitch jumps, or malformed MIDI messages.

Formula:

// Clamp to 7-bit MIDI range (0–127)
function clamp7(value) {
return Math.max(0, Math.min(127, value));
}

// Clamp to 14-bit range (0–16383), e.g. for pitch bend or NRPN values
function clamp14(value) {
return Math.max(0, Math.min(16383, value));
}

Use case: Keeps manipulated data firmly within valid MIDI bounds, 0127 for standard 7-bit messages, or 016383 for 14-bit messages (pitch bend, NRPN), before transmitting.

6. MIDI Note Number to Frequency

Synthesisers constantly convert raw MIDI note integers (a linear scale) to hertz (a logarithmic frequency scale). The standard formula is based on equal temperament with A4 = 440 Hz as the reference pitch (MIDI note 69).

Formula:

where is the MIDI note number.

function midiNoteToFrequency(note, ref = 440) {
return ref * Math.pow(2, (note - 69) / 12);
}

// Middle C (note 60) -> 261.63 Hz
// A4      (note 69) -> 440.00 Hz
// A5      (note 81) -> 880.00 Hz

The inverse, converting a frequency back to a (possibly fractional) MIDI note number, uses the logarithm:

const A4 = 69;

function frequencyToMidiFloat(freq, ref = 440) {
const note = A4 + 12 * Math.log(freq / ref) / Math.log(2);
// Rounded to nearest 32-bit float to avoid floating-point drift
// and return whole semitone numbers where possible.
return Math.fround(note);
}

// 261.63 Hz -> ~60.0 (Middle C)
// 466.16 Hz -> ~70.0 (A#4 / Bb4)
// 450.00 Hz -> ~69.8 (slightly sharp of A4)

Use case: Determines the exact playback frequency for any given MIDI note, or recovers a fractional note number from a measured frequency for microtonal or tuning applications.

Quick Reference

OperationFormulaJavaScript
Scale valueoutMin + (v - inMin) * range / inRange
Pitch classn % 12
Extract 7-bit bytev & 0x7F
High byte of 14-bit(v >> 7) & 0x7F
Power curveMath.pow(v / 127, gamma)
Clamp 7-bitMath.max(0, Math.min(127, v))
Note → Hz440 * Math.pow(2, (n - 69) / 12)
Hz → note69 + 12 * Math.log(f / 440) / Math.log(2)