Files
go-sip/internal/audio/wav.go
T

277 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package audio
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
goaudio "github.com/go-audio/audio"
wavcodec "github.com/go-audio/wav"
)
const (
minWAVSampleRate = 8000
maxWAVSampleRate = 48000
wavPCMFormat = 1
)
type wavPCMLayout struct {
format uint16
channels uint16
sampleRate uint32
byteRate uint32
blockAlign uint16
bitDepth uint16
dataOffset int
dataSize int
riffEnd int
}
// DecodeWAVToPCM16Mono decodes integer PCM RIFF/WAVE audio and normalizes it to
// mono PCM16 at targetRate. WAV metadata is validated before the decoder reads
// sample data so truncated chunks cannot be mistaken for completed audio.
func DecodeWAVToPCM16Mono(encoded []byte, targetRate, maxAudioBytes int) ([]byte, int, error) {
if len(encoded) == 0 {
return nil, 0, errors.New("WAV audio is empty")
}
if maxAudioBytes <= 0 || len(encoded) > maxAudioBytes {
return nil, 0, errors.New("WAV audio exceeds the configured bound")
}
if targetRate < minWAVSampleRate || targetRate > maxWAVSampleRate {
return nil, 0, errors.New("WAV target sample rate is outside the supported 8000–48000 Hz range")
}
layout, err := inspectWAVPCMLayout(encoded)
if err != nil {
return nil, 0, err
}
decoder := wavcodec.NewDecoder(bytes.NewReader(encoded))
decoder.ReadInfo()
if err := decoder.Err(); err != nil {
return nil, 0, fmt.Errorf("read WAV format: %w", err)
}
if decoder.WavAudioFormat != layout.format || decoder.NumChans != layout.channels || decoder.SampleRate != layout.sampleRate || decoder.BitDepth != layout.bitDepth {
return nil, 0, errors.New("WAV decoder metadata does not match the RIFF header")
}
if layout.format != wavPCMFormat {
return nil, 0, fmt.Errorf("unsupported WAV audio format %d", layout.format)
}
if layout.channels < 1 || layout.channels > 2 {
return nil, 0, fmt.Errorf("unsupported WAV channel count %d", layout.channels)
}
if layout.sampleRate < minWAVSampleRate || layout.sampleRate > maxWAVSampleRate {
return nil, 0, fmt.Errorf("WAV sample rate %d is outside the supported 8000–48000 Hz range", layout.sampleRate)
}
bytesPerSample := int(layout.bitDepth / 8)
if bytesPerSample == 0 || layout.blockAlign != layout.channels*uint16(bytesPerSample) || layout.byteRate != layout.sampleRate*uint32(layout.blockAlign) {
return nil, 0, errors.New("WAV block alignment does not match its PCM format")
}
if layout.bitDepth != 8 && layout.bitDepth != 16 && layout.bitDepth != 24 && layout.bitDepth != 32 {
return nil, 0, fmt.Errorf("unsupported WAV PCM bit depth %d", layout.bitDepth)
}
if layout.dataSize == 0 || layout.dataSize%int(layout.blockAlign) != 0 {
return nil, 0, errors.New("WAV data chunk contains no complete PCM frames")
}
if err := decoder.FwdToPCM(); err != nil {
return nil, 0, fmt.Errorf("locate WAV data chunk: %w", err)
}
if err := decoder.Err(); err != nil {
return nil, 0, fmt.Errorf("read WAV data chunk: %w", err)
}
if decoder.PCMChunk == nil {
return nil, 0, errors.New("WAV data chunk is unavailable")
}
dataOffset, err := decoder.Seek(0, io.SeekCurrent)
if err != nil {
return nil, 0, fmt.Errorf("read WAV data offset: %w", err)
}
paddedDataSize := layout.dataSize + (layout.dataSize & 1)
if dataOffset != int64(layout.dataOffset) || decoder.PCMSize != paddedDataSize || int64(layout.dataOffset)+int64(layout.dataSize) > int64(layout.riffEnd) {
return nil, 0, errors.New("WAV decoder data boundaries do not match the RIFF chunks")
}
expectedSamples := layout.dataSize / bytesPerSample
frames := expectedSamples / int(layout.channels)
outputSamples := int64(frames) * int64(targetRate) / int64(layout.sampleRate)
if outputSamples <= 0 {
return nil, 0, errors.New("WAV conversion produced no complete PCM16 samples")
}
if outputSamples > int64(maxAudioBytes/2) {
return nil, 0, errors.New("converted WAV PCM exceeds the configured audio bound")
}
buffer := &goaudio.IntBuffer{Data: make([]int, 4096)}
mono := make([]byte, 0, frames*2)
decodedSamples := 0
for {
n, err := decoder.PCMBuffer(buffer)
if err != nil {
return nil, 0, fmt.Errorf("decode WAV PCM samples: %w", err)
}
if n == 0 {
break
}
if n < 0 || n > len(buffer.Data) {
return nil, 0, errors.New("WAV decoder returned an invalid sample count")
}
remaining := expectedSamples - decodedSamples
if n > remaining {
extra := n - remaining
if extra != 1 || layout.dataSize%2 == 0 {
return nil, 0, errors.New("WAV decoder returned samples outside the data chunk")
}
n = remaining
}
if n%int(layout.channels) != 0 {
return nil, 0, errors.New("WAV data ends with an incomplete audio frame")
}
for i := 0; i < n; i += int(layout.channels) {
left, err := wavSampleToPCM16(buffer.Data[i], layout.bitDepth)
if err != nil {
return nil, 0, err
}
monoSample := left
if layout.channels == 2 {
right, err := wavSampleToPCM16(buffer.Data[i+1], layout.bitDepth)
if err != nil {
return nil, 0, err
}
monoSample = int16((int32(left) + int32(right)) / 2)
}
var sample [2]byte
binary.LittleEndian.PutUint16(sample[:], uint16(monoSample))
mono = append(mono, sample[:]...)
}
decodedSamples += n
}
if decodedSamples != expectedSamples {
return nil, 0, fmt.Errorf("WAV data is incomplete: decoded %d of %d samples", decodedSamples, expectedSamples)
}
pcm16 := ResamplePCM16(mono, int(layout.sampleRate), targetRate)
if len(pcm16) == 0 || len(pcm16)%2 != 0 || len(pcm16) > maxAudioBytes {
return nil, 0, errors.New("converted WAV PCM is empty, incomplete or oversized")
}
return pcm16, int(layout.sampleRate), nil
}
type wavLengthMismatchError struct {
riffDeclaredBytes uint64
receivedBytes uint64
dataChunkDeclaredBytes uint64
dataChunkAvailableBytes uint64
hasDataChunk bool
}
func (e *wavLengthMismatchError) Error() string {
if e.hasDataChunk {
return fmt.Sprintf("RIFF/WAVE length invalid or truncated: RIFF declared %d bytes, received %d bytes; data chunk declared %d bytes, available %d bytes", e.riffDeclaredBytes, e.receivedBytes, e.dataChunkDeclaredBytes, e.dataChunkAvailableBytes)
}
return fmt.Sprintf("RIFF/WAVE length invalid or truncated: RIFF declared %d bytes, received %d bytes; data chunk size unavailable", e.riffDeclaredBytes, e.receivedBytes)
}
func wavLengthMismatch(encoded []byte, riffDeclaredBytes uint64) error {
mismatch := &wavLengthMismatchError{
riffDeclaredBytes: riffDeclaredBytes,
receivedBytes: uint64(len(encoded)),
}
for offset := 12; offset+8 <= len(encoded); {
chunkID := string(encoded[offset : offset+4])
chunkSize := uint64(binary.LittleEndian.Uint32(encoded[offset+4 : offset+8]))
chunkStart := uint64(offset + 8)
if chunkID == "data" {
mismatch.hasDataChunk = true
mismatch.dataChunkDeclaredBytes = chunkSize
available := uint64(len(encoded)) - chunkStart
if available > chunkSize {
available = chunkSize
}
mismatch.dataChunkAvailableBytes = available
return mismatch
}
chunkEnd := chunkStart + chunkSize
if chunkEnd < chunkStart || chunkEnd > uint64(len(encoded)) {
return mismatch
}
paddedEnd := chunkEnd + (chunkSize & 1)
if paddedEnd > uint64(len(encoded)) {
return mismatch
}
offset = int(paddedEnd)
}
return mismatch
}
func inspectWAVPCMLayout(encoded []byte) (wavPCMLayout, error) {
if len(encoded) < 12 || string(encoded[:4]) != "RIFF" || string(encoded[8:12]) != "WAVE" {
return wavPCMLayout{}, errors.New("audio is not a RIFF/WAVE file")
}
riffEnd64 := uint64(binary.LittleEndian.Uint32(encoded[4:8])) + 8
if riffEnd64 < 12 {
return wavPCMLayout{}, errors.New("RIFF/WAVE length is invalid or truncated")
}
if riffEnd64 > uint64(len(encoded)) {
return wavPCMLayout{}, wavLengthMismatch(encoded, riffEnd64)
}
layout := wavPCMLayout{dataOffset: -1, riffEnd: int(riffEnd64)}
var sawFormat bool
for offset := 12; offset < layout.riffEnd; {
if layout.riffEnd-offset < 8 {
return wavPCMLayout{}, errors.New("RIFF/WAVE contains a truncated chunk header")
}
chunkID := string(encoded[offset : offset+4])
chunkSize := uint64(binary.LittleEndian.Uint32(encoded[offset+4 : offset+8]))
chunkStart := uint64(offset + 8)
chunkEnd := chunkStart + chunkSize
if chunkEnd > uint64(layout.riffEnd) {
return wavPCMLayout{}, errors.New("RIFF/WAVE chunk exceeds the declared file length")
}
switch chunkID {
case "fmt ":
if sawFormat || chunkSize < 16 {
return wavPCMLayout{}, errors.New("RIFF/WAVE format chunk is missing or invalid")
}
layout.format = binary.LittleEndian.Uint16(encoded[chunkStart : chunkStart+2])
layout.channels = binary.LittleEndian.Uint16(encoded[chunkStart+2 : chunkStart+4])
layout.sampleRate = binary.LittleEndian.Uint32(encoded[chunkStart+4 : chunkStart+8])
layout.byteRate = binary.LittleEndian.Uint32(encoded[chunkStart+8 : chunkStart+12])
layout.blockAlign = binary.LittleEndian.Uint16(encoded[chunkStart+12 : chunkStart+14])
layout.bitDepth = binary.LittleEndian.Uint16(encoded[chunkStart+14 : chunkStart+16])
sawFormat = true
case "data":
if !sawFormat || layout.dataOffset >= 0 {
return wavPCMLayout{}, errors.New("RIFF/WAVE data chunk is missing, duplicated or invalid")
}
layout.dataOffset = int(chunkStart)
layout.dataSize = int(chunkSize)
}
paddedEnd := chunkEnd + (chunkSize & 1)
if paddedEnd > uint64(layout.riffEnd) {
return wavPCMLayout{}, errors.New("RIFF/WAVE chunk padding is truncated")
}
offset = int(paddedEnd)
}
if !sawFormat || layout.dataOffset < 0 {
return wavPCMLayout{}, errors.New("RIFF/WAVE is missing its format or data chunk")
}
return layout, nil
}
func wavSampleToPCM16(sample int, bitDepth uint16) (int16, error) {
switch bitDepth {
case 8:
return int16((sample - 128) << 8), nil
case 16:
return int16(sample), nil
case 24:
return int16(sample >> 8), nil
case 32:
return int16(sample >> 16), nil
default:
return 0, fmt.Errorf("unsupported WAV PCM bit depth %d", bitDepth)
}
}