commit a0d3d6fb41f760d18ebc49d3ff47c5a97954f90c from: Oliver Lowe date: Sun Dec 22 12:13:27 2024 UTC wav: support format extension field It's an optional field, but aucat(1) from OpenBSD creates files with this field populated so I feel like it may not be that obscure in the wild. commit - d0a2114cd0f9dda0212b0195364e81fb5e69a68d commit + a0d3d6fb41f760d18ebc49d3ff47c5a97954f90c blob - f1b47d3c3c7c83af3ccdd5e90da8ee7d827d04ef blob + 21cda9fcfa3140b729d6912115dfa50edf958cbc --- wav/wav.go +++ wav/wav.go @@ -1,8 +1,12 @@ -// Package wav provides an encoder and decoder of WAVE data. -// https://en.wikipedia.org/wiki/WAV +// Package wav implements access to WAV (or WAVE) files. +// WAVE is a file format for storing digitised audio, +// most commonly as raw PCM signals. +// +// See also https://en.wikipedia.org/wiki/WAV package wav import ( + "bytes" "encoding/binary" "fmt" "io" @@ -13,102 +17,171 @@ type File struct { Bitstream io.Reader } +const ( + headerLength = 44 + extensionLength = 24 +) + +const ( + AudioFormatPCMInteger uint16 = 1 + AudioFormatFloat uint16 = 3 + AudioFormatExtensible uint16 = 0xfffe +) + +var ( + riffID = [4]byte{'R', 'I', 'F', 'F'} + waveID = [4]byte{'W', 'A', 'V', 'E'} +) + +var formatChunkID = [4]byte{'f', 'm', 't', ' '} + +var dataChunkID = [4]byte{'d', 'a', 't', 'a'} + type Header struct { FileSize uint32 - BlocSize uint32 AudioFormat uint16 ChannelCount uint16 Frequency uint32 BytesPerSecond uint32 BytesPerBloc uint16 BitsPerSample uint16 + Extension *FormatExtension DataSize uint32 } +func ReadFile(r io.Reader) (*File, error) { + h, err := readHeader(r) + if err != nil { + return nil, fmt.Errorf("read header: %w", err) + } + return &File{ + Header: Header{ + h.File.Length, + h.Format.AudioFormat, + h.Format.ChannelCount, + h.Format.Frequency, + h.Format.BytesPerSecond, + h.Format.BytesPerBloc, + h.Format.BitsPerSample, + h.FormatExtension, + h.Data.Length, + }, + Bitstream: r, + }, nil +} + +func EncodeHeader(hdr Header) []byte { + h := header{ + File: fileChunk{riffID, hdr.FileSize, waveID}, + Format: formatChunk{ + formatChunkID, + formatChunkLength, + hdr.AudioFormat, + hdr.ChannelCount, + hdr.Frequency, + hdr.BytesPerSecond, + hdr.BytesPerBloc, + hdr.BitsPerSample, + }, + FormatExtension: hdr.Extension, + Data: dataChunk{dataChunkID, hdr.DataSize}, + } + if h.Format.AudioFormat == AudioFormatExtensible { + h.Format.Length = extendedFormatChunkLength + } + + bcap := headerLength + if h.Format.AudioFormat == AudioFormatExtensible { + bcap += extensionLength + } + buf := bytes.NewBuffer(make([]byte, 0, bcap)) + binary.Write(buf, binary.LittleEndian, h.File) + binary.Write(buf, binary.LittleEndian, h.Format) + if h.FormatExtension != nil { + binary.Write(buf, binary.LittleEndian, h.FormatExtension) + } + binary.Write(buf, binary.LittleEndian, h.Data) + return buf.Bytes() +} + type header struct { - FileBlocID [4]byte - FileSize uint32 - FileFormatID [4]byte + File fileChunk + Format formatChunk + // Extension holds optional extra audio format information for + FormatExtension *FormatExtension + Data dataChunk +} - FormatBlocID [4]byte - BlocSize uint32 +type fileChunk struct { + ID [4]byte + Length uint32 + FormatID [4]byte +} + +const ( + formatChunkLength = 16 + extendedFormatChunkLength = formatChunkLength + extensionLength +) + +type formatChunk struct { + ID [4]byte + Length uint32 AudioFormat uint16 ChannelCount uint16 Frequency uint32 BytesPerSecond uint32 BytesPerBloc uint16 BitsPerSample uint16 +} - DataBlocID [4]byte - DataSize uint32 +type dataChunk struct { + ID [4]byte + Length uint32 } -var ( - fileBlocID = [4]byte{'R', 'I', 'F', 'F'} - fileFormatID = [4]byte{'W', 'A', 'V', 'E'} -) +type FormatExtension struct { + Length uint16 + ValidBits uint16 + ChannelMask uint32 + SubFormat [16]byte +} -var formatBlocID = [4]byte{'f', 'm', 't', ' '} - -var dataBlocID = [4]byte{'d', 'a', 't', 'a'} - func readHeader(rd io.Reader) (*header, error) { - var h header - if err := binary.Read(rd, binary.LittleEndian, &h); err != nil { - return nil, err + var head header + var fchunk fileChunk + if err := binary.Read(rd, binary.LittleEndian, &fchunk); err != nil { + return nil, fmt.Errorf("read file chunk: %w", err) } - if h.FileBlocID != fileBlocID { - return nil, fmt.Errorf("bad file block id %x", h.FileBlocID) - } else if h.FileFormatID != fileFormatID { - return nil, fmt.Errorf("bad file format id %x", h.FileFormatID) - } else if h.FormatBlocID != formatBlocID { - return nil, fmt.Errorf("bad format block id %x", h.FileFormatID) - } else if h.DataBlocID != dataBlocID { - return nil, fmt.Errorf("bad data block id %x", h.DataBlocID) + if fchunk.ID != riffID { + return nil, fmt.Errorf("bad RIFF id %x", fchunk.ID) + } else if fchunk.FormatID != waveID { + return nil, fmt.Errorf("bad WAVE file format id %x", fchunk.FormatID) } - return &h, nil -} + head.File = fchunk -func EncodeHeader(hdr Header) [44]byte { - var buf [44]byte - h := header{ - fileBlocID, - hdr.FileSize, - fileFormatID, - formatBlocID, - hdr.BlocSize, - hdr.AudioFormat, - hdr.ChannelCount, - hdr.Frequency, - hdr.BytesPerSecond, - hdr.BytesPerBloc, - hdr.BitsPerSample, - dataBlocID, - hdr.DataSize, + var fmtchunk formatChunk + if err := binary.Read(rd, binary.LittleEndian, &fmtchunk); err != nil { + return nil, fmt.Errorf("read file chunk: %w", err) } - binary.Encode(buf[:], binary.LittleEndian, h) - return buf -} + if fmtchunk.ID != formatChunkID { + return nil, fmt.Errorf("bad format chunk id %x", fmtchunk.ID) + } + head.Format = fmtchunk -func ReadFile(r io.Reader) (*File, error) { - h, err := readHeader(r) - if err != nil { - return nil, fmt.Errorf("read header: %w", err) + if fmtchunk.AudioFormat == AudioFormatExtensible { + var ext FormatExtension + if err := binary.Read(rd, binary.LittleEndian, &ext); err != nil { + return nil, fmt.Errorf("read format chunk extension: %w", err) + } + head.FormatExtension = &ext } - return &File{ - Header: Header{ - h.FileSize, - h.BlocSize, - h.AudioFormat, - h.ChannelCount, - h.Frequency, - h.BytesPerSecond, - h.BytesPerBloc, - h.BitsPerSample, - h.DataSize, - }, - Bitstream: r, - }, nil + + var data dataChunk + if err := binary.Read(rd, binary.LittleEndian, &data); err != nil { + return nil, fmt.Errorf("read data chunk: %w", err) + } + head.Data = data + return &head, nil } blob - 17d0e50710ffa13942ad76429c8fcc586e3d87e7 blob + acca55fc18743d5daf5d4b94da5f4f90ca2e2f74 --- wav/wav_test.go +++ wav/wav_test.go @@ -3,11 +3,12 @@ package wav import ( "io" "os" + "reflect" "testing" ) func TestDecodeEncode(t *testing.T) { - var source [44]byte + source := make([]byte, headerLength+extensionLength) f, err := os.Open("test.wav") if err != nil { t.Fatal(err) @@ -25,7 +26,7 @@ func TestDecodeEncode(t *testing.T) { t.Fatal(err) } header := EncodeHeader(file.Header) - if source != header { + if !reflect.DeepEqual(source, header) { t.Errorf("encode header: want %v, got %v", source, header) } }