Skip to content

Instantly share code, notes, and snippets.

@alileza
Created December 14, 2025 08:15
Show Gist options
  • Select an option

  • Save alileza/6db9cc7866c5e2f66f63623446e42319 to your computer and use it in GitHub Desktop.

Select an option

Save alileza/6db9cc7866c5e2f66f63623446e42319 to your computer and use it in GitHub Desktop.

Deep Dive: μ-law (Mulaw) Audio Encoding

μ-law is a companding algorithm used in telephony (North America, Japan). It compresses 14-bit linear PCM into 8-bit samples, giving better quality for quiet sounds at the expense of loud sounds.

Mulaw Byte Structure:

Bit:    7    6    5    4    3    2    1    0
      +----+----+----+----+----+----+----+----+
      | S  |    Exponent   |     Mantissa     |
      +----+----+----+----+----+----+----+----+

S = Sign bit (0 = positive, 1 = negative)
Exponent = 3 bits (0-7)
Mantissa = 4 bits (0-15)

Decoding Formula:

sample = ((mantissa << 3) + bias) << exponent
sample = sample - bias
if (sign_bit) sample = -sample

The Bias Value Problem:

The bias value is critical for correct decoding:

Bias Value Hex What Happens
33 (wrong) 0x21 Samples decoded ~4x too quiet, then shifted incorrectly
132 (correct) 0x84 Proper dynamic range, correct audio

Why 132 (0x84)?

The μ-law standard uses bias = 132 because:

  1. It provides the optimal quantization step sizes
  2. Matches the encoder's bias value
  3. Results in proper 14-bit PCM reconstruction

Visual: Wrong vs Correct Decoding

Original audio wave:     ~~~~∿∿∿~~~~

With bias=33 (wrong):    _-_-_-_-_-_   (clipped, distorted)

With bias=132 (correct): ~~~~∿∿∿~~~~   (matches original)

The Masking Problem:

JavaScript's bitwise NOT (~) operates on 32-bit integers:

// Without masking
~0xAB = 0xFFFFFF54  // 32-bit result, wrong!

// With masking
(~0xAB) & 0xFF = 0x54  // 8-bit result, correct!

Complete Correct Implementation:

function mulawToPcm(mulawData) {
    const pcmData = new Float32Array(mulawData.length);

    for (let i = 0; i < mulawData.length; i++) {
        // Step 1: Invert bits and mask to 8-bit
        let mulaw = (~mulawData[i]) & 0xFF;

        // Step 2: Extract sign (bit 7)
        let sign = (mulaw & 0x80) ? -1 : 1;

        // Step 3: Extract exponent (bits 4-6)
        let exponent = (mulaw >> 4) & 0x07;

        // Step 4: Extract mantissa (bits 0-3)
        let mantissa = mulaw & 0x0F;

        // Step 5: Decode with correct bias (0x84 = 132)
        let sample = ((mantissa << 3) + 0x84) << exponent;
        sample -= 0x84;

        // Step 6: Apply sign and normalize to [-1, 1]
        pcmData[i] = (sign * sample) / 32768.0;
    }

    return pcmData;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment