147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
const (
|
|
maxInputPCMBytes = 15 << 20
|
|
maxOutputPCMBytes = 64 << 20
|
|
wavHeaderSize = 44
|
|
)
|
|
|
|
func readPCM16WAV(path string) ([]byte, error) {
|
|
fileInfo, errStat := os.Stat(path)
|
|
if errStat != nil {
|
|
return nil, errStat
|
|
}
|
|
if fileInfo.Size() > maxInputPCMBytes+(1<<20) {
|
|
return nil, fmt.Errorf("WAV file is too large: %d bytes", fileInfo.Size())
|
|
}
|
|
payload, errRead := os.ReadFile(path)
|
|
if errRead != nil {
|
|
return nil, errRead
|
|
}
|
|
if len(payload) < 12 || string(payload[:4]) != "RIFF" || string(payload[8:12]) != "WAVE" {
|
|
return nil, errors.New("input is not a RIFF/WAVE file")
|
|
}
|
|
|
|
var formatFound bool
|
|
var audioFormat uint16
|
|
var channels uint16
|
|
var sampleRate uint32
|
|
var bitsPerSample uint16
|
|
var pcm bytes.Buffer
|
|
for offset := 12; offset+8 <= len(payload); {
|
|
chunkID := string(payload[offset : offset+4])
|
|
chunkSize := int(binary.LittleEndian.Uint32(payload[offset+4 : offset+8]))
|
|
chunkStart := offset + 8
|
|
chunkEnd := chunkStart + chunkSize
|
|
if chunkSize < 0 || chunkEnd < chunkStart || chunkEnd > len(payload) {
|
|
return nil, fmt.Errorf("invalid WAV %q chunk size", chunkID)
|
|
}
|
|
switch chunkID {
|
|
case "fmt ":
|
|
if chunkSize < 16 {
|
|
return nil, errors.New("WAV fmt chunk is too short")
|
|
}
|
|
audioFormat = binary.LittleEndian.Uint16(payload[chunkStart : chunkStart+2])
|
|
channels = binary.LittleEndian.Uint16(payload[chunkStart+2 : chunkStart+4])
|
|
sampleRate = binary.LittleEndian.Uint32(payload[chunkStart+4 : chunkStart+8])
|
|
bitsPerSample = binary.LittleEndian.Uint16(payload[chunkStart+14 : chunkStart+16])
|
|
formatFound = true
|
|
case "data":
|
|
if pcm.Len()+chunkSize > maxInputPCMBytes {
|
|
return nil, fmt.Errorf("WAV PCM data exceeds %d bytes", maxInputPCMBytes)
|
|
}
|
|
_, _ = pcm.Write(payload[chunkStart:chunkEnd])
|
|
}
|
|
offset = chunkEnd
|
|
if chunkSize%2 != 0 {
|
|
offset++
|
|
}
|
|
}
|
|
if !formatFound {
|
|
return nil, errors.New("WAV fmt chunk is missing")
|
|
}
|
|
if audioFormat != 1 {
|
|
return nil, fmt.Errorf("WAV audio format must be PCM (1), got %d", audioFormat)
|
|
}
|
|
if channels != 1 {
|
|
return nil, fmt.Errorf("WAV must be mono, got %d channels", channels)
|
|
}
|
|
if sampleRate != audioSampleRate {
|
|
return nil, fmt.Errorf("WAV sample rate must be %d Hz, got %d Hz", audioSampleRate, sampleRate)
|
|
}
|
|
if bitsPerSample != 16 {
|
|
return nil, fmt.Errorf("WAV must use 16-bit samples, got %d bits", bitsPerSample)
|
|
}
|
|
if pcm.Len() == 0 {
|
|
return nil, errors.New("WAV data chunk is empty or missing")
|
|
}
|
|
if pcm.Len()%audioBytesPerSample != 0 {
|
|
return nil, errors.New("WAV PCM data contains an incomplete sample")
|
|
}
|
|
return append([]byte(nil), pcm.Bytes()...), nil
|
|
}
|
|
|
|
func writePCM16WAV(path string, pcm []byte) error {
|
|
if len(pcm) == 0 {
|
|
return errors.New("cannot write an empty WAV response")
|
|
}
|
|
if len(pcm) > maxOutputPCMBytes {
|
|
return fmt.Errorf("response PCM data exceeds %d bytes", maxOutputPCMBytes)
|
|
}
|
|
if len(pcm)%audioBytesPerSample != 0 {
|
|
return errors.New("response PCM data contains an incomplete sample")
|
|
}
|
|
|
|
var payload bytes.Buffer
|
|
payload.Grow(wavHeaderSize + len(pcm))
|
|
writeString := func(value string) error {
|
|
_, errWrite := payload.WriteString(value)
|
|
return errWrite
|
|
}
|
|
writeValue := func(value any) error {
|
|
return binary.Write(&payload, binary.LittleEndian, value)
|
|
}
|
|
if errWrite := writeString("RIFF"); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeValue(uint32(36 + len(pcm))); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeString("WAVEfmt "); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
for _, value := range []any{
|
|
uint32(16),
|
|
uint16(1),
|
|
uint16(1),
|
|
uint32(audioSampleRate),
|
|
uint32(audioSampleRate * audioBytesPerSample),
|
|
uint16(audioBytesPerSample),
|
|
uint16(16),
|
|
} {
|
|
if errWrite := writeValue(value); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
if errWrite := writeString("data"); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeValue(uint32(len(pcm))); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if _, errWrite := payload.Write(pcm); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := os.WriteFile(path, payload.Bytes(), 0o644); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
return nil
|
|
}
|