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
}
Summary
The
wav.Decodefunction inbeep/wav/decode.goincorrectly 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):
65535(unsigned 16-bit max)int16range is[-32768, 32767][-0.5, 0.5]instead of[-1.0, 1.0]24-bit PCM (current code):
[-8388608, 8323072]16777215(unsigned 24-bit max)[-0.5, 0.496]instead of[-1.0, 1.0]Expected Behavior
Normalized float64 samples should range from
-1.0to1.0for all bit depths.Actual Behavior
-1.0to1.0)-0.5to0.5❌-0.5to0.496❌Reproduction
Proposed Fix
For 16-bit:
Divide by
32768instead of65535.For 24-bit:
Divide by
8388608instead of16777215.Impact
Any application using beep to decode 16-bit or 24-bit WAV files will experience:
Workaround
Multiply all decoded samples by 2.0 after calling
Stream():