Commit Diff


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)
 	}
 }