├── files ├── a.wav ├── alaw.wav └── mulaw.wav ├── test_helper.go ├── .github └── workflows │ └── go.yml ├── go.mod ├── wav.go ├── LICENSE ├── README.md ├── writer.go ├── go.sum ├── reader_test.go ├── reader.go └── writer_test.go /files/a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/go-wav/HEAD/files/a.wav -------------------------------------------------------------------------------- /files/alaw.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/go-wav/HEAD/files/alaw.wav -------------------------------------------------------------------------------- /files/mulaw.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/go-wav/HEAD/files/mulaw.wav -------------------------------------------------------------------------------- /test_helper.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "runtime" 7 | ) 8 | 9 | func fixture(basename string) string { 10 | _, filename, _, _ := runtime.Caller(1) 11 | 12 | return path.Join(path.Dir(filename), "files", basename) 13 | } 14 | 15 | func fixtureFile(basename string) (file *os.File, err error) { 16 | file, err = os.Open(fixture(basename)) 17 | 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/youpy/go-wav 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | github.com/youpy/go-riff v0.1.0 8 | github.com/zaf/g711 v1.4.0 9 | gotest.tools v2.2.0+incompatible 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/google/go-cmp v0.5.6 // indirect 15 | github.com/pkg/errors v0.9.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /wav.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | const ( 8 | AudioFormatPCM = 1 9 | AudioFormatIEEEFloat = 3 10 | AudioFormatALaw = 6 11 | AudioFormatMULaw = 7 12 | ) 13 | 14 | type WavFormat struct { 15 | AudioFormat uint16 16 | NumChannels uint16 17 | SampleRate uint32 18 | ByteRate uint32 19 | BlockAlign uint16 20 | BitsPerSample uint16 21 | } 22 | 23 | type WavData struct { 24 | io.Reader 25 | Size uint32 26 | pos uint32 27 | } 28 | 29 | type Sample struct { 30 | Values [2]int 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 youpy 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-wav ![workflow status](https://github.com/youpy/go-wav/actions/workflows/go.yml/badge.svg) 2 | 3 | A Go library to read/write WAVE(RIFF waveform Audio) Format 4 | 5 | ## Usage 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "github.com/youpy/go-wav" 14 | "io" 15 | "os" 16 | ) 17 | 18 | func main() { 19 | infile_path := flag.String("infile", "", "wav file to read") 20 | flag.Parse() 21 | 22 | file, _ := os.Open(*infile_path) 23 | reader := wav.NewReader(file) 24 | 25 | defer file.Close() 26 | 27 | for { 28 | samples, err := reader.ReadSamples() 29 | if err == io.EOF { 30 | break 31 | } 32 | 33 | for _, sample := range samples { 34 | fmt.Printf("L/R: %d/%d\n", reader.IntValue(sample, 0), reader.IntValue(sample, 1)) 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | ## Supported format 41 | 42 | Format 43 | 44 | - PCM 45 | - IEEE float (read-only) 46 | - G.711 A-law (read-only) 47 | - G.711 µ-law (read-only) 48 | 49 | Number of channels 50 | 51 | - 1(mono) 52 | - 2(stereo) 53 | 54 | Bits per sample 55 | 56 | - 32-bit 57 | - 24-bit 58 | - 16-bit 59 | - 8-bit 60 | 61 | ## Documentation 62 | 63 | - https://godoc.org/github.com/youpy/go-wav 64 | 65 | ## See Also 66 | 67 | - http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html 68 | 69 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "io" 7 | "math" 8 | 9 | "github.com/youpy/go-riff" 10 | ) 11 | 12 | type Writer struct { 13 | io.Writer 14 | Format *WavFormat 15 | } 16 | 17 | func NewWriter(w io.Writer, numSamples uint32, numChannels uint16, sampleRate uint32, bitsPerSample uint16) (writer *Writer) { 18 | blockAlign := numChannels * bitsPerSample / 8 19 | byteRate := sampleRate * uint32(blockAlign) 20 | format := &WavFormat{AudioFormatPCM, numChannels, sampleRate, byteRate, blockAlign, bitsPerSample} 21 | dataSize := numSamples * uint32(format.BlockAlign) 22 | riffSize := 4 + 8 + 16 + 8 + dataSize 23 | riffWriter := riff.NewWriter(w, []byte("WAVE"), riffSize) 24 | 25 | writer = &Writer{riffWriter, format} 26 | riffWriter.WriteChunk([]byte("fmt "), 16, func(w io.Writer) { 27 | binary.Write(w, binary.LittleEndian, format) 28 | }) 29 | riffWriter.WriteChunk([]byte("data"), dataSize, func(w io.Writer) {}) 30 | 31 | return writer 32 | } 33 | 34 | func (w *Writer) WriteSamples(samples []Sample) (err error) { 35 | bitsPerSample := w.Format.BitsPerSample 36 | numChannels := w.Format.NumChannels 37 | bytesPerSample := int(bitsPerSample/8) * int(numChannels) 38 | by := make([]byte, 0, len(samples)*bytesPerSample) 39 | 40 | for _, sample := range samples { 41 | for i := uint16(0); i < numChannels; i++ { 42 | value := toUint(sample.Values[i], int(bitsPerSample)) 43 | for b := uint16(0); b < bitsPerSample; b += 8 { 44 | by = append(by, uint8((value>>b)&math.MaxUint8)) 45 | } 46 | } 47 | } 48 | 49 | bufWriter := bufio.NewWriter(w.Writer) 50 | _, err = bufWriter.Write(by) 51 | bufWriter.Flush() 52 | return err 53 | } 54 | 55 | func toUint(value int, bits int) uint { 56 | var result uint 57 | 58 | switch bits { 59 | case 32: 60 | result = uint(uint32(value)) 61 | case 16: 62 | result = uint(uint16(value)) 63 | case 8: 64 | result = uint(value) 65 | default: 66 | if value < 0 { 67 | result = uint((1 << uint(bits)) + value) 68 | } else { 69 | result = uint(value) 70 | } 71 | } 72 | 73 | return result 74 | } 75 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 5 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 7 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 13 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 14 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 16 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 17 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 18 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k= 20 | github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ= 21 | github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c= 22 | github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo= 23 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 24 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 31 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 32 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "math" 8 | "os" 9 | "testing" 10 | 11 | "gotest.tools/assert" 12 | ) 13 | 14 | func TestRead(t *testing.T) { 15 | blockAlign := 4 16 | 17 | file, err := fixtureFile("a.wav") 18 | if err != nil { 19 | t.Fatalf("Failed to open fixture file") 20 | } 21 | 22 | reader := NewReader(file) 23 | fmt, err := reader.Format() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | assert.Equal(t, AudioFormatPCM, int(fmt.AudioFormat)) 29 | assert.Equal(t, 2, int(fmt.NumChannels)) 30 | assert.Equal(t, 44100, int(fmt.SampleRate)) 31 | assert.Equal(t, 44100*4, int(fmt.ByteRate)) 32 | assert.Equal(t, blockAlign, int(fmt.BlockAlign)) 33 | assert.Equal(t, 16, int(fmt.BitsPerSample)) 34 | 35 | duration, err := reader.Duration() 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | assert.Equal(t, "1.381496598s", duration.String()) 41 | 42 | samples, err := reader.ReadSamples(1) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | assert.Equal(t, 1, len(samples)) 48 | 49 | sample := samples[0] 50 | 51 | assert.Equal(t, 318, reader.IntValue(sample, 0)) 52 | assert.Equal(t, 289, reader.IntValue(sample, 1)) 53 | assert.Assert(t, math.Abs(reader.FloatValue(sample, 0)-0.009705) <= 0.0001) 54 | assert.Assert(t, math.Abs(reader.FloatValue(sample, 1)-0.008820) <= 0.0001) 55 | 56 | bytes, err := ioutil.ReadAll(reader) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | assert.Equal(t, len(bytes), int(reader.WavData.Size)-(1*blockAlign)) 62 | 63 | t.Logf("Data size: %d", len(bytes)) 64 | } 65 | 66 | func TestReadMulaw(t *testing.T) { 67 | blockAlign := 1 68 | 69 | file, err := fixtureFile("mulaw.wav") 70 | if err != nil { 71 | t.Fatalf("Failed to open fixture file") 72 | } 73 | 74 | reader := NewReader(file) 75 | fmt, err := reader.Format() 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | assert.Equal(t, AudioFormatMULaw, int(fmt.AudioFormat)) 81 | assert.Equal(t, 1, int(fmt.NumChannels)) 82 | assert.Equal(t, 8000, int(fmt.SampleRate)) 83 | assert.Equal(t, 8000, int(fmt.ByteRate)) 84 | assert.Equal(t, blockAlign, int(fmt.BlockAlign)) 85 | assert.Equal(t, 8, int(fmt.BitsPerSample)) 86 | 87 | duration, err := reader.Duration() 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | assert.Equal(t, "4.59125s", duration.String()) 93 | 94 | samples, err := reader.ReadSamples(1) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | assert.Equal(t, 1, len(samples)) 100 | 101 | bytes, err := ioutil.ReadAll(reader) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | assert.Equal(t, len(bytes), int(reader.WavData.Size)-(1*blockAlign)) 107 | 108 | t.Logf("Data size: %d", len(bytes)) 109 | } 110 | 111 | func TestReadAlaw(t *testing.T) { 112 | blockAlign := 1 113 | 114 | file, err := fixtureFile("alaw.wav") 115 | if err != nil { 116 | t.Fatalf("Failed to open fixture file") 117 | } 118 | 119 | reader := NewReader(file) 120 | fmt, err := reader.Format() 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | assert.Equal(t, AudioFormatALaw, int(fmt.AudioFormat)) 126 | assert.Equal(t, 1, int(fmt.NumChannels)) 127 | assert.Equal(t, 8000, int(fmt.SampleRate)) 128 | assert.Equal(t, 8000, int(fmt.ByteRate)) 129 | assert.Equal(t, blockAlign, int(fmt.BlockAlign)) 130 | assert.Equal(t, 8, int(fmt.BitsPerSample)) 131 | 132 | duration, err := reader.Duration() 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | assert.Equal(t, "4.59125s", duration.String()) 138 | 139 | samples, err := reader.ReadSamples(1) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | assert.Equal(t, 1, len(samples)) 145 | 146 | bytes, err := ioutil.ReadAll(reader) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | 151 | assert.Equal(t, len(bytes), int(reader.WavData.Size)-(1*blockAlign)) 152 | 153 | t.Logf("Data size: %d", len(bytes)) 154 | } 155 | 156 | func BenchmarkReadSamples(b *testing.B) { 157 | n := []uint32{1, 10, 100, 1000, 2000, 3000, 5000, 8000, 10000, 20000, 40000} 158 | 159 | var t int 160 | 161 | for _, numSamples := range n { 162 | b.Run(fmt.Sprintf("%d", numSamples), func(b *testing.B) { 163 | for i := 0; i < b.N; i++ { 164 | 165 | file, _ := os.Open("./files/a.wav") 166 | reader := NewReader(file) 167 | 168 | for { 169 | samples, err := reader.ReadSamples(numSamples) 170 | if err == io.EOF { 171 | break 172 | } 173 | for _, sample := range samples { 174 | t += reader.IntValue(sample, 0) 175 | t += reader.IntValue(sample, 1) 176 | } 177 | } 178 | } 179 | }) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "math" 8 | "time" 9 | 10 | "github.com/youpy/go-riff" 11 | "github.com/zaf/g711" 12 | ) 13 | 14 | type Reader struct { 15 | r *riff.Reader 16 | riffChunk *riff.RIFFChunk 17 | format *WavFormat 18 | *WavData 19 | } 20 | 21 | func NewReader(r riff.RIFFReader) *Reader { 22 | riffReader := riff.NewReader(r) 23 | return &Reader{r: riffReader} 24 | } 25 | 26 | func (r *Reader) Format() (format *WavFormat, err error) { 27 | if r.format == nil { 28 | format, err = r.readFormat() 29 | if err != nil { 30 | return 31 | } 32 | r.format = format 33 | } else { 34 | format = r.format 35 | } 36 | 37 | return 38 | } 39 | 40 | func (r *Reader) Duration() (time.Duration, error) { 41 | format, err := r.Format() 42 | if err != nil { 43 | return 0.0, err 44 | } 45 | 46 | err = r.loadWavData() 47 | if err != nil { 48 | return 0.0, err 49 | } 50 | 51 | sec := float64(r.WavData.Size) / float64(format.BlockAlign) / float64(format.SampleRate) 52 | 53 | return time.Duration(sec*1000000000) * time.Nanosecond, nil 54 | } 55 | 56 | func (r *Reader) Read(p []byte) (n int, err error) { 57 | err = r.loadWavData() 58 | if err != nil { 59 | return n, err 60 | } 61 | 62 | return r.WavData.Read(p) 63 | } 64 | 65 | func (r *Reader) ReadSamples(params ...uint32) (samples []Sample, err error) { 66 | var bytes []byte 67 | var numSamples, b, n int 68 | 69 | if len(params) > 0 { 70 | numSamples = int(params[0]) 71 | } else { 72 | numSamples = 2048 73 | } 74 | 75 | format, err := r.Format() 76 | if err != nil { 77 | return 78 | } 79 | 80 | numChannels := int(format.NumChannels) 81 | blockAlign := int(format.BlockAlign) 82 | bitsPerSample := int(format.BitsPerSample) 83 | 84 | bytes = make([]byte, numSamples*blockAlign) 85 | n, err = r.Read(bytes) 86 | 87 | if err != nil { 88 | return 89 | } 90 | 91 | numSamples = n / blockAlign 92 | r.WavData.pos += uint32(numSamples * blockAlign) 93 | samples = make([]Sample, numSamples) 94 | offset := 0 95 | 96 | for i := 0; i < numSamples; i++ { 97 | for j := 0; j < numChannels; j++ { 98 | soffset := offset + (j * bitsPerSample / 8) 99 | 100 | switch format.AudioFormat { 101 | case AudioFormatIEEEFloat: 102 | bits := 103 | uint32((int(bytes[soffset+3]) << 24) + 104 | (int(bytes[soffset+2]) << 16) + 105 | (int(bytes[soffset+1]) << 8) + 106 | int(bytes[soffset])) 107 | samples[i].Values[j] = int(math.MaxInt32 * math.Float32frombits(bits)) 108 | 109 | case AudioFormatALaw: 110 | var val uint 111 | pcm := g711.DecodeAlaw(bytes[soffset : soffset+(bitsPerSample/8)]) 112 | for b = 0; b < len(pcm); b++ { 113 | val += uint(pcm[b]) << uint(b*8) 114 | } 115 | 116 | samples[i].Values[j] = toInt(val, bitsPerSample*2) 117 | 118 | case AudioFormatMULaw: 119 | var val uint 120 | pcm := g711.DecodeUlaw(bytes[soffset : soffset+(bitsPerSample/8)]) 121 | for b = 0; b < len(pcm); b++ { 122 | val += uint(pcm[b]) << uint(b*8) 123 | } 124 | 125 | samples[i].Values[j] = toInt(val, bitsPerSample*2) 126 | 127 | default: 128 | var val uint 129 | for b = 0; b*8 < bitsPerSample; b++ { 130 | val += uint(bytes[soffset+b]) << uint(b*8) 131 | } 132 | 133 | samples[i].Values[j] = toInt(val, bitsPerSample) 134 | } 135 | } 136 | 137 | offset += blockAlign 138 | } 139 | 140 | return 141 | } 142 | 143 | func (r *Reader) IntValue(sample Sample, channel uint) int { 144 | return sample.Values[channel] 145 | } 146 | 147 | func (r *Reader) FloatValue(sample Sample, channel uint) float64 { 148 | return float64(r.IntValue(sample, channel)) / math.Pow(2, float64(r.format.BitsPerSample)-1) 149 | } 150 | 151 | func (r *Reader) readFormat() (fmt *WavFormat, err error) { 152 | var riffChunk *riff.RIFFChunk 153 | 154 | fmt = new(WavFormat) 155 | 156 | if r.riffChunk == nil { 157 | riffChunk, err = r.r.Read() 158 | if err != nil { 159 | return 160 | } 161 | 162 | r.riffChunk = riffChunk 163 | } else { 164 | riffChunk = r.riffChunk 165 | } 166 | 167 | fmtChunk := findChunk(riffChunk, "fmt ") 168 | 169 | if fmtChunk == nil { 170 | err = errors.New("format chunk is not found") 171 | return 172 | } 173 | 174 | err = binary.Read(fmtChunk, binary.LittleEndian, fmt) 175 | if err != nil { 176 | return 177 | } 178 | 179 | return 180 | } 181 | 182 | func (r *Reader) loadWavData() error { 183 | if r.WavData == nil { 184 | data, err := r.readData() 185 | if err != nil { 186 | return err 187 | } 188 | r.WavData = data 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func (r *Reader) readData() (data *WavData, err error) { 195 | var riffChunk *riff.RIFFChunk 196 | 197 | if r.riffChunk == nil { 198 | riffChunk, err = r.r.Read() 199 | if err != nil { 200 | return 201 | } 202 | 203 | r.riffChunk = riffChunk 204 | } else { 205 | riffChunk = r.riffChunk 206 | } 207 | 208 | dataChunk := findChunk(riffChunk, "data") 209 | if dataChunk == nil { 210 | err = errors.New("data chunk is not found") 211 | return 212 | } 213 | 214 | data = &WavData{bufio.NewReader(dataChunk), dataChunk.ChunkSize, 0} 215 | 216 | return 217 | } 218 | 219 | func findChunk(riffChunk *riff.RIFFChunk, id string) (chunk *riff.Chunk) { 220 | for _, ch := range riffChunk.Chunks { 221 | if string(ch.ChunkID[:]) == id { 222 | chunk = ch 223 | break 224 | } 225 | } 226 | 227 | return 228 | } 229 | 230 | func toInt(value uint, bits int) int { 231 | var result int 232 | 233 | switch bits { 234 | case 32: 235 | result = int(int32(value)) 236 | case 16: 237 | result = int(int16(value)) 238 | case 8: 239 | result = int(value) 240 | default: 241 | msb := uint(1 << (uint(bits) - 1)) 242 | 243 | if value >= msb { 244 | result = -int((1 << uint(bits)) - value) 245 | } else { 246 | result = int(value) 247 | } 248 | } 249 | 250 | return result 251 | } 252 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestWrite(t *testing.T) { 13 | outfile, err := ioutil.TempFile("/tmp", "outfile") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | defer func() { 19 | outfile.Close() 20 | os.Remove(outfile.Name()) 21 | }() 22 | 23 | var numSamples uint32 = 2 24 | var numChannels uint16 = 2 25 | var sampleRate uint32 = 44100 26 | var bitsPerSample uint16 = 16 27 | 28 | writer := NewWriter(outfile, numSamples, numChannels, sampleRate, bitsPerSample) 29 | samples := make([]Sample, numSamples) 30 | 31 | samples[0].Values[0] = 32767 32 | samples[0].Values[1] = -32768 33 | samples[1].Values[0] = 123 34 | samples[1].Values[1] = -123 35 | 36 | err = writer.WriteSamples(samples) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | outfile.Close() 42 | file, err := os.Open(outfile.Name()) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | defer func() { 48 | file.Close() 49 | os.Remove(outfile.Name()) 50 | }() 51 | 52 | reader := NewReader(file) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | fmt, err := reader.Format() 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | assert.Equal(t, int(fmt.AudioFormat), AudioFormatPCM) 63 | assert.Equal(t, fmt.NumChannels, numChannels) 64 | assert.Equal(t, fmt.SampleRate, sampleRate) 65 | assert.Equal(t, fmt.ByteRate, sampleRate*4) 66 | assert.Equal(t, fmt.BlockAlign, numChannels*(bitsPerSample/8)) 67 | assert.Equal(t, fmt.BitsPerSample, bitsPerSample) 68 | 69 | samples, err = reader.ReadSamples() 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | assert.Equal(t, len(samples), 2) 75 | assert.Equal(t, samples[0].Values[0], 32767) 76 | assert.Equal(t, samples[0].Values[1], -32768) 77 | assert.Equal(t, samples[1].Values[0], 123) 78 | assert.Equal(t, samples[1].Values[1], -123) 79 | } 80 | 81 | func TestWrite8BitStereo(t *testing.T) { 82 | outfile, err := ioutil.TempFile("/tmp", "outfile") 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | defer func() { 88 | outfile.Close() 89 | os.Remove(outfile.Name()) 90 | }() 91 | 92 | var numSamples uint32 = 2 93 | var numChannels uint16 = 2 94 | var sampleRate uint32 = 44100 95 | var bitsPerSample uint16 = 8 96 | 97 | writer := NewWriter(outfile, numSamples, numChannels, sampleRate, bitsPerSample) 98 | samples := make([]Sample, numSamples) 99 | 100 | samples[0].Values[0] = 255 101 | samples[0].Values[1] = 0 102 | samples[1].Values[0] = 123 103 | samples[1].Values[1] = 234 104 | 105 | err = writer.WriteSamples(samples) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | outfile.Close() 111 | file, err := os.Open(outfile.Name()) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | 116 | defer func() { 117 | file.Close() 118 | os.Remove(outfile.Name()) 119 | }() 120 | 121 | reader := NewReader(file) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | fmt, err := reader.Format() 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | 131 | assert.Equal(t, int(fmt.AudioFormat), AudioFormatPCM) 132 | assert.Equal(t, fmt.NumChannels, numChannels) 133 | assert.Equal(t, fmt.SampleRate, sampleRate) 134 | assert.Equal(t, fmt.ByteRate, sampleRate*2) 135 | assert.Equal(t, fmt.BlockAlign, numChannels*(bitsPerSample/8)) 136 | assert.Equal(t, fmt.BitsPerSample, bitsPerSample) 137 | 138 | samples, err = reader.ReadSamples() 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | 143 | assert.Equal(t, len(samples), 2) 144 | assert.Equal(t, samples[0].Values[0], 255) 145 | assert.Equal(t, samples[0].Values[1], 0) 146 | assert.Equal(t, samples[1].Values[0], 123) 147 | assert.Equal(t, samples[1].Values[1], 234) 148 | } 149 | 150 | func TestWrite24BitStereo(t *testing.T) { 151 | outfile, err := ioutil.TempFile("/tmp", "outfile") 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | defer func() { 157 | outfile.Close() 158 | os.Remove(outfile.Name()) 159 | }() 160 | 161 | var numSamples uint32 = 2 162 | var numChannels uint16 = 2 163 | var sampleRate uint32 = 44100 164 | var bitsPerSample uint16 = 24 165 | 166 | writer := NewWriter(outfile, numSamples, numChannels, sampleRate, bitsPerSample) 167 | samples := make([]Sample, numSamples) 168 | 169 | samples[0].Values[0] = 32767 170 | samples[0].Values[1] = -32768 171 | samples[1].Values[0] = 123 172 | samples[1].Values[1] = -123 173 | 174 | err = writer.WriteSamples(samples) 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | outfile.Close() 180 | file, err := os.Open(outfile.Name()) 181 | if err != nil { 182 | t.Fatal(err) 183 | } 184 | 185 | defer func() { 186 | file.Close() 187 | os.Remove(outfile.Name()) 188 | }() 189 | 190 | reader := NewReader(file) 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | fmt, err := reader.Format() 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | 200 | assert.Equal(t, int(fmt.AudioFormat), AudioFormatPCM) 201 | assert.Equal(t, fmt.NumChannels, numChannels) 202 | assert.Equal(t, fmt.SampleRate, sampleRate) 203 | assert.Equal(t, fmt.ByteRate, sampleRate*6) 204 | assert.Equal(t, fmt.BlockAlign, numChannels*(bitsPerSample/8)) 205 | assert.Equal(t, fmt.BitsPerSample, bitsPerSample) 206 | 207 | samples, err = reader.ReadSamples() 208 | if err != nil { 209 | t.Fatal(err) 210 | } 211 | 212 | assert.Equal(t, len(samples), 2) 213 | assert.Equal(t, samples[0].Values[0], 32767) 214 | assert.Equal(t, samples[0].Values[1], -32768) 215 | assert.Equal(t, samples[1].Values[0], 123) 216 | assert.Equal(t, samples[1].Values[1], -123) 217 | } 218 | 219 | func TestWrite32BitStereo(t *testing.T) { 220 | outfile, err := ioutil.TempFile("/tmp", "outfile") 221 | if err != nil { 222 | t.Fatal(err) 223 | } 224 | 225 | defer func() { 226 | outfile.Close() 227 | os.Remove(outfile.Name()) 228 | }() 229 | 230 | var numSamples uint32 = 2 231 | var numChannels uint16 = 2 232 | var sampleRate uint32 = 44100 233 | var bitsPerSample uint16 = 32 234 | 235 | writer := NewWriter(outfile, numSamples, numChannels, sampleRate, bitsPerSample) 236 | samples := make([]Sample, numSamples) 237 | 238 | samples[0].Values[0] = 32767 239 | samples[0].Values[1] = -32768 240 | samples[1].Values[0] = 123 241 | samples[1].Values[1] = -123 242 | 243 | err = writer.WriteSamples(samples) 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | 248 | outfile.Close() 249 | file, err := os.Open(outfile.Name()) 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | 254 | defer func() { 255 | file.Close() 256 | os.Remove(outfile.Name()) 257 | }() 258 | 259 | reader := NewReader(file) 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | 264 | fmt, err := reader.Format() 265 | if err != nil { 266 | t.Fatal(err) 267 | } 268 | 269 | assert.Equal(t, int(fmt.AudioFormat), AudioFormatPCM) 270 | assert.Equal(t, fmt.NumChannels, numChannels) 271 | assert.Equal(t, fmt.SampleRate, sampleRate) 272 | assert.Equal(t, fmt.ByteRate, sampleRate*8) 273 | assert.Equal(t, fmt.BlockAlign, numChannels*(bitsPerSample/8)) 274 | assert.Equal(t, fmt.BitsPerSample, bitsPerSample) 275 | 276 | samples, err = reader.ReadSamples() 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | 281 | assert.Equal(t, len(samples), 2) 282 | assert.Equal(t, samples[0].Values[0], 32767) 283 | assert.Equal(t, samples[0].Values[1], -32768) 284 | assert.Equal(t, samples[1].Values[0], 123) 285 | assert.Equal(t, samples[1].Values[1], -123) 286 | } 287 | 288 | func BenchmarkWriteSamples(b *testing.B) { 289 | n := 4096 290 | samples := []Sample{} 291 | 292 | file, _ := os.Open("./files/a.wav") 293 | reader := NewReader(file) 294 | 295 | for { 296 | spls, err := reader.ReadSamples(uint32(n)) 297 | if err == io.EOF { 298 | break 299 | } 300 | samples = append(samples, spls...) 301 | } 302 | 303 | b.Run("write samples", func(b *testing.B) { 304 | for i := 0; i < b.N; i++ { 305 | tmpfile, err := ioutil.TempFile("", "example") 306 | if err != nil { 307 | b.Fatal(err) 308 | } 309 | defer os.Remove(tmpfile.Name()) 310 | writer := NewWriter(tmpfile, uint32(len(samples)), 2, 44100, 16) 311 | writer.WriteSamples(samples) 312 | } 313 | }) 314 | } 315 | --------------------------------------------------------------------------------