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,85 +17,40 @@ 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 } -type header struct { - FileBlocID [4]byte - FileSize uint32 - FileFormatID [4]byte - - FormatBlocID [4]byte - BlocSize uint32 - AudioFormat uint16 - ChannelCount uint16 - Frequency uint32 - BytesPerSecond uint32 - BytesPerBloc uint16 - BitsPerSample uint16 - - DataBlocID [4]byte - DataSize uint32 -} - -var ( - fileBlocID = [4]byte{'R', 'I', 'F', 'F'} - fileFormatID = [4]byte{'W', 'A', 'V', 'E'} -) - -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 - } - 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) - } - return &h, nil -} - -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, - } - binary.Encode(buf[:], binary.LittleEndian, h) - return buf -} - func ReadFile(r io.Reader) (*File, error) { h, err := readHeader(r) if err != nil { @@ -99,16 +58,130 @@ func ReadFile(r io.Reader) (*File, error) { } return &File{ Header: Header{ - h.FileSize, - h.BlocSize, - h.AudioFormat, - h.ChannelCount, - h.Frequency, - h.BytesPerSecond, - h.BytesPerBloc, - h.BitsPerSample, - h.DataSize, + 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 { + File fileChunk + Format formatChunk + // Extension holds optional extra audio format information for + FormatExtension *FormatExtension + Data dataChunk +} + +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 +} + +type dataChunk struct { + ID [4]byte + Length uint32 +} + +type FormatExtension struct { + Length uint16 + ValidBits uint16 + ChannelMask uint32 + SubFormat [16]byte +} + +func readHeader(rd io.Reader) (*header, error) { + 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 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) + } + head.File = fchunk + + var fmtchunk formatChunk + if err := binary.Read(rd, binary.LittleEndian, &fmtchunk); err != nil { + return nil, fmt.Errorf("read file chunk: %w", err) + } + if fmtchunk.ID != formatChunkID { + return nil, fmt.Errorf("bad format chunk id %x", fmtchunk.ID) + } + head.Format = fmtchunk + + 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 + } + + 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) } }