Skip to content

WAV Decoder Normalizes 16-bit and 24-bit Audio to Half Amplitude #176

@martinarisk

Description

@martinarisk

Summary

The wav.Decode function in beep/wav/decode.go incorrectly normalizes 16-bit and 24-bit PCM audio samples, resulting in audio that plays at approximately 50% of the correct amplitude.

Root Cause

The decoder divides signed PCM values by the unsigned maximum value instead of the signed maximum absolute value:

16-bit PCM (current code):

val := float64(int16(p[i+0])+int16(p[i+1])*(1<<8)) / (1<<16 - 1)
  • Divides by 65535 (unsigned 16-bit max)
  • But int16 range is [-32768, 32767]
  • Result: [-0.5, 0.5] instead of [-1.0, 1.0]

24-bit PCM (current code):

val := float64((int32(p[i+0])<<8)+(int32(p[i+1])<<16)+(int32(p[i+2])<<24)) / (1 << 8) / (1<<24 - 1)
  • After bit shifting and dividing by 256, the range is approximately [-8388608, 8323072]
  • Then divides by 16777215 (unsigned 24-bit max)
  • Result: approximately [-0.5, 0.496] instead of [-1.0, 1.0]

Expected Behavior

Normalized float64 samples should range from -1.0 to 1.0 for all bit depths.

Actual Behavior

  • 8-bit: Correct (ranges from -1.0 to 1.0)
  • 16-bit: Ranges from -0.5 to 0.5
  • 24-bit: Ranges from approximately -0.5 to 0.496

Reproduction

package main

import (
    "fmt"
    "math"
    "github.com/faiface/beep/wav"
    "os"
)

func main() {
    // Create a 16-bit mono WAV with max amplitude
    // (use any audio tool to generate a 0dBFS sine wave)
    
    file, _ := os.Open("test_16bit.wav")
    defer file.Close()
    
    stream, format, _ := wav.Decode(file)
    
    samples := make([][2]float64, 1024)
    maxAmp := 0.0
    
    for {
        n, ok := stream.Stream(samples)
        if !ok {
            break
        }
        for i := 0; i < n; i++ {
            if math.Abs(samples[i][0]) > maxAmp {
                maxAmp = math.Abs(samples[i][0])
            }
        }
    }
    
    fmt.Printf("Format: %d-bit, %d Hz\n", format.Precision*8, format.SampleRate)
    fmt.Printf("Max amplitude: %.6f (expected: ~1.0, got: ~0.5)\n", maxAmp)
}

Proposed Fix

For 16-bit:

val := float64(int16(p[i+0])+int16(p[i+1])*(1<<8)) / (1<<15)

Divide by 32768 instead of 65535.

For 24-bit:

val := float64((int32(p[i+0])<<8)+(int32(p[i+1])<<16)+(int32(p[i+2])<<24)) / (1 << 8) / (1<<23)

Divide by 8388608 instead of 16777215.

Impact

Any application using beep to decode 16-bit or 24-bit WAV files will experience:

  • Audio playing at half volume
  • Incorrect RMS/peak measurements
  • Potential clipping when amplifying to correct levels
  • Incompatibility with other audio libraries that normalize correctly

Workaround

Multiply all decoded samples by 2.0 after calling Stream():

n, ok := stream.Stream(samples)
for i := 0; i < n; i++ {
    samples[i][0] *= 2.0
    samples[i][1] *= 2.0
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions